Nectar10.psm1

<#
These series of PowerShell functions allow administrators to automate many functions in Nectar DXP and the Endpoint Client Controller that are otherwise difficult or time-consuming to perform in the UI.
 
To install, follow the instructions at https://support.nectarcorp.com/docs/powershell
 
If you want help with a particular command, type Get-Help <commandname> -Full (ie. Get-Help Connect-NectarCloud -Full). Full documentation is available at https://support.nectarcorp.com/docs/powershell
#>



#################################################################################################################################################
#################################################################################################################################################
## ##
## Nectar DXP Functions ##
## ##
#################################################################################################################################################
#################################################################################################################################################

#################################################################################################################################################
# #
# Tenant Connection Functions #
# #
#################################################################################################################################################

Function Connect-NectarCloud {
    <#
        .SYNOPSIS
        Connects to Nectar DXP cloud and store the credentials for later use.
  
        .DESCRIPTION
        Connects to Nectar DXP cloud and store the credentials for later use.
          
        .PARAMETER CloudFQDN
        The FQDN of the Nectar DXP cloud.
  
        .PARAMETER TenantName
        The name of a Nectar DXP cloud tenant to connect to and use for subsequent commands. Only useful for multi-tenant deployments
          
        .PARAMETER Credential
        The credentials used to access the Nectar DXP UI. Normally in username@domain.com format
          
        .PARAMETER CredSecret
        Use stored credentials saved via Set-Secret. Requires prior installation of Microsoft.PowerShell.SecretManagement PS module and an appropriate
        secret vault, such as Microsoft.PowerShell.SecretStore. Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on
        the local machine. This is the minimum requirement for using this feature.
        Install the modules by running:
        Install-Module Microsoft.PowerShell.SecretManagement
        Install-Module Microsoft.PowerShell.SecretStore
 
        Register a credential secret by doing something like: Set-Secret -Name NectarCreds -Vault SecretStore -Secret (Get-Credential)
          
        .PARAMETER EnvFromFile
        Use a CSV file called N10EnvList.csv located in the user's default Documents folder to show a list of environments to select from.
        Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if N10EnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default credentials to use for the selected environment. This feature uses the Microsoft.PowerShell.SecretManagement PS module,
        which must be installed and configured with a secret store prior to using this option.
        N10EnvList.csv must have a header with three columns defined as "Environment, DefaultTenant, Secret".
        Each environment and Secret (if used) should be on their own separate lines
 
        .PARAMETER UseToken
        Use a JWT (JSON web token) to connect to Nectar DXP instead of using credentials. This feature uses the Microsoft.PowerShell.SecretManagement PS module,
        which must be installed and configured with a secret store prior to using this option.
        The PS SecretManagement module can use any number of 3rd party secret stores that provide access to centralized secret management tools, such as Keeper and AWS Secrets.
        Locally, the Microsoft.PowerShell.SecretStore can be used to store secrets securely on the local machine. This is the minimum requirement for using this feature.
        Install the modules by running:
        Install-Module Microsoft.PowerShell.SecretManagement
        Install-Module Microsoft.PowerShell.SecretStore
 
        When -UseToken is selected, the function will check for a secret called <envname>-accesstoken (ie contoso.nectar.services-accesstoken).
        The secret must contain two fields called AccessToken and RefreshToken and must be writable.
        The token itself can be generated in the Nectar DXP UI or via New-NectarToken (when logged in with a local account).
        The New-NectarTokenRegistration can be used to generate a token using the default secret store (if supported by the secret store).
        If using the default Microsoft SecretStore, you can generate a token and save it as a secret on the local machine by running:
        New-NectarToken -TokenName <tokenname> | New-NectarTokenRegistration -CloudFQDN <NectarDXPFQDN>
        ie. New-NectarToken -TokenName laptop | New-NectarTokenRegistration -CloudFQDN contoso.nectar.services
 
        .PARAMETER TokenIdentifier
        An optional unique identifier (such as script name or username) to use when retreiving secrets from a secret store shared by multiple parties/scripts
          
        .EXAMPLE
        $Cred = Get-Credential
        Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar DXP cloud using the credentials supplied to the Get-Credential command
          
        .EXAMPLE
        Connect-NectarCloud -CloudFQDN contoso.nectar.services -CredSecret MyNectarCreds
        Connects to contoso.nectar.services Nectar DXP cloud using previously stored secret called MyNectarCreds
 
        .EXAMPLE
        Connect-NectarCloud -CloudFQDN contoso.nectar.services -UseToken
        Connects to contoso.nectar.services Nectar DXP cloud using previously stored token stored in a Microsoft Secret Vault called contoso.nectar.services-accesstoken
 
        .NOTES
        Version 2.0
    #>

    
    [Alias("cnc")]
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                $True
            } 
            Else {
                Throw "ERROR: Nectar DXP cloud name must be in FQDN format."
            }
        })]
        [string]$CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential]$Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CredSecret,
        [Parameter(Mandatory=$False)]
        [switch]$UseToken
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\N10EnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }
    
    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $CloudFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "CloudFQDN: $CloudFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($CloudFQDN)
                
                # Check for default tenant in N10EnvList.csv and use if available, but don't override if user explicitly set the TenantName
                If (!$PsBoundParameters['TenantName']) {
                    $TenantName = $EnvSet[$EnvPos].DefaultTenant
                    Write-Verbose "DefaultTenant: $TenantName"
                }
                
                # Check for stored credential target in N10EnvList.csv and use if available
                $CredSecret = $EnvSet[$EnvPos].CredSecret
                Write-Verbose "Secret: $CredSecret"
            }
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line
        If ((-not $Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = Read-Host "Enter the Nectar DXP cloud FQDN"
            $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:?#[\]@!\$&'\(\)\*\+,;=.]+)"
            $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
            $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        }
        ElseIf (($Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = $Global:NectarCloud
        }
        
        # Ask for credentials if global Nectar creds aren't available
        If (((-not $Global:NectarCred) -And (-not $Credential)) -Or (($Global:NectarCloud -ne $CloudFQDN) -And (-Not $Credential)) -And (-Not $CredSecret) -And (-Not $UseToken) -And (-Not $Global:NectarSecretName)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:NectarCred -And (-not $Credential)) {
            $Credential = $Global:NectarCred
        }

        # Set the token name based on the inputs
        If ($TokenIdentifier) {
            $NectarSecretName = "$($CloudFQDN)-$($TokenIdentifier)-accesstoken"
        }
        ElseIf ($UseToken) {
            $NectarSecretName = "$($CloudFQDN)-accesstoken"
        }

        # Check secret store for stored token
        If ($Global:NectarSecretName -ne $NectarSecretName -And $UseToken) {
            Try {
                $Global:NectarDefaultVault = (Get-SecretVault | Where-Object {$_.IsDefault -eq $True}).Name
                $Global:NectarToken = Get-Secret -Name $NectarSecretName -AsPlainText -Vault $Global:NectarDefaultVault -ErrorAction SilentlyContinue #Stop

                # If no secret name was found, try to escape the dots with a \. Explicitly required for Keeper vaults, but may be used by others
                If (-Not $Global:NectarToken) {
                    $NectarSecretName = $NectarSecretName.Replace('.','\.')
                    $Global:NectarToken = Get-Secret -Name $NectarSecretName -AsPlainText -Vault $Global:NectarDefaultVault -ErrorAction Stop
                }
                $Global:NectarSecretName = $NectarSecretName        
            }
            Catch {
                Throw "Could not find access token for $CloudFQDN. Run New-NectarTokenRegistration to create one."
                Return
            }
        }
        
        # Pull stored credentials if specified
        If ($CredSecret) {
            Try {
                $Credential = Get-Secret $CredSecret
            }
            Catch {
                Throw "Cannot find secret: $CredSecret"
            }
        }
        
        # Only run on first execution of Connect-NectarCloud or if the CloudFQDN has changed
        If ((-Not $Global:NectarCred -And -Not $Global:NectarSecretName) -Or (-Not $Global:NectarCloud) -Or ($Global:NectarCloud -ne $CloudFQDN)) {
            # Check and notify if updated Nectar PS module available
            [string]$InstalledNectarPSVer = (Get-InstalledModule -Name Nectar10 -ErrorAction SilentlyContinue).Version
            
            If ($InstalledNectarPSVer -gt 0) {
                [string]$LatestNectarPSVer = (Find-Module Nectar10).Version
                If ($LatestNectarPSVer -gt $InstalledNectarPSVer) {
                    Write-Host "=============== Nectar PowerShell module version $LatestNectarPSVer available ===============" -ForegroundColor Yellow
                    Write-Host "You are running version $InstalledNectarPSVer. Type " -ForegroundColor Yellow -NoNewLine
                    Write-Host 'Update-Module Nectar10' -ForegroundColor Green -NoNewLine
                    Write-Host ' to update. NOTE: Close and reopen the PowerShell window for any module update to take effect.' -ForegroundColor Yellow
                }
            }
            
            # Create authorization header
            If ($UseToken) { # -Or $Global:NectarSecretName) { # Refresh token and create auth header
                Try {
                    $RefreshHeaders = @{
                        'x-refresh-token'    = $Global:NectarToken.RefreshToken
                        'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
                    }

                    Write-Verbose 'Refreshing token'
                    $WebRequest = Invoke-WebRequest -Uri "https://$CloudFQDN/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders -UseBasicParsing
                    $Global:NectarToken.AccessToken = ($WebRequest.Content | ConvertFrom-JSON).AccessToken
                    $Global:NectarTokenRefreshTime = Get-Date

                    If ($Global:NectarDefaultVault -eq 'Keeper') {
                        Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.AccessToken
                    }
                    Else {
                        Set-Secret -Name $Global:NectarSecretName -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken
                    }

                    $Headers = @{ 'Authorization' = "Bearer $($Global:NectarToken.AccessToken)" }
                    $ConnectionString = $NectarSecretName.Replace('\.','.')
                    # Remove credential global variables
                    Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue
                }
                Catch {
                    Throw "Could not refresh token for $($Global:NectarSecretName). Check that the token exists."
                }                
            }
            Else { # Create basic auth header
                $UserName = $Credential.UserName
                $Password = $Credential.GetNetworkCredential().Password
                $Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($UserName):$($Password)"))
                $Headers = @{Authorization = "Basic $Base64AuthInfo"}
                $ConnectionString = $Credential.UserName
            }

            # Attempt connection to tenant
            $URI = "https://$CloudFQDN/dapi/info/network/types"
            Write-Verbose $URI
            $WebRequest = Invoke-WebRequest -Uri $URI -Method GET -Headers $Headers -UseBasicParsing -SessionVariable NectarSession

            If ($WebRequest.StatusCode -ne 200) {
                Throw "Could not connect to $CloudFQDN using $ConnectionString"
            }
            Else {
                Write-Host -ForegroundColor Green "Successful connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$CloudFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow $ConnectionString
                $Global:NectarCloud         = $CloudFQDN
                $Global:NectarCred             = $Credential
                $Global:NectarAuthHeader    = $Headers

                $UserInfo = Invoke-RestMethod -Uri "https://$Global:NectarCloud/adminapi/user" -Method GET -Headers $Global:NectarAuthHeader -WebSession $NectarSession

                # If there is only one available tenant, assign that to the NectarTenantName global variable
                $Global:NectarTenantList = $UserInfo.tenants.tenant
                If ($Global:NectarTenantList.Count -eq 1) { $Global:NectarTenantName = $Global:NectarTenantList }
            }
        }

        # If token was used, check the last refresh time and update it if its more than 90 minutes old
        If ($Global:NectarToken -And (New-TimeSpan -Start $Global:NectarTokenRefreshTime -End (Get-Date)).TotalMinutes -gt 90) {
            $RefreshHeaders = @{
                'x-refresh-token'    = $Global:NectarToken.RefreshToken
                'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
            }
            
            Write-Verbose 'Refreshing token'
            $WebRequest = Invoke-WebRequest -Uri "https://$($Global:NectarCloud)/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders -UseBasicParsing -SessionVariable NectarSession
            $Global:NectarToken.AccessToken = ($WebRequest.Content | ConvertFrom-JSON).AccessToken
            $Global:NectarTokenRefreshTime = Get-Date

            If ($Global:NectarDefaultVault -eq 'Keeper') {
                Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken.AccessToken
            }
            Else {
                Set-Secret -Name $Global:NectarSecretName -Vault $Global:NectarDefaultVault -Secret $Global:NectarToken
            }

            $Headers = @{
                'authorization'    = "Bearer $($Global:NectarToken.AccessToken)"
            }

            $Global:NectarAuthHeader = $Headers
        }

        # Check to see if tenant name was entered and set global variable, if valid.
        If ($TenantName) {
            Try {
                If ($Global:NectarTenantList -Contains $TenantName) {
                    $Global:NectarTenantName = $TenantName.ToLower()
                    Write-Host -ForegroundColor Green "Successsfully set the tenant name to " -NoNewLine
                    Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine
                    Write-Host -ForegroundColor Green ". This tenantname will be used in all subsequent commands."
                }
                Else {
                    $Global:NectarTenantList | ForEach-Object{ $TList += ($(if($TList){", "}) + $_) }
                    Write-Error "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList"
                }
            }
            Catch {
                # Just set the tenant name if we are unable to validate the tenant name.
                $Global:NectarTenantName = $TenantName
                Write-Host -ForegroundColor Green "Set the tenant name to " -NoNewLine
                Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine
                Write-Host -ForegroundColor Green " but unable to verify if the tenant exists. This tenantname will be used in all subsequent commands."
            }
        }
        ElseIf ($PSBoundParameters.ContainsKey('TenantName')) { # Remove the NectarTenantName global variable only if TenantName is explicitly set to NULL
            Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue
        }
    }
}




Function Disconnect-NectarCloud {
    <#
        .SYNOPSIS
        Disconnects from any active Nectar DXP connection
         
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables
 
        .EXAMPLE
        Disconnect-NectarCloud
        Disconnects from all active connections to Nectar DXP tenants
 
        .NOTES
        Version 1.1
    #>

    [Alias("dnc")]
    [cmdletbinding()]
    param ()
    
    Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarCloud -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTenantList -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarAuthHeader -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarSecretName -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarToken -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTokenRefreshTime -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarDefaultVault -Scope Global -ErrorAction:SilentlyContinue
}





Function Get-NectarToken {
    <#
        .SYNOPSIS
        Returns a list of all JWT tokens assigned to the logged in user
         
        .DESCRIPTION
        Returns a list of all JWT tokens assigned to the logged in user
         
        .EXAMPLE
        Get-NectarToken
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param ()

    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/jwt/token"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON
        }
        Catch {
        }
    }
}




Function New-NectarToken {
    <#
        .SYNOPSIS
        Creates a new JSON web token (JWT) to be used in scripts
         
        .DESCRIPTION
        Creates a new JSON web token (JWT) to be used in scripts
 
        .PARAMETER TokenName
        A descriptive name for the token
 
        .EXAMPLE
        New-NectarToken -TokenName 'ScriptToken'
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param (
        [Parameter(Mandatory=$True)]
        [string]$TokenName
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/jwt/token?tokenName=$TokenName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON
        }
        Catch {
            Write-Error "Could not create access token."
        }
    }
}




Function Update-NectarToken {
    <#
        .SYNOPSIS
        Refreshes an expired Nectar token
         
        .DESCRIPTION
        Refreshes the currently used Nectar token
 
        .EXAMPLE
        Update-NectarToken
 
        .NOTES
        Version 1.0
    #>

    $RefreshHeaders = @{
        'x-refresh-token'    = $Global:NectarToken.RefreshToken
        'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
    }

    Try {
        $Global:NectarToken.AccessToken = Invoke-RestMethod -Uri "https://$Global:NectarCloud/aapi/jwt/token/renew" -Method POST -Headers $RefreshHeaders
        
        $Headers = @{
            'authorization'        = "Bearer $($Global:NectarToken.AccessToken)"
        }
        
        $Global:NectarAuthHeader = $Headers
        Set-Secret -Name "$($Global:NectarSecretName).AccessToken" -Secret $Global:NectarToken.AccessToken
        Write-Host -ForegroundColor Green "Successfully updated " -NoNewLine
        Write-Host -ForegroundColor Yellow $Global:NectarSecretName
    }
    Catch {
        Throw "Error refreshing $($Global:NectarSecretName)"
    }
}




Function Remove-NectarToken {
    <#
        .SYNOPSIS
        Remove a Nectar token
         
        .DESCRIPTION
        Remove a Nectar token
 
        .PARAMETER RefreshToken
        The GUID of a refresh token to remove.
 
        .EXAMPLE
        Remove-NectarToken -AccessToken fd173c75-891c-4357-b5a3-0855c2a56299
 
        .EXAMPLE
        Get-NectarToken | Where-Object {$_.Name -eq 'Testing'} | Remove-NectarToken
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$RefreshToken
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        $RefreshHeaders =  $Global:NectarAuthHeader
        $RefreshHeaders['x-refresh-token'] = $RefreshToken

        Try {
            $URI = "https://$Global:NectarCloud/aapi/jwt/token/revoke"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method DELETE -uri $URI -Headers $RefreshHeaders
            Return "Total number of tokens removed: $JSON"
        }
        Catch {
            Write-Error "Could not delete access token with refresh token $($RefreshToken)."
        }
    }
}





Function New-NectarTokenRegistration {
    <#
        .SYNOPSIS
        Registers a Nectar DXP token for connecting to Nectar DXP using JWT
 
        .DESCRIPTION
        Registers a Nectar DXP token for connecting to Nectar DXP using JWT. One-time task required before attempting to access Nectar DXP APIs.
        Stored using Microsoft SecretManagement PS module.
        If SecretManagement PS module is is not installed, install via:
        Install-Module Microsoft.PowerShell.SecretManagement
 
        There are several PS modules that connect to different secret providers. Install the one appropriate for your situation prior to running this command.
        For example, the PS SecretStore stores secrets on the local machine and can be installed via:
        Install-Module Microsoft.PowerShell.SecretStore
         
        .PARAMETER CloudFQDN
        The FQDN of the Nectar DXP cloud.
 
        .PARAMETER Identifier
        An optional unique identifier (such as script name or username) to use when saving secrets to a secret store shared by multiple parties/scripts
 
        .PARAMETER AccessToken
        The access token to use for connecting to Nectar DXP
 
        .PARAMETER RefreshToken
        The refresh token used to refresh the Nectar DXP access token every 2 hours
 
        .PARAMETER SecretVault
        The name of the secret vault to install the secret. Use if installing to non-default secret vault
 
        .EXAMPLE
        Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar DXP cloud using the credentials supplied to the Get-Credential command
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$True)]
        [ValidateScript ({
            If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                $True
            } 
            Else {
                Throw "ERROR: Nectar DXP cloud name must be in FQDN format."
            }
        })]
        [string]$CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Identifier,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$AccessToken,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$RefreshToken,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SecretVault
    )
    
    Begin {
        # Verify the SecretManagement module is installed
        If (!(Get-InstalledModule -Name 'Microsoft.PowerShell.SecretManagement' -ErrorAction SilentlyContinue)) {
            Throw "SecretManagement module not installed. Please install using 'Install-Module Microsoft.PowerShell.SecretManagement'"
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }

        # Make sure CloudFQDN does not have extraneous characters and just includes the FQDN
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:?#[\]@!\$&'\(\)\*\+,;=.]+)"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
        $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        
        # Create hash table with token information
        $TokenData = @{
            'AccessToken'     = $AccessToken
            'RefreshToken'    = $RefreshToken
        }

        If ($Identifier) {
            $SecretName = "$($CloudFQDN)-$($Identifier)-accesstoken"
        }
        Else {
            $SecretName = "$($CloudFQDN)-accesstoken"
        }

        $Params = @{
            'Name'        = $SecretName
            'Secret'    = $TokenData
        }

        If ($SecretVault) {$Params.Add('Vault',$SecretVault)}

        Set-Secret @Params
        Write-Host "Successfully created $SecretName"
    }
}



Function Get-NectarCloudInfo {
    <#
        .SYNOPSIS
        Shows information about the active Nectar DXP connection
         
        .DESCRIPTION
        Shows information about the active Nectar DXP connection
 
        .EXAMPLE
        Get-NectarCloud
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnci")]
    [cmdletbinding()]
    param ()
    
    $CloudInfo = "" | Select-Object -Property CloudFQDN, Credential
    $CloudInfo.CloudFQDN = $Global:NectarCloud
    $CloudInfo.Credential = ($Global:NectarCred).UserName
    $CloudInfo | Add-Member -TypeName 'Nectar.CloudInfo'
    
    Try {
        $TenantCount = Get-NectarTenantNames
        If ($TenantCount.Count -gt 1) {
            If ($Global:NectarTenantName) {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $Global:NectarTenantName
            }
            Else {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue '<Not Set>'
            }
        }
    }
    Catch {
    }
    
    Return $CloudInfo
}



Function Get-NectarTenantNames {
    <#
        .SYNOPSIS
        Shows all the available Nectar tenants on the cloud host.
         
        .DESCRIPTION
        Shows all the available Nectar tenants on the cloud host. Only available for multi-tenant deployments and not visible to read-only admins.
 
        .EXAMPLE
        Get-NectarTenantNames
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntn")]
    [cmdletbinding()]
    param ()
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/tenant"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
             
            $TenantList = Foreach ($Item in $JSON) {
                $PSObject = New-Object PSObject -Property @{
                    TenantName = $Item
                }
                $PSObject
            }
            Return $TenantList
        }
        Catch {
            Write-Error 'No tenants found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarTenantDetails {
    <#
        .SYNOPSIS
        Shows detailed information about a specific tenant
         
        .DESCRIPTION
        Shows detailed information about a specific tenant. Not visible to read-only admins
 
        .PARAMETER TenantName
        Return information for the specified tenant
 
        .EXAMPLE
        Get-NectarTenantDetails
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntn")]
    [cmdletbinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/clients/$($TenantName)?tenant=$TenantName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
             
            Return $JSON
        }
        Catch {
            Write-Error 'Invalid tenantname or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarRIGAgent {
    <#
        .SYNOPSIS
        Return information about RIG agents connected to the environment
         
        .DESCRIPTION
        Return information about RIG agents connected to the environment
 
        .PARAMETER SearchQuery
        A string to search for. Will search for match against all fields
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort ordered output in ascending or descending order
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 10000
         
        .PARAMETER ResultSize
        The total number of results to return. Maximum result size is 9,999,999 results
     
        .EXAMPLE
        Get-NectarRIGAgent
        Returns information about all RIG agents in the environment
 
        .EXAMPLE
        Get-NectarRIGAgent -SearchQuery contoso -OrderByField
        Returns information about gateways from all Cisco clusters
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('name', 'clientName', 'lastRequestDate', 'cpu', 'memory', 'disk', IgnoreCase=$True)]
        [string]$OrderByField = 'name',
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection = 'asc',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$PageSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$ResultSize,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) { $PageSize = $ResultSize }

        $Params = @{
            'pageNumber'         = 1
            'pageSize'            = $PageSize
            'orderByField'        = $OrderByField
            'orderDirection'    = $OrderDirection
        }

        If ($SearchQuery) { $Params.Add('searchQuery', $SearchQuery) }

        $URI = "https://$Global:NectarCloud/aapi/tenant/agents"
        Write-Verbose $URI
        
        $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
        
        $TotalPages = $JSON.totalPages

        $JSON.elements

        If ($TotalPages -gt 1 -and !($ResultSize)) {
            $PageNum = 2
            Write-Verbose "Page size: $PageSize"
            While ($PageNum -le $TotalPages) {
                Write-Verbose "Working on page $PageNum of $TotalPages"
                $Params.PageNumber = $PageNum
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
                $JSON.elements
                $PageNum++
            }
        }
    }
}
    



Function Get-NectarTenantMetrics {
    <#
        .SYNOPSIS
        Shows all the tenant sizing metrics on the platform
         
        .DESCRIPTION
        Shows all the tenant sizing metrics on the platform. Only available for global admins.
 
        .PARAMETER TenantName
        Return metric values for a single tenant
 
        .EXAMPLE
        Get-NectarTenantMetrics
        Returns all metrics for all tenants
 
        .EXAMPLE
        Get-NectarTenantMetrics -TenantName Contoso
        Returns all metrics for the Contoso tenant
         
        .EXAMPLE
        Get-NectarTenantMetrics | Where {$_.name -like '*USERS' -And $_.value -gt 0} | FT
        Returns all user counts for all tenants where the value is greater than 0
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntm")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # The JSON format in this API doesn't lend itself to manipulation in PS, so we have to fix it up
            $URI = "https://$Global:NectarCloud/aapi/tenant/datasource/metrics?lastOnly=true"
            Write-Verbose $URI

            $MetricListRaw = (Invoke-WebRequest -Method GET -uri $URI -Headers $Global:NectarAuthHeader).content
            $MetricListRawUpdated = $MetricListRaw -Replace '(\"[a-z_0-9]+\")\ \:\ \[\ \{', '{"tenantName" : $1, "data" : [ {'
            $MetricListRawUpdated = $MetricListRawUpdated -Replace '\{\s+\{', '[{'
            $MetricListRawUpdated = $MetricListRawUpdated -Replace '\]\,', ']},'
            $MetricListRawUpdated = $MetricListRawUpdated -Replace '\}\s+\]\s+\}', '}]}]'
            $MetricList = ConvertFrom-JSON $MetricListRawUpdated
            
            If ($TenantName) {
                Return ($MetricList | Where-Object {$_.TenantName -eq $TenantName}).data
            }
            Else {
                Return $MetricList    
            }
        }
        Catch {
            Write-Error 'No tenants found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarTenantMetricTimeline {
    <#
        .SYNOPSIS
        Shows a monthly timeline of specific metric values for a given tenant
         
        .DESCRIPTION
        Shows a monthly timeline of specific metric values for a given tenant. Only available for global admins.
 
        .PARAMETER TenantName
        The tenant to return metrics on
 
        .PARAMETER Metric
        The specified metric to return data on
 
        .EXAMPLE
        Get-NectarTenantMetricTimeline -Metric CISCO_USERS -TenantName Contoso
        Returns the daily CISCO_USERS count for the past month for the Contoso tenant
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntmt")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("name")]
        [string]$Metric
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/tenant/datasource/metrics"

            $Params = @{
                lastOnly     = 'false'
                name         = $Metric
                periodCount    = 1
                tenant         = $TenantName
            }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params

            Return $JSON.$TenantName
        }
        Catch {
            Write-Error "Could not find tenant with name $TenantName or a matching metric called $Metric"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantLicenseCount {
    <#
        .SYNOPSIS
        Shows the current and historical license status for a given tenant
         
        .DESCRIPTION
        Shows the current and historical license status for a given tenant
 
        .EXAMPLE
        Get-NectarTenantLicenseCount
 
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/client/license/user-counts?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON
        }
        Catch {
            Write-Error 'No tenant license information found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantDatasources {
    <#
        .SYNOPSIS
        Shows all the datasources available on a given tenant.
         
        .DESCRIPTION
        Shows all the datasources available on a given tenant.
 
        .EXAMPLE
        Get-NectarTenantDatasources
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntdc")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }
            
            $URI = "https://$Global:NectarCloud/aapi/tenant/datasources?tenant=$TenantName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON.data
        }
        Catch {
            Write-Error 'No tenant datasources found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantPlatforms {
    <#
        .SYNOPSIS
        Shows all the platforms available on a given tenant.
         
        .DESCRIPTION
        Shows all the platforms available on a given tenant.
 
        .EXAMPLE
        Get-NectarTenantPlatforms
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntp")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/tenant/platforms?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            ForEach ($Item in $JSON) {
                $PlatformList = [pscustomobject][ordered]@{
                    TenantName = $TenantName
                    Platform = $Item.platform
                    Supported = $Item.supported
                }
                $PlatformList
            }
        }
        Catch {
            Write-Error 'No tenant platforms found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantSSOConfig {
    <#
        .SYNOPSIS
        Shows the SSO config for a given tenant.
         
        .DESCRIPTION
        Shows the SSO config for a given tenant.
 
        .EXAMPLE
        Get-NectarTenantSSOConfig
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntsc")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/client/sso-config?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON

            ForEach ($Item in $JSON) {
                $PlatformList = [pscustomobject][ordered]@{
                    TenantName = $TenantName
                    Platform = $Item.platform
                    Supported = $Item.supported
                }
                $PlatformList
            }
        }
        Catch {
            Write-Error 'No tenant platforms found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarUserAccountSettings {
    <#
        .SYNOPSIS
        Returns information about the current logged in user's UI settings.
 
        .DESCRIPTION
        Returns information about the current logged in user's UI settings.
         
        .EXAMPLE
        Get-NectarUserAccountSettings
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnas")]
    Param ()
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        $URI = "https://$Global:NectarCloud/aapi/user"
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -Uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON
        }
        Catch {
            Write-Error "Unable to obtain user's UI settings."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarUserAccountMapping {
    <#
        .SYNOPSIS
        Return user account mapping information
         
        .DESCRIPTION
        Return user account mapping information
 
        .PARAMETER MappingSource
        The source for the user mapping. Choose from 'Endpoint', 'WebRTC' or 'Jabra'
 
        .PARAMETER SearchQuery
        A string to search for. Will search for match against all fields
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort ordered output in ascending or descending order
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
     
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 10000
         
        .PARAMETER ResultSize
        The total number of results to return. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarUserAccountMapping -MappingSource WebRTC
        Return all WebRTC account mappings
 
        .EXAMPLE
        Get-NectarUserAccountMapping -MappingSource Endpoint -SearchQuery TFerguson
        Find a endpoint account mapping for TFerguson
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('Endpoint','WebRTC','Jabra', IgnoreCase=$True)]
        [string]$MappingSource = 'Endpoint',
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,        
        [Parameter(Mandatory=$False)]
        [string]$OrderByField,
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$PageSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$ResultSize,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        # Take care of any cases that don't match the above rule
        Switch ($MappingSource) {
            'Endpoint' {
                $URLPath = 'testing/entities'
                $OrderByField = 'name'
                break
            }
            'WebRTC' {
                $URLPath = 'webrtc/user/entities'
                $OrderByField = 'loginId'
                break
            }
            'Jabra' {
                $URLPath = 'jabra/user/entities'
                $OrderByField = 'domainName'
                break
            }
        }

        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) { $PageSize = $ResultSize }

        $Params = @{
            'pageNumber'         = 1
            'pageSize'            = $PageSize
            'orderByField'        = $OrderByField
            'orderDirection'    = $OrderDirection
            'tenant'            = $TenantName            
        }

        If ($SearchQuery) { $Params.Add('q', $SearchQuery) }

        $URI = "https://$Global:NectarCloud/aapi/$URLPath"
        Write-Verbose $URI        
        
        $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
        
        $TotalPages = $JSON.totalPages

        If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }

        $JSON.elements

        If ($TotalPages -gt 1 -and !($ResultSize)) {
            $PageNum = 2
            Write-Verbose "Page size: $PageSize"
            While ($PageNum -le $TotalPages) {
                Write-Verbose "Working on page $PageNum of $TotalPages"
                $Params.PageNumber = $PageNum
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }
                $JSON.elements
                $PageNum++
            }
        }
    }
}




Function Get-NectarPinnedUsers {
    <#
        .SYNOPSIS
        Returns information about the signed in user's Pinned Users list
 
        .DESCRIPTION
        Users can be "pinned" to the Users screen. This command will return information about all pinned users
         
        .EXAMPLE
        Get-NectarPinnedUsers
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnpu")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
        
        $Params = @{}
        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) {
            $Params.Add('pageSize',$ResultSize)
        }
        Else {
            $Params.Add('pageSize',$PageSize)
        }        
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        If ($TenantName) { $Params.Add('Tenant',$TenantName) }
        
        $URI = "https://$Global:NectarCloud/dapi/user/pinned"
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -Uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            $TotalPages = $JSON.totalPages
            
            # Add the tenant name to the output which helps pipelining
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} 
            
            # Remove the Photo element, which takes a lot of room on the page and isn't necessary
            $JSON.elements | Select-Object -Property * -ExcludeProperty Photo
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -Uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params

                    # Remove the Photo element, which takes a lot of room on the page and isn't necessary
                    $JSON.elements | Select-Object -Property * -ExcludeProperty Photo
                    $PageNum++
                }
            }            
        }
        Catch {
            Write-Error "Error getting pinned user list"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Add-NectarPinnedUser {
    <#
        .SYNOPSIS
        Adds a user to the signed in user's Pinned Users list
 
        .DESCRIPTION
        Users can be "pinned" to the Users screen. This command will add a user to the signed in user's Pinned users list
         
        .EXAMPLE
        Add-NectarPinnedUser tferguson@contoso.com
        Adds the user TFerguson@contoso.com to the signed in Pinned Users list
         
        .EXAMPLE
        Import-Csv .\Users.csv | Add-NectarPinnedUser
        Adds users to pinned user list using a CSV file of email addresses as input. CSV file must have a header called EmailAddress
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("anpu")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
        
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # Get the current preference settings and get the pinned user list
        $Preferences = (Get-NectarUserAccountSettings).Preferences
        [System.Collections.ArrayList]$PinnedUserList = $Preferences.pinnedUserIds
        
        $URI = "https://$Global:NectarCloud/aapi/user/preferences?tenant=$TenantName"
    }        
    Process {
        Try {
            # Get the ID of the desired user and add to the Pinned User List
            [string]$UserID = (Get-NectarUser $EmailAddress -ErrorAction Stop).Id

            If ($PinnedUserList.Contains($UserID)) {
                Write-Error "$EmailAddress already in Pinned User list"
            }
            Else {
                $Null = $PinnedUserList.Add($UserID)
            }
            
            # Re-insert pinned user list to user preferences
            $Preferences.pinnedUserIds = $PinnedUserList
            
            # Convert Preferences back to JSON
            $JSONPrefs = $Preferences | ConvertTo-JSON -Depth 10
            
            # Save the updated preferences
            $NULL = Invoke-RestMethod -Method POST -Uri $URI -Headers $Global:NectarAuthHeader -Body $JSONPrefs -ContentType "application/json"
                        
        }
        Catch {
            Write-Error "Error adding $EmailAddress to pinned user list"
        }
    }
    End {
        Try {
            # Re-insert pinned user list to user preferences
            $Preferences.pinnedUserIds = $PinnedUserList
            
            # Convert Preferences back to JSON
            $JSONPrefs = $Preferences | ConvertTo-JSON -Depth 10
            
            # Save the updated preferences
            $NULL = Invoke-RestMethod -Method POST -Uri $URI -Headers $Global:NectarAuthHeader -Body $JSONPrefs -ContentType "application/json"
        }
        Catch {
            Write-Error "Error adding $EmailAddress to pinned user list"
        }
    }
}



Function Remove-NectarPinnedUser {
    <#
        .SYNOPSIS
        Remove a user from the signed in user's Pinned Users list
 
        .DESCRIPTION
        Users can be "pinned" to the Users screen. This command will remove a user from the signed in user's Pinned users list
         
        .EXAMPLE
        Remove-NectarPinnedUsers tferguson@contoso.com
        Removes the user TFerguson@contoso.com from the signed in Pinned Users list
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("rnpu")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Email')]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ID,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
        
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # Get the current preference settings and get the pinned user list
        $Preferences = (Get-NectarUserAccountSettings).Preferences
        [System.Collections.ArrayList]$PinnedUserList = $Preferences.pinnedUserIds
        
        $URI = "https://$Global:NectarCloud/aapi/user/preferences?tenant=$TenantName"
    }        
    Process {
        Try {
            # Get the ID of the desired user and remove from the Pinned User List
            If (!$ID) {    [string]$ID = (Get-NectarUser $EmailAddress).Id }
        
            $PinnedUserList.Remove($ID)
        }
        Catch {
            Write-Error "Error removing $EmailAddress from pinned user list"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
    End {
        Try {
            # Re-insert pinned user list to user preferences
            $Preferences.pinnedUserIds = $PinnedUserList
            
            # Convert Preferences back to JSON
            $JSONPrefs = $Preferences | ConvertTo-JSON -Depth 10
            
            # Save the updated preferences
            $NULL = Invoke-RestMethod -Method POST -Uri $URI -Headers $Global:NectarAuthHeader -Body $JSONPrefs -ContentType "application/json"
        }
        Catch {
            Write-Error 'Error updating user preferences'
            Get-JSONErrorStream -JSONResponse $_
        }    
    }
}




#################################################################################################################################################
# #
# Tenant Email Domain Functions #
# #
#################################################################################################################################################

Function Get-NectarEmailDomain {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP allowed email domains that can be used for login IDs.
         
        .DESCRIPTION
        Returns a list of Nectar DXP allowed email domains that can be used for login IDs.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarEmailDomain
        Returns all the allowed email domains for the logged in tenant.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gne")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

            $URI = "https://$Global:NectarCloud/aapi/client/domains?searchQuery=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            If ($JSON.domainNames) { Return $JSON.domainNames }
            If ($JSON.elements) { Return $JSON.elements }
        }
        Catch {
            Write-Error 'No email domains found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarEmailDomain {
    <#
        .SYNOPSIS
        Add a new allowed email domain that can be used for login IDs.
         
        .DESCRIPTION
        Add a new allowed email domain that can be used for login IDs.
         
        .PARAMETER EmailDomain
        The email domain to add to the tenant.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarEmailDomain -EmailDomain contoso.com
        Adds the contoso.com email domain to the logged in Nectar DXP tenant.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nne")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$EmailDomain,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/client/domains?tenant=$TenantName"
        
        $JSONBody = $EmailDomain
        
        Try {
            $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose "Successfully added $EmailDomain as an allowed email domain."
        }
        Catch {
            Write-Error "Unable to add $EmailDomain to list of allowed email domains."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarEmailDomain {
    <#
        .SYNOPSIS
        Remove an allowed email domain that can be used for login IDs.
         
        .DESCRIPTION
        Remove an allowed email domain that can be used for login IDs.
         
        .PARAMETER EmailDomain
        The email domain to remove from the tenant.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarEmailDomain -EmailDomain contoso.com
        Removes the contoso.com email domain from the list of allowed domains on the logged in Nectar DXP tenant.
 
        .NOTES
        Version 1.1
    #>

    [Alias("rne")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("domainName")]
        [string]$EmailDomain,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        If ($EmailDomain -and !$Identity) {
            $Identity = (Get-NectarEmailDomain -TenantName $TenantName -SearchQuery $EmailDomain -ResultSize 1 -ErrorVariable GetDomainError).ID
        }

        If (!$GetDomainError) {
            $URI = "https://$Global:NectarCloud/aapi/client/domains/$Identity/?tenant=$TenantName"

            Try {
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully deleted $EmailDomain from list of allowed email account domains."
            }
            Catch {
                Write-Error "Unable to delete email domain $EmailDomain. Ensure you typed the name of the email domain correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# Tenant Admin Functions #
# #
#################################################################################################################################################

Function Get-NectarUserAccount {
    <#
        .SYNOPSIS
        Get information about one or more Nectar DXP user accounts.
         
        .DESCRIPTION
        Get information about one or more Nectar DXP user accounts.
 
        .PARAMETER SearchQuery
        A full or partial match of the user's first or last name or email address
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-NectarUserAccount -SearchQuery tferguson@contoso.com
        Returns information about the user tferguson@contoso.com
         
        .NOTES
        Version 1.1
    #>

    
    [Alias('gna', 'Get-NectarAdmin')]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/users?searchQuery=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
        Try {
            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            Return $JSON.elements
        }
        Catch {
            Write-Error "Unable to get user details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarUserAccount {
    <#
        .SYNOPSIS
        Update one or more Nectar DXP user accounts.
         
        .DESCRIPTION
        Update one or more Nectar DXP user accounts.
         
        .PARAMETER FirstName
        The first name of the user
         
        .PARAMETER LastName
        The last name of the user
 
        .PARAMETER EmailAddress
        The email address of the user
         
        .PARAMETER AdminStatus
        True if Admin, False if not. Used when importing many user accounts via CSV
         
        .PARAMETER IsAdmin
        Include if user is to be an admin. If not present, then user will be read-onl
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER Identity
        The numerical identity of the user
         
        .NOTES
        Version 1.1
    #>

    
    [Alias('sna', 'Set-NectarAdmin')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("name")]
        [string]$FirstName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LastName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("email")]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("admin")]
        [string]$AdminStatus,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$IsAdmin,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Role,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # If ($IsAdmin -and !$AdminStatus) {
            # $AdminStatus = "true"
        # }
        # ElseIf (!$IsAdmin -and !$AdminStatus) {
            # $AdminStatus = "false"
        # }
        
        If ($EmailAddress -And !$Identity) {
            $UserInfo = Get-NectarUserAccount -SearchQuery $EmailAddress -Tenant $TenantName -ResultSize 1
            $Identity = $UserInfo.id
        }
        
        If (-not $FirstName) {$FirstName = $UserInfo.firstName}
        If (-not $LastName) {$LastName = $UserInfo.lastName}
        If (-not $EmailAddress) {$EmailAddress = $UserInfo.email}
        If (-not $IsAdmin -and !$AdminStatus) {$AdminStatus = $UserInfo.admin}
            
        $URI = "https://$Global:NectarCloud/aapi/user/$Identity/?tenant=$TenantName"

        $Body = @{
            id = $Identity
            email = $EmailAddress
            firstName = $FirstName
            lastName = $LastName
# isAdmin = $AdminStatus
            userRoleName = $Role
# userStatus = "ACTIVE"
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to apply changes for user $EmailAddress."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarUserAccount {
    <#
        .SYNOPSIS
        Create a new Nectar DXP user account.
         
        .DESCRIPTION
        Create a new Nectar DXP user account.
 
        .PARAMETER FirstName
        The user's first name
 
        .PARAMETER LastName
        The user's last name
 
        .PARAMETER EmailAddress
        The user's email address
         
        .PARAMETER Password
        The password to assign to the new account
         
        .PARAMETER AdminStatus
        True if Admin, False if not. Used when importing many admin accounts via CSV
         
        .PARAMETER IsAdmin
        Include if user is to be an admin. If not present, then user will be read-only
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarUser -FirstName Turd -LastName Ferguson -Email tferguson@contoso.com -Password VeryStrongPassword -IsAdmin
        Creates a new admin user called Turd Ferguson
         
        .EXAMPLE
        Import-Csv .\Users.csv | New-NectarUser
        Creates user accounts using a CSV file as input. CSV file must have the following headers: FirstName,LastName,Password,Email,AdminStatus (Use True/False for AdminStatus)
 
        .NOTES
        Version 1.2
    #>

    
    [Alias('nna', 'New-NectarAdmin')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$FirstName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LastName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("email")]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [SecureString]$Password,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AdminStatus,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$IsAdmin,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Role,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName        
    )    

    Begin {
        Connect-NectarCloud
    }        
    Process {
        If ($IsAdmin -and !$AdminStatus) {
            $AdminStatus = "true"
        }
        ElseIf (!$IsAdmin -and !$AdminStatus) {
            $AdminStatus = "false"
        }

        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
                
        $URI = "https://$Global:NectarCloud/aapi/user?tenant=$TenantName"
        $Body = @{
            email = $EmailAddress
            firstName = $FirstName
            lastName = $LastName
            isAdmin = $AdminStatus
            userRoleName = $Role
            userStatus = "ACTIVE"
        }
        
        If ($Password) { $Body.Add('password',$Password) }
            
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to create user account $EmailAddress."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarUserAccount {
    <#
        .SYNOPSIS
        Removes one or more Nectar DXP user account.
 
        .DESCRIPTION
        Removes one or more Nectar DXP user account.
         
        .PARAMETER EmailAddress
        The email address of the user account to remove.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
             
        .PARAMETER Identity
        The numerical ID of the user account to remove. Can be obtained via Get-NectarUserAccount and pipelined to Remove-NectarUser
         
        .EXAMPLE
        Remove-NectarUserAccount tferguson@nectarcorp.com
        Removes the user account tferguson@nectarcorp.com
         
        .NOTES
        Version 1.1
    #>

    
    [Alias('rna', 'Remove-NectarAdmin')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("email")]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($EmailAddress -and !$Identity) {
            $Identity = (Get-NectarUserAccount -SearchQuery $EmailAddress -Tenant $TenantName -ResultSize 1 -ErrorVariable GetUserError).ID
        }
            
        If (!$GetUserError) {
            $URI = "https://$Global:NectarCloud/aapi/user/$Identity/?tenant=$TenantName"
            
            Try {
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully deleted $EmailAddress from user account list."
            }
            Catch {
                Write-Error "Unable to delete user $EmailAddress. Ensure you typed the name of the user correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}





#################################################################################################################################################
# #
# Tenant Location Functions #
# #
#################################################################################################################################################

Function Get-NectarLocation {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP locations
         
        .DESCRIPTION
        Returns a list of Nectar DXP locations
 
        .PARAMETER SearchQuery
        The name of the location to get information on based on either network, networkName, City, StreetAddress, State, SiteName or SiteCode. Can be a partial match, and may return more than one entry.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarLocation
        Returns the first 10 locations
         
        .EXAMPLE
        Get-NectarLocation -ResultSize 100
        Returns the first 100 locations
 
        .EXAMPLE
        Get-NectarLocation -LocationName Location2
        Returns up to 10 locations that contains "location2" anywhere in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214, MyLocation299 etc
 
        .EXAMPLE
        Get-NectarLocation -LocationName ^Location2
        Returns up to 10 locations that starts with "location2" in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214 etc, but NOT MyLocation299
 
        .EXAMPLE
        Get-NectarLocation -LocationName ^Location2$
        Returns a location explicitly named "Location2". The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 5000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/aapi/config/locations?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&searchQuery=$SearchQuery"
        
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            If (!$JSON.elements) {
                Write-Error "Location $SearchQuery not found."
            }
            Else {
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -TypeName 'Nectar.LocationList'
                Return $JSON.elements
            }
        }
        Catch {
            Write-Error "Unable to get location details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarLocation {
    <#
        .SYNOPSIS
        Update a Nectar DXP location in the location database
         
        .DESCRIPTION
        Update a Nectar DXP location in the location database. This command can use the Google Geocode API to automatically populate the latitude/longitude for each location. You can register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. This command will prompt for the GeoCode API key and will save it in the appropriate location. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key. If this is not an option, then use the -SkipGeoLocate switch
 
        .PARAMETER SearchQuery
        A string to search for. Will search in Network, NetworkName, City, Street Address, Region etc.
         
        .PARAMETER Network
        The IP subnet of the network
         
        .PARAMETER NetworkRange
        The subnet mask of the network
         
        .PARAMETER ExtNetwork
        The IP subnet of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER ExtNetworkRange
        The subnet mask of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER NetworkName
        The name to give to the network
         
        .PARAMETER SiteName
        The name to give to the siteCode
         
        .PARAMETER SiteCode
        A site code to assign to the site
         
        .PARAMETER Region
        The name of the region. Typically is set to country name or whatever is appropriate for the company
         
        .PARAMETER StreetAddress
        The street address of the location
         
        .PARAMETER City
        The city of the location
         
        .PARAMETER State
        The state/province of the location
         
        .PARAMETER PostCode
        The postal/zip code of the location
         
        .PARAMETER Country
        The 2-letter ISO country code of the location
         
        .PARAMETER Description
        A description to apply to the location
         
        .PARAMETER IsWireless
        True or false if the network is strictly wireless
         
        .PARAMETER IsExternal
        True or false if the network is outside the corporate network
         
        .PARAMETER IsVPN
        True or false if the network is a VPN
 
        .PARAMETER NetworkRangeType
        The type of network range. Choose from Sequential or Subnet
         
        .PARAMETER Latitude
        The geographical latitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER Longitude
        The geographical longitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .PARAMETER Identity
        The numerical ID of the location to update. Can be obtained via Get-NectarLocation and pipelined to Set-NectarLocation
         
        .EXAMPLE
        Set-NectarLocation HeadOffice -Region WestUS
        Changes the region for HeadOffice to WestUS
 
        .EXAMPLE
        Get-NectarLocation | Set-NectarLocation
        Will go through each location and update the latitude/longitude. Useful if a Google Geocode API key was obtained after initial location loading
         
        .NOTES
        Version 1.11
    #>

    
    [Alias("snl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("subnet")]
        [string]$Network,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$NetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ExtNetwork,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$ExtNetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(1,99)]
        [Alias("Network Name")]
        [string]$NetworkName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SiteName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("Site Code")]
        [string]$SiteCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Region,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("address")]
        [string]$StreetAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$City,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("province")]
        [string]$State,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("zipcode")]
        [string]$PostCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,2)]
        [string]$Country,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("isWirelessNetwork","Wireless(Yes/No)","Wireless","Wifi")]
        [string]$IsWireless,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("External(Yes/No)","External")]
        [string]$IsExternal,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("VPN","VPN(Yes/No)")]
        [string]$IsVPN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Sequential','Subnet', IgnoreCase=$True)]
        [string]$NetworkRangeType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-90,90)]
        [Alias("CoordinatesLatitude")]
        [double]$Latitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-180,180)]
        [Alias("CoordinatesLongitude")]
        [double]$Longitude,
        [Parameter(Mandatory=$False)]
        [switch]$ForceGeoLocate,
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        Try {
            If ($SearchQuery) {
                $LocationInfo = Get-NectarLocation -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1
                $Identity = $LocationInfo.id #.ToString()
            }
            
            If (-not $Network) {$Network = $LocationInfo.network}
            If (-not $NetworkRange) {$NetworkRange = $LocationInfo.networkRange}
            If (-not $ExtNetwork) {$ExtNetwork = $LocationInfo.externalNetwork}
            If (-not $ExtNetworkRange) {$ExtNetworkRange = $LocationInfo.externalNetworkRange}
            If (-not $NetworkRangeType) {$NetworkRangeType = $LocationInfo.networkRangeType}
            If (-not $NetworkName) {$NetworkName = $LocationInfo.networkName}
            If (-not $SiteName) {$SiteName = $LocationInfo.siteName}
            If (-not $SiteCode) {$SiteCode = $LocationInfo.siteCode}
            If (-not $Region) {$Region = $LocationInfo.region}
            If (-not $StreetAddress) {$StreetAddress = $LocationInfo.streetAddress}
            If (-not $City) {$City = $LocationInfo.city}
            If (-not $State) {$State = $LocationInfo.state}
            If (-not $PostCode) {$PostCode = $LocationInfo.zipCode}
            If (-not $Country) {$Country = $LocationInfo.country}
            If (-not $Description) {$Description = $LocationInfo.description}
            If (-not $IsWireless) {$IsWireless = $LocationInfo.isWirelessNetwork}
            If (-not $IsExternal) {$IsExternal = $LocationInfo.isExternal}
            If (-not $IsVPN) {$IsVPN = $LocationInfo.vpn}
            If ($NULL -eq $Latitude -or $Latitude -eq 0) {$Latitude = $LocationInfo.latitude}
            If ($NULL -eq $Longitude -or $Longitude -eq 0) {$Longitude = $LocationInfo.longitude}
            If (-not $IsVPN) {$IsVPN = $LocationInfo.vpn}

            If ((($NULL -eq $Latitude -Or $NULL -eq $Longitude) -Or ($Latitude -eq 0 -And $Longitude -eq 0)) -Or $ForceGeoLocate -And !$SkipGeoLocate) {
                Write-Verbose "Lat/Long missing. Getting Lat/Long."
                $LatLong = Get-LatLong "$StreetAddress, $City, $State, $PostCode, $Region"
                $Latitude = $LatLong.Latitude
                $Longitude = $LatLong.Longitude
            }

            $URI = "https://$Global:NectarCloud/aapi/config/location/$Identity/?tenant=$TenantName"

            $Body = @{
                city = $City
                description = $Description
                id = $Identity
                isExternal = ParseBool $IsExternal
                isWirelessNetwork = ParseBool $IsWireless
                latitude = $Latitude
                longitude = $Longitude
                network = $Network
                networkRange = $NetworkRange
                externalNetwork = $ExtNetwork
                externalNetworkRange = $ExtNetworkRange
                networkName = $NetworkName
                networkRangeIpEnd = $NULL
                networkRangeIpStart = $NULL
                networkRangeType = $NetworkRangeType
                region = $Region
                siteCode = $SiteCode
                siteName = $SiteName
                state = $State
                streetAddress = $StreetAddress
                country = $Country
                vpn = ParseBool $IsVPN
                zipCode = $PostCode
            }
            
            Write-Verbose $Body
            
            $JSONBody = $Body | ConvertTo-Json

            Try {
                Write-Verbose $JSONBody
                $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8' 

            }
            Catch {
                If ($NetworkName) {
                    $IDText = $NetworkName
                }
                Else {
                    $IDText = "with ID $Identity"
                }
                
                Write-Error "Unable to apply changes for location $IDText."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
        Catch {
            Write-Error $_
        }
    }
}



Function New-NectarLocation {
    <#
        .SYNOPSIS
        Creates a Nectar DXP location in the location database
         
        .DESCRIPTION
        Creates a Nectar DXP location in the location database. This command can use the Google Geocode API to automatically populate the latitude/longitude for each location. You can register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. This command will prompt for the GeoCode API key and will save it in the appropriate location. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key. If this is not an option, then use the -SkipGeoLocate switch
 
        .PARAMETER Network
        The IP subnet of the network
         
        .PARAMETER NetworkRange
        The subnet mask of the network
         
        .PARAMETER ExtNetwork
        The IP subnet of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER ExtNetworkRange
        The subnet mask of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER NetworkName
        The name to give to the network
         
        .PARAMETER SiteName
        The name to give to the site
         
        .PARAMETER SiteCode
        A site code to assign to the site
         
        .PARAMETER Region
        The name of the region. Typically is set to country name or whatever is appropriate for the company
         
        .PARAMETER StreetAddress
        The street address of the location
         
        .PARAMETER City
        The city of the location
         
        .PARAMETER State
        The state/province of the location
         
        .PARAMETER PostCode
        The postal/zip code of the location
         
        .PARAMETER Country
        The 2-letter ISO country code of the location
         
        .PARAMETER Description
        A description to apply to the location
         
        .PARAMETER IsWireless
        True or false if the network is strictly wireless
         
        .PARAMETER IsExternal
        True or false if the network is outside the corporate network
         
        .PARAMETER IsVPN
        True or false if the network is a VPN
 
        .PARAMETER NetworkRangeType
        The type of network range. Choose from Sequential, Subnet or Virtual
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .PARAMETER Latitude
        The geographical latitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER Longitude
        The geographical longitude of the location. If not specified, will attempt automatic geolocation.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarLocation -Network 10.14.3.0 -NetworkRange 24 -NetworkName Corp5thFloor -SiteName 'Head Office'
        Creates a new location using the minimum required information
         
        .EXAMPLE
        New-NectarLocation -Network 10.15.1.0 -NetworkRange 24 -ExtNetwork 79.23.155.71 -ExtNetworkRange 28 -NetworkName Corp3rdFloor -SiteName 'Head Office' -SiteCode HO3 -IsWireless True -IsVPN False -Region EastUS -StreetAddress '366 North Broadway' -City Jericho -State 'New York' -Country US -PostCode 11753 -Description 'Head office 3rd floor' -Latitude 40.7818283 -Longitude -73.5351438
        Creates a new location using all available fields
                 
        .EXAMPLE
        Import-Csv LocationData.csv | New-NectarLocation
        Imports a CSV file called LocationData.csv and creates new locations
         
        .EXAMPLE
        Import-Csv LocationData.csv | New-NectarLocation -SkipGeolocate
        Imports a CSV file called LocationData.csv and creates new locations but will not attempt geolocation
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('subnet','searchquery')]
        [string]$Network,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$NetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('ExternalNetwork')]
        [string]$ExtNetwork,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('ExternalNetworkRange')]
        [ValidateRange(0,32)]
        [string]$ExtNetworkRange,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateLength(1,99)]
        [Alias('Network Name')]
        [string]$NetworkName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('Site Name')]
        [string]$SiteName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Site Code')]
        [string]$SiteCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Region,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Street Address', 'address')]
        [string]$StreetAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$City,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('province')]
        [string]$State,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Zip Code', 'zipcode')]
        [string]$PostCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,2)]
        [string]$Country,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('isWirelessNetwork','Wireless(Yes/No)','Wireless(True/False)','Wireless','Wifi')]
        [string]$IsWireless,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('External(Yes/No)','External(True/False)','External','Internal/External')]
        [string]$IsExternal,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('VPN','VPN(Yes/No)','VPN(True/False)')]
        [string]$IsVPN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Sequential','Subnet','Virtual', IgnoreCase=$True)]
        [string]$NetworkRangeType = 'Subnet',
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-90,90)]
        [Alias('Coordinates Latitude','CoordinatesLatitude')]
        [double]$Latitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-180,180)]
        [Alias('Coordinates Longitude','CoordinatesLongitude')]
        [double]$Longitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName        
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/config/location?tenant=$TenantName"

        If (-not $Latitude -Or -not $Longitude -And !$SkipGeoLocate) {
            $LatLong = Get-LatLong "$StreetAddress, $City, $State, $PostCode, $Country"
            [double]$Latitude = $LatLong.Latitude
            [double]$Longitude = $LatLong.Longitude
        }

        $Body = @{
            city = $City
            description = $Description
            isExternal = ParseBool $IsExternal
            isWirelessNetwork = ParseBool $IsWireless
            latitude = $Latitude
            longitude = $Longitude
            network = $Network
            networkName = $NetworkName
            networkRange = $NetworkRange
            externalNetwork = $ExtNetworkName
            externalNetworkRange = $ExtNetworkRange
            region = $Region
            siteCode = $SiteCode
            siteName = $SiteName
            state = $State
            streetAddress = $StreetAddress
            country = $Country
            vpn = ParseBool $IsVPN
            zipCode = $PostCode
            networkRangeType = $NetworkRangeType
        }

        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to create location $NetworkName with network $Network/$NetworkRange"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarLocation {
    <#
        .SYNOPSIS
        Removes a Nectar DXP location from the location database
         
        .DESCRIPTION
        Removes a Nectar DXP location from the location database
 
        .PARAMETER SearchQuery
        The name of the location to remove. Can be a partial match. To return an exact match and to avoid ambiguity, enclose location name with ^ at the beginning and $ at the end.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER Identity
        The numerical ID of the location to remove. Can be obtained via Get-NectarLocation and pipelined to Remove-NectarLocation
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("networkName")]
        [string]$SearchQuery, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
        If ($SearchQuery -And !$Identity) {
            $LocationInfo = Get-NectarLocation -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1 -ErrorVariable GetLocationError
            $Identity = $LocationInfo.id
            $NetworkName = $LocationInfo.networkName
        }
            
        If (!$GetLocationError) {
            $URI = "https://$Global:NectarCloud/aapi/config/location/$Identity/?tenant=$TenantName"
            
            Try {
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully deleted $LocationName."
            }
            Catch {
                Write-Error "Unable to delete location $NetworkName. Ensure you typed the name of the location correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



Function Import-NectarLocations {
    <#
        .SYNOPSIS
        Imports a CSV list of locations into Nectar DXP
         
        .DESCRIPTION
        Import a CSV list of locations into Nectar DXP. This will overwrite any existing locations with the same network ID. Useful for making wholesale changes without wiping and replacing everything.
        Assumes you are working from an export from the existing Nectar DXP location list.
 
        .PARAMETER Path
        The path to the CSV file to import into Nectar DXP. The CSV file must use the standard column heading template used by Nectar DXP exports.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key or the lat/long is already included in the CSV.
         
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Path, 
        [Parameter(Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate    
    )
    
    $LocTable = ((Get-Content -Path $Path -Raw) -replace '\(Yes/No\)','')
    $LocTable = $LocTable -replace '\"?Network\"?\,',"""SearchQuery""," 
    
    $LocationList = ConvertFrom-Csv $LocTable

    ForEach ($Location in $LocationList) {
        $LocationHashTable = @{}
        $Location.psobject.properties | ForEach-Object { $LocationHashTable[$_.Name] = $_.Value }
        
        If ($TenantName) { $LocationHashTable += @{TenantName = $TenantName } } # Add the tenant name to the hashtable
        If ($SkipGeoLocate) { $LocationHashTable += @{SkipGeoLocate = $TRUE} }
        
        Try {
            Write-Host "Updating location with subnet $($Location.SearchQuery)"
            Write-Verbose $LocationHashTable
            Set-NectarLocation @LocationHashTable -ErrorAction:Stop
        }
        Catch {
            Write-Host "Location does not exist. Creating location $($Location.SearchQuery)"
            New-NectarLocation @LocationHashTable
        }
    }
}



Function Import-MSTeamsLocations {
    <#
        .SYNOPSIS
        Imports a CSV list of locations downloaded from Microsoft CQD into Nectar DXP
         
        .DESCRIPTION
        Import a CSV list of locations downloaded from Microsoft CQD into Nectar DXP. This will overwrite any existing locations with the same network ID. Useful for making wholesale changes without wiping and replacing everything.
 
        .PARAMETER Path
        The path to the CSV file to import into Nectar DXP. The CSV file must be in the same format as downloaded from Microsoft CQD as per https://docs.microsoft.com/en-us/microsoftteams/cqd-upload-tenant-building-data
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Path, 
        [Parameter(Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate    
    )
    
    $Header = 'SearchQuery', 'NetworkName', 'NetworkRange', 'SiteName', 'OwnershipType', 'BuildingType', 'BuildingOfficeType', 'City', 'PostCode', 'Country', 'State', 'Region', 'IsExternal', 'ExpressRoute', 'IsVPN'
    $LocationList = Import-Csv $Path -Header $Header | Select-Object 'SearchQuery','NetworkRange','NetworkName','SiteName','City','PostCode','Country','State','Region','IsExternal','IsVPN'
    
    ForEach ($Location in $LocationList) {
        If ($Location.IsExternal -eq 0) { $Location.IsExternal = 1 } Else { $Location.IsExternal = 0 }
        If ($Location.IsVPN -eq 1) { $Location.IsVPN = 1 } Else { $Location.IsVPN = 0 }
    
        $LocationHashTable = @{}
        $Location.psobject.properties | ForEach-Object { $LocationHashTable[$_.Name] = $_.Value }
        
        If ($TenantName) { $LocationHashTable += @{TenantName = $TenantName } }# Add the tenant name to the hashtable
        If ($SkipGeoLocate) { $LocationHashTable += @{SkipGeoLocate = $TRUE} }
                
        Try {
            Write-Host "Updating location with subnet $($Location.SearchQuery)"
            Write-Verbose $LocationHashTable
            Set-NectarLocation @LocationHashTable -ErrorAction:Stop
        }
        Catch {
            Write-Host "Location does not exist. Creating location $($Location.SearchQuery)"
            New-NectarLocation @LocationHashTable
        }
    }
}




#################################################################################################################################################
# #
# Tenant Alert Functions #
# #
#################################################################################################################################################

Function Get-NectarAlerts {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP alert configurations
         
        .DESCRIPTION
        Returns a list of Nectar DXP alert configurations
 
        .PARAMETER AlertType
        The type of alert to return data on. Choose from 'NectarScore-Users', 'NectarScore-Locations', or 'PoorCalls'
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-NectarScoreAlerts
        Returns the first 10000 alert configurations
         
        .EXAMPLE
        Get-NectarScoreAlerts -ResultSize 100
        Returns the first 100 alert configurations
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gna")]    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("ClientNotificationType")]
        [ValidateSet('NECTAR_SCORE_ALERT','NECTAR_SCORE_ALERT_BY_LOCATION','POOR_CALLS_BY_USER', IgnoreCase=$True)]
        [string]$AlertType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize=10000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

            $Params = @{
                'tenant'                    = $TenantName
                'orderByField'                 = 'name'
                'orderDirection'             = 'asc'
                'pageSize'                    = $ResultSize
            }
            
            Switch ($AlertType) {
                'NECTAR_SCORE_ALERT' {
                    $URI = "https://$Global:NectarCloud/aapi/client/notification/uhs/configurations"
                    $Params.Add('type','NECTAR_SCORE_ALERT')
                }
                'NECTAR_SCORE_ALERT_BY_LOCATION' {
                    $URI = "https://$Global:NectarCloud/aapi/client/notification/poor-calls/configurations"
                    $Params.Add('clientNotificationType','NECTAR_SCORE_ALERT_BY_LOCATION')
                }
                'POOR_CALLS_BY_USER' {
                    $URI = "https://$Global:NectarCloud/aapi/client/notification/poor-calls/configurations"
                }
            }

            Write-Verbose $URI

            If (!$NectarError) {
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements
            }
        }
        Catch {
            Write-Error "Unable to retrieve alert information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}




Function Set-NectarAlert {
    <#
        .SYNOPSIS
        Modify a Nectar DXP alert configuration
         
        .DESCRIPTION
        Modify a Nectar DXP alert configuration
 
        .PARAMETER ID
        The numerical ID of the alert to configure
 
        .PARAMETER Enabled
        Enable or disable the specified alert
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-NectarScoreAlerts
        Returns the first 10000 alert configurations
         
        .EXAMPLE
        Get-NectarScoreAlerts -ResultSize 100
        Returns the first 100 alert configurations
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("sna")]    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("ClientNotificationType")]
        [ValidateSet('NECTAR_SCORE_ALERT','NECTAR_SCORE_ALERT_BY_LOCATION','POOR_CALLS_BY_USER', IgnoreCase=$True)]
        [string]$AlertType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [bool]$Enabled,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize=10000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            If ($Null -ne $Enabled) {
                Switch ($AlertType) {
                    'NECTAR_SCORE_ALERT' {
                        $URI = "https://$Global:NectarCloud/aapi/client/notification/uhs/configuration/$ID/enabled?enabled=$Enabled&tenant=$TenantName"
                    }
                    'NECTAR_SCORE_ALERT_BY_LOCATION' {
                        $URI = "https://$Global:NectarCloud/aapi/client/notification/poor-calls/configuration/$ID/enabled?enabled=$Enabled&tenant=$TenantName"
                    }
                    'POOR_CALLS_BY_USER' {
                        $URI = "https://$Global:NectarCloud/aapi/client/notification/poor-calls/configuration/$ID/enabled?enabled=$Enabled&tenant=$TenantName"
                    }
                }
            }

            Write-Verbose $URI

            If (!$NectarError) {
                $JSON = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader
            }
        }
        Catch {
            Write-Error "Unable to set alert configuration"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}





#################################################################################################################################################
# #
# Tenant Data Diagnostic Functions #
# #
#################################################################################################################################################

Function Get-NectarDataDiagnosticSummary {
    <#
        .SYNOPSIS
        Returns the current summary of a tenant's data diagnostics
         
        .DESCRIPTION
        Returns the current summary of a tenant's data diagnostics
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarDataDiagnosticSummary
        Returns the current summary of a tenant's data diagnostics
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $URI = "https://$Global:NectarCloud/aapi/diagnostics/latest-pk-sources?tenant=$TenantName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            Return $JSON
        }
        Catch {
            Write-Error "Could not retrieve data diagnostic summary"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





Function Get-NectarDataDiagnosticDetail {
    <#
        .SYNOPSIS
        Returns the detail of a specific source for a tenant's data diagnostics
         
        .DESCRIPTION
        Returns the detail of a specific source for a tenant's data diagnostics
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarDataDiagnosticDetail -Source 'RIGDB Derby'
        Returns the current detail of a tenant's RIG Derby data diagnostics
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Source,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $URI = "https://$Global:NectarCloud/aapi/diagnostics/latest-pk-source?source=$Source&tenant=$TenantName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            Return $JSON
        }
        Catch {
            Write-Error "Could not retrieve data diagnostic detail for $Source"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



#################################################################################################################################################
# #
# Tenant Datamart Functions #
# #
#################################################################################################################################################

Function Get-NectarDatamartStatus {
    <#
        .SYNOPSIS
        Returns the current status of a tenant's datamart process
         
        .DESCRIPTION
        Returns the current status of a tenant's datamart process
 
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarDatamartStatus
        Returns the last 30 days daily status for the datamart
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnds")]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_DAY','LAST_WEEK','LAST_MONTH', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_WEEK',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $URI = "https://$Global:NectarCloud/aapi/datamart?timePeriod=$TimePeriod&tenant=$TenantName"
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            Return $JSON
        }
        Catch {
            Write-Error "Could not retrieve datamart status"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarDatamartLag {
    <#
        .SYNOPSIS
        Returns the current lag of a tenant's datamart process
         
        .DESCRIPTION
        Returns the current lag of a tenant's datamart process
 
        .PARAMETER Platform
        The platform to show datamart lag
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarDatamartLag -Platform 'MS Teams'
        Returns the current lag for all MS Teams datamart processes
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnds")]
    Param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('MS Teams','Skype for Business SQL Server - CDR','Skype for Business SQL Server - QoE','RIGDB Derby', IgnoreCase=$False)]
        [string]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Params = @{
                'source' = $PLatform
                'tenant' = $TenantName.ToLower()
            }

            $URI = "https://$Global:NectarCloud/aapi/diagnostics/latest-pk-source"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
            $CurrentTime = (Get-Date).ToUniversalTime()
            Return $JSON | Select-Object table, @{n='lastActivity';e={$_.pk}}, @{n='lag';e={(New-TimeSpan -Start $_.pk -End $CurrentTime).ToString("dd\.hh\:mm\:ss")}}
        }
        Catch {
            Write-Error "Could not retrieve datamart lag"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Invoke-NectarDatamartUpdate {
    <#
        .SYNOPSIS
        Trigger a reload of the datamart to process new call records
         
        .DESCRIPTION
        Trigger a reload of the datamart to process new call records
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to invoke a datamart reload for. Must be used in conjunction with TimePeriodTo parameters. Use format 'YYYY-MM-DD'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to invoke a datamart reload for. Must be used in conjunction with TimePeriodFrom parameters. Use format 'YYYY-MM-DD'. All time/dates in UTC.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Invoke-NectarDatamartStatus -Platform TEAMS -TimePeriodFrom '2022-03-01' -TimePeriodTo '2022-03-05'
        Reloads the Teams datamart tables for the period between March 1 to 5
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("inds")]
    Param (
        [Parameter(Mandatory=$True)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$True)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$True)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','RIG','LYNC_VKM','MISCELLANEOUS','CORE','ALL', IgnoreCase=$False)]
        [string]$Platform,    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
            
            $Params = @{
                'startDate'    = $TimePeriodFrom.ToString('yyyy-MM-ddT00:00:00.000')
                'endDate'    = $TimePeriodTo.ToString('yyyy-MM-ddT00:00:00.000')
                'platform'     = $Platform
                'tenant'     = $TenantName
                'module'    = 'PLATFORM'
                'clearOld'     = 'false'
            }
            
            $URI = "https://$Global:NectarCloud/aapi/datamart"
            
            $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            Return
        # }
        # Catch {
        # Write-Error "Could not invoke datamart update"
        # Get-JSONErrorStream -JSONResponse $_
        # }
    }
}





#################################################################################################################################################
# #
# DID Number Location Management Functions #
# #
#################################################################################################################################################

Function Get-NectarNumberLocation {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP service locations used in the DID Management tool.
         
        .DESCRIPTION
        Returns a list of Nectar DXP service locations used in the DID Management tool.
 
        .PARAMETER LocationName
        The name of the service location to get information on. Can be a partial match. To return an exact match and to avoid ambiguity, enclose service location name with ^ at the beginning and $ at the end.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarNumberLocation
        Returns the first 10 service locations
         
        .EXAMPLE
        Get-NectarNumberLocation -ResultSize 100
        Returns the first 100 service locations
 
        .EXAMPLE
        Get-NectarNumberLocation -LocationName Location2
        Returns up to 10 service locations that contains "location2" anywhere in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214, MyLocation299 etc
 
        .EXAMPLE
        Get-NectarNumberLocation -LocationName ^Location2
        Returns up to 10 service locations that starts with "location2" in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214 etc, but NOT MyLocation299
 
        .EXAMPLE
        Get-NectarNumberLocation -LocationName ^Location2$
        Returns a service location explicitly named "Location2". The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnnl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

            $URI = "https://$Global:NectarCloud/dapi/numbers/locations?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&q=$LocationName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.Number.LocationList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "Service location not found. Ensure you typed the name of the service location correctly."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarNumberLocation {
    <#
        .SYNOPSIS
        Update a Nectar DXP service location used in the DID Management tool.
         
        .DESCRIPTION
        Update a Nectar DXP service location used in the DID Management tool.
 
        .PARAMETER LocationName
        The name of the service location to get information on. Can be a partial match. To return an exact match and to avoid ambiguity, enclose location name with ^ at the beginning and $ at the end.
         
        .PARAMETER NewLocationName
        Replace the existing service location name with this one.
         
        .PARAMETER ServiceID
        The service ID associated with the telephony provider for this service location
 
        .PARAMETER ServiceProvider
        The name of the service provider that provides telephony service to this service location
 
        .PARAMETER NetworkLocation
        The phyiscal location for this service location
 
        .PARAMETER Notes
        Can be used for any additional information
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarNumberLocation -LocationName Dallas -ServiceID 44FE98 -ServiceProvider Verizon -Notes "Head office"
        Returns the first 10 locations
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("snnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("name")]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$NewLocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        $ServiceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("provider")]
        $ServiceProvider,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("location")]
        $NetworkLocation,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        $Notes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($LocationName -And !$Identity) {
            $LocationInfo = Get-NectarNumberLocation -LocationName $LocationName -Tenant $TenantName -ResultSize 1
            $Identity = $LocationInfo.id
        }    
        
        If ($LocationInfo.Count -gt 1) {
            Write-Error "Multiple number locations found that match $LocationName. Please refine your location name search query"
            Break
        }

        If ($NewLocationName) {$LocationName = $NewLocationName}
        If (-not $ServiceID) {$ServiceID = $LocationInfo.ServiceId}
        If (-not $ServiceProvider) {$ServiceProvider = $LocationInfo.provider}
        If (-not $NetworkLocation) {$NetworkLocation = $LocationInfo.location}
        If (-not $Notes) {$Notes = $LocationInfo.notes}

        $URI = "https://$Global:NectarCloud/dapi/numbers/location/$Identity/?tenant=$TenantName"

        $Body = @{
            name = $LocationName
            serviceId = $ServiceID
            provider = $ServiceProvider
            location = $NetworkLocation
            notes = $Notes
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to apply changes for location $LocationName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarNumberLocation {
    <#
        .SYNOPSIS
        Create a new Nectar Service Location for DID Management used in the DID Management tool.
         
        .DESCRIPTION
        Create a new Nectar Service Location for DID Management used in the DID Management tool.
 
        .PARAMETER LocationName
        The name of the new service location. Must be unique.
         
        .PARAMETER ServiceID
        The service ID for telephony services at the newservice location. Can be used as desired. Not required.
         
        .PARAMETER ServiceProvider
        The service provider for telephony services at the newservice location. Can be used as desired. Not required.
         
        .PARAMETER ServiceProvider
        The network location to associate with the newservice location. Can be used as desired. Not required.
 
        .PARAMETER Notes
        Any relevent notes about the service location. Can be used as desired. Not required.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarNumberLocation -LocationName Dallas -ServiceID 348FE22 -ServiceProvider Verizon -NetworkLocation Dallas -Notes "This is headquarters"
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nnnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("name")]
        [string]$LocationName, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ServiceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("provider")]
        [string]$ServiceProvider,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("location")]
        [string]$NetworkLocation,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Notes,    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/dapi/numbers/location/?tenant=$TenantName"
        $Body = @{
            name = $LocationName
            serviceId = $ServiceID
            provider = $ServiceProvider
            location = $NetworkLocation
            notes = $Notes
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to create service location $LocationName. The service location may already exist."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarNumberLocation {
    <#
        .SYNOPSIS
        Removes one or more service locations in the DID Management tool.
 
        .DESCRIPTION
        Removes one or more service locations in the DID Management tool.
         
        .PARAMETER LocationName
        The name of the number service location to remove.
     
        .PARAMETER Identity
        The numerical ID of the number service location. Can be obtained via Get-NectarNumberLocation and pipelined to Remove-NectarNumberLocation
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarNumberLocation Tokyo
        Removes the Toyota location. The command will fail if the location has number ranges assigned.
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($LocationName -And !$Identity) {
            $Identity = (Get-NectarNumberLocation -LocationName $LocationName -Tenant $TenantName -ResultSize 1 -ErrorVariable GetLocationError).ID
        }
        
        If ($Identity.Count -gt 1) {
            Write-Error "Multiple number locations found that match $LocationName. Please refine your location name search query"
            Break
        }
            
        If (!$GetLocationError) {
            $URI = "https://$Global:NectarCloud/dapi/numbers/location/$Identity/?tenant=$TenantName"
            
            Try {
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully deleted $LocationName."
            }
            Catch {
                Write-Error "Unable to delete service location $LocationName. Ensure you typed the name of the service location correctly and that the service location has no assigned ranges."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# DID Number Range Management Functions #
# #
#################################################################################################################################################

Function Get-NectarNumberRange {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP number ranges in the DID Management tool
 
        .DESCRIPTION
        Returns a list of Nectar DXP ranges in the DID Management tool
         
        .PARAMETER RangeName
        The name of the number range to get information on. Can be a partial match. To return an exact match and to avoid ambiguity, enclose range name with ^ at the beginning and $ at the end.
     
        .PARAMETER LocationName
        The name of the location to get information on. Will be an exact match.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarNumberRange
        Returns the first 10 number ranges
         
        .EXAMPLE
        Get-NectarNumberRange -ResultSize 100
        Returns the first 100 number ranges
 
        .EXAMPLE
        Get-NectarNumberRange -LocationName Tokyo
        Returns the first 10 number ranges at the Tokyo location
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName Range2
        Returns up to 10 ranges that contains "range2" anywhere in the name. The search is not case-sensitive. This example would return Range2, Range20, Range214, MyRange299 etc
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName ^Range2
        Returns up to 10 ranges that starts with "range2" in the name. The search is not case-sensitive. This example would return Range2, Range20, Range214 etc, but NOT MyRange299.
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName ^Range2$
        Returns any range explicitly named "Range2". The search is not case-sensitive. This example would return Range2 only. If there are multiple ranges with the name Range2, all will be returned.
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName ^Range2$ -LocationName Tokyo
        Returns a range explicitly named "Range2" in the Tokyo location. The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RangeName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($LocationName) {    
            $LocationID = (Get-NectarNumberLocation -LocationName "$LocationName" -Tenant $TenantName -ErrorVariable LocError).ID 
        }
        
        If ($LocError) { Break}

        $URI = "https://$Global:NectarCloud/dapi/numbers/ranges?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&serviceLocationId=$LocationID&q=$RangeName"
        Write-Verbose $URI
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.Number.RangeList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "An error occurred."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarNumberRange {
    <#
        .SYNOPSIS
        Make changes to a Nectar range for DID Management
         
        .DESCRIPTION
        Make changes to a Nectar range for DID Management
 
        .PARAMETER RangeName
        The name of the range. Must be unique.
         
        .PARAMETER RangeType
        The type of range. Can be either STANDARD (for DID ranges) or EXTENSION (for extension-based ranges).
         
        .PARAMETER FirstNumber
        The first number in a STANDARD range. Must be numeric, but can start with +.
         
        .PARAMETER LastNumber
        The last number in a STANDARD range. Must be numeric, but can start with +. Must be larger than FirstNumber, and must have the same number of digits.
         
        .PARAMETER BaseNumber
        The base DID for an EXTENSION range. Must be numeric, but can start with +.
         
        .PARAMETER ExtStart
        The first extension number in an EXTENSION range. Must be numeric.
         
        .PARAMETER ExtEnd
        The last extension number in an EXTENSION range. Must be numeric. Must be larger than ExtStart, and must have the same number of digits.
 
        .PARAMETER RangeSize
        The number of phone numbers/extensions in a range. Can be used instead of LastNumber/ExtEnd.
         
        .PARAMETER HoldDays
        The number of days to hold a newly-freed number before returning it to the pool of available numbers.
         
        .PARAMETER LocationName
        The service location to assign the range to.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarNumberRange -RangeName DIDRange1 -RangeType STANDARD -FirstNumber +15552223333 -LastNumber +15552224444 -LocationName Dallas
        Edits a DID range for numbers that fall in the range of +15552223333 to +15552224444
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("snnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("name")]
        [string]$RangeName, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("STANDARD","EXTENSION", IgnoreCase=$True)]
        [Alias("type")]
        [string]$RangeType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$FirstNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$LastNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$BaseNumber,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^\d+$")]
        [string]$ExtStart,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^\d+$")]
        [string]$ExtEnd,            
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RangeSize,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$HoldDays = 0,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("serviceLocationId")]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($RangeName -And !$Identity) {
            $RangeInfo = Get-NectarNumberRange -RangeName $RangeName -Tenant $TenantName -ResultSize 1
            $Identity = $RangeInfo.id
            
            If ($NewRangeName) {$RangeName = $NewRangeName}
            If (-not $FirstNumber) {$FirstNumber = $RangeInfo.firstNumber}
            If (-not $LastNumber) {$LastNumber = $RangeInfo.lastNumber}
            If (-not $RangeType) {$RangeType = $RangeInfo.type}
            If (-not $HoldDays) {$HoldDays = $RangeInfo.holdDays}
            If (-not $BaseNumber) {$BaseNumber = $RangeInfo.baseNumber}
            If (-not $ExtStart) {$ExtStart = $RangeInfo.extStart}
            If (-not $ExtEnd) {$ExtEnd = $RangeInfo.extEnd}
            # If (-not $LocationName) {$LocationID = $RangeInfo.serviceLocationId}
        }
            
        $LocationID = (Get-NectarNumberLocation -LocationName "$LocationName" -Tenant $TenantName -ErrorVariable LocError).ID
        
        $URI = "https://$Global:NectarCloud/dapi/numbers/range/$Identity/?tenant=$TenantName"

        $Body = @{
            name = $RangeName
            type = $RangeType
            holdDays = $HoldDays
            serviceLocationId = $LocationID
        }

        If ($FirstNumber) { $Body.Add('firstNumber', $FirstNumber) }
        If ($LastNumber) { $Body.Add('lastNumber', $LastNumber) }
        If ($BaseNumber) { $Body.Add('baseNumber', $BaseNumber) }
        If ($ExtStart) { $Body.Add('extStart', $ExtStart) }
        If ($ExtEnd) { $Body.Add('extEnd', $ExtEnd) }
        If ($RangeSize) { $Body.Add('rangeSize', $RangeSize) }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to apply changes for range $RangeName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarNumberRange {
    <#
        .SYNOPSIS
        Create a new Nectar range for DID Management
         
        .DESCRIPTION
        Create a new Nectar range for DID Management
 
        .PARAMETER RangeName
        The name of the new range. Must be unique.
         
        .PARAMETER RangeType
        The type of range. Can be either STANDARD (for DID ranges) or EXTENSION (for extension-based ranges).
         
        .PARAMETER FirstNumber
        The first number in a STANDARD range. Must be numeric, but can start with +.
         
        .PARAMETER LastNumber
        The last number in a STANDARD range. Must be numeric, but can start with +. Must be larger than FirstNumber, and must have the same number of digits.
         
        .PARAMETER BaseNumber
        The base DID for an EXTENSION range. Must be numeric, but can start with +.
         
        .PARAMETER ExtStart
        The first extension number in an EXTENSION range. Must be numeric.
         
        .PARAMETER ExtEnd
        The last extension number in an EXTENSION range. Must be numeric. Must be larger than ExtStart, and must have the same number of digits.
 
        .PARAMETER RangeSize
        The number of phone numbers/extensions in a range. Can be used instead of LastNumber/ExtEnd.
         
        .PARAMETER HoldDays
        The number of days to hold a newly-freed number before returning it to the pool of available numbers.
         
        .PARAMETER LocationName
        The location to assign the range to.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .EXAMPLE
        New-NectarNumberRange -RangeName DIDRange1 -RangeType STANDARD -FirstNumber +15552223333 -LastNumber +15552224444 -LocationName Dallas
        Creates a DID range for numbers that fall in the range of +15552223333 to +15552224444
 
        .EXAMPLE
        New-NectarNumberRange -RangeName DIDRange1 -RangeType STANDARD -FirstNumber +15552223000 -RangeSize 1000 -LocationName Dallas
        Creates a DID range for numbers that fall in the range of +15552223000 to +15552223999
         
        .EXAMPLE
        New-NectarNumberRange -RangeName ExtRange1 -RangeType EXTENSION -BaseNumber +15552223000 -ExtStart 2000 -ExtEnd 2999 -LocationName Dallas
        Creates an extension range for numbers that fall in the range of +15552223000 x2000 to x2999
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("nnnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("name")]
        [string]$RangeName, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet("STANDARD","EXTENSION", IgnoreCase=$True)]
        [string]$RangeType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$FirstNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$LastNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$BaseNumber,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^\d+$")]
        [string]$ExtStart,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^\d+$")]
        [string]$ExtEnd,            
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RangeSize,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$HoldDays,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $LocationID = (Get-NectarNumberLocation -LocationName $LocationName -Tenant $TenantName -ErrorVariable NumLocError).ID
        
        If ($LocationID.Count -gt 1) {
            Write-Error "Multiple locations found that match $LocationName. Please refine your location name search query"
            Break
        }
    
        $URI = "https://$Global:NectarCloud/dapi/numbers/range?tenant=$TenantName"
        $Body = @{
            name = $RangeName
            type = $RangeType
            holdDays = $HoldDays
            serviceLocationId = $LocationID
        }

        If ($FirstNumber) { $Body.Add('firstNumber', $FirstNumber) }
        If ($LastNumber) { $Body.Add('lastNumber', $LastNumber) }
        If ($BaseNumber) { $Body.Add('baseNumber', $BaseNumber) }
        If ($ExtStart) { $Body.Add('extStart', $ExtStart) }
        If ($ExtEnd) { $Body.Add('extEnd', $ExtEnd) }
        If ($RangeSize) { $Body.Add('rangeSize', $RangeSize) }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to create range $RangeName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarNumberRange {
    <#
        .SYNOPSIS
        Removes one or more ranges from a service location in the DID Management tool.
 
        .DESCRIPTION
        Removes one or more ranges from a service location in the DID Management tool.
         
        .PARAMETER RangeName
        The name of the number range to remove.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER Identity
        The numerical ID of the number range. Can be obtained via Get-NectarNumberRange and pipelined to Remove-NectarNumberRange
         
        .EXAMPLE
        Remove-NectarNumberRange Range1
        Removes the range Range1
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RangeName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )

    Begin {
        Connect-NectarCloud
    }    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($RangeName -And !$Identity) {
            $Identity = (Get-NectarNumberRange -RangeName $RangeName -Tenant $TenantName -ErrorVariable GetRangeError).ID
        }
        
        If ($Identity.Count -gt 1) {
            Write-Error "Multiple ranges found that match $RangeName. Please refine your range name search query"
            Break
        }
            
        If (!$GetRangeError) {
            $URI = "https://$Global:NectarCloud/dapi/numbers/range/$Identity/?tenant=$TenantName"
            
            Try {
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully deleted $RangeName number range."
            }
            Catch {
                Write-Error "Unable to delete $RangeName number range. Ensure you typed the name of the range correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# DID Number Management Functions #
# #
#################################################################################################################################################

Function Get-NectarNumber {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP numbers from the DID Management tool
 
        .DESCRIPTION
        Returns a list of Nectar DXP numbers from the DID Management tool
         
        .PARAMETER PhoneNumber
        The phone number to return information about. Can be a partial match. To return an exact match and to avoid ambiguity, enclose number with ^ at the beginning and $ at the end.
     
        .PARAMETER LocationName
        The name of the location to get number information about. Will be an exact match.
 
        .PARAMETER RangeName
        The name of the range to get number information about. Will be an exact match.
 
        .PARAMETER NumberState
        Returns information about numbers that are either USED, UNUSED or RESERVED
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarNumber
        Returns the first 10 numbers
         
        .EXAMPLE
        Get-NectarNumber -ResultSize 100
        Returns the first 100 numbers
 
        .EXAMPLE
        Get-NectarNumber -LocationName Tokyo
        Returns the first 10 numbers at the Tokyo location
 
        .EXAMPLE
        Get-NectarNumber -RangeName Range2
        Returns up to 10 numbers from a number range called Range2.
 
        .EXAMPLE
        Get-NectarNumber -RangeName Range2 -NumberState UNUSED -ResultSize 100
        Returns up to 100 unused numbers in the Range2 range.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnn")]    
    Param (
        [Parameter(Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$PhoneNumber,
        [Parameter(Mandatory=$False)]
        [string]$LocationName, 
        [Parameter(Mandatory=$False)]
        [string]$RangeName,
        [Parameter(Mandatory=$False)]
        [ValidateSet("USED","UNUSED","RESERVED", IgnoreCase=$True)]
        [string]$NumberState,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            If ($LocationName) {
                $LocationID = (Get-NectarNumberLocation -LocationName ^$LocationName$ -Tenant $TenantName -ResultSize 1 -ErrorVariable NectarError).ID 
            }
            
            If ($RangeName) {
                $RangeID = (Get-NectarNumberRange -RangeName $RangeName -LocationName $LocationName -Tenant $TenantName -ResultSize 1 -ErrorVariable +NectarError).ID
            }
            
            If ($PhoneNumber) {
                # Replace + with %2B if present
                $PhoneNumber = $PhoneNumber.Replace("+", "%2B")
            }
            
            $URI = "https://$Global:NectarCloud/dapi/numbers/"
            
            $Params = @{
                'orderByField' = 'number'
                'orderDirection' = 'asc'
            }
            
            If ($ResultSize) { 
                $Params.Add('pageSize', $ResultSize) 
            }
            Else { 
                $Params.Add('pageSize', $PageSize)
            }

            If ($LocationID) { $Params.Add('serviceLocationId', $LocationID) }
            If ($RangeID) { $Params.Add('numbersRangeId', $RangeID) }
            If ($NumberState) { $Params.Add('states', $NumberState) }
            If ($PhoneNumber) { $Params.Add('q', $PhoneNumber) }
            If ($TenantName) { $Params.Add('Tenant', $TenantName) }            
            
            If (!$NectarError) {
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -TypeName 'Nectar.Number.List'
                $JSON.elements
                
                $TotalPages = $JSON.totalPages
            
                If ($TotalPages -gt 1 -and !($ResultSize)) {
                    $PageNum = 2
                    Write-Verbose "Page size: $PageSize"
                    While ($PageNum -le $TotalPages) {
                        Write-Verbose "Working on page $PageNum of $TotalPages"
                        $PagedURI = $URI + "?pageNumber=$PageNum"
                        $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params
                        If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }
                        $JSON.elements | Add-Member -TypeName 'Nectar.Number.List'
                        $JSON.elements
                        $PageNum++
                    }
                }
            }
        }
        Catch {
            Write-Error "Unable to retrieve number information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}



Function Set-NectarNumber {
    <#
        .SYNOPSIS
        Makes changes to one or more phone numbers.
         
        .DESCRIPTION
        Makes changes to one or more phone numbers.
 
        .PARAMETER PhoneNumber
        A phone number to make changes to. Must be an exact match.
         
        .PARAMETER NumberState
        Change the state of a phone number to either UNUSED or RESERVED. A number marked USED cannot be modified.
         
        .PARAMETER Comment
        A comment to add to a reserved phone number.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarNumber +12223334444 -NumberState RESERVED
        Reserves the number +12223334444
         
        .NOTES
        Version 1.1
    #>


    [Alias("snn")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName,Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$PhoneNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("UNUSED","RESERVED", IgnoreCase=$True)]
        [string]$NumberState,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,254)]
        [string]$Comment,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias("id")]
        [String]$Identity
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($PhoneNumber -And !$Identity) {
            $Identity = (Get-NectarNumber -PhoneNumber $PhoneNumber -Tenant $TenantName -ResultSize 1 -ErrorVariable PhoneNumError).ID
        }
        
        If (!$PhoneNumError) {
            $URI = "https://$Global:NectarCloud/dapi/numbers/$Identity/state?state=$NumberState&tenant=$TenantName"
        
            If (($Comment) -And ($NumberState -eq "RESERVED")) {
                # Convert special characters to URI-compatible versions
                #$Comment = [uri]::EscapeDataString($Comment)
                $URI += "&comment=$Comment"
            }
            
            Try {
                $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader
                Write-Verbose "Successfully applied changes to $PhoneNumber."
            }
            Catch {
                Write-Error "Unable to apply changes for phone number $PhoneNumber. The number may already be in the desired state."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



Function Get-NectarUnallocatedNumber {
    <#
        .SYNOPSIS
        Returns the next available number in a given location/range.
 
        .DESCRIPTION
        Returns the next available number in a given location/range.
         
        .PARAMETER LocationName
        The service location to return a number for
         
        .PARAMETER RangeName
        The range to return a number for
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarUnallocatedNumber -RangeName Jericho
        Returns the next available number in the Jericho range.
             
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnun")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$LocationName, 
        [Parameter(Mandatory=$False)]
        [string]$RangeName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    # Use globally set tenant name, if one was set and not explicitly included in the command
    If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
    
    $NextFreeNum = Get-NectarNumber -LocationName $LocationName -RangeName $RangeName -NumberState UNUSED -Tenant $TenantName -ResultSize 1 

    If ($NextFreeNum) {
        Return $NextFreeNum
    }
    Else {
        Write-Error "No available phone number found."
    }
}




#################################################################################################################################################
# #
# Supported Device Functions #
# #
#################################################################################################################################################

Function Get-NectarSupportedDevice {
    <#
        .SYNOPSIS
        Get information about 1 or more Nectar DXP supported devices.
         
        .DESCRIPTION
        Get information about 1 or more Nectar DXP supported devices.
 
        .PARAMETER SearchQuery
        A full or partial match of the device's name
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-NectarSupportedDevice -SearchQuery Realtek
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnd")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )    
    
    Begin {
        # Get the encoding for resolving special characters
        $TextEncoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/supported/devices?q=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
        Try {
            # Has to be done this way to get the proper UTF8 encoded characters
            $WebContent = Invoke-WebRequest -Method GET -Uri $URI -Headers $Global:NectarAuthHeader
            $JSON = ([System.Text.Encoding]::UTF8).GetString($TextEncoding.GetBytes($WebContent.Content)) | ConvertFrom-Json

            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.DeviceList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "Unable to get device details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarSupportedDevice {
    <#
        .SYNOPSIS
        Update 1 or more Nectar DXP supported device.
         
        .DESCRIPTION
        Update 1 or more Nectar DXP supported device.
 
        .PARAMETER DeviceName
        The name of the supported device
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER Identity
        The numerical identity of the supported device
         
        .EXAMPLE
        Set-NectarSupportedDevice Identity 233 -Supported $FALSE
         
        .EXAMPLE
        Get-NectarSupportedDevice -SearchQuery realtek | Set-NectarSupportedDevice -Supported $FALSE
        Sets all devices with 'Realtek' in the name to Unsupported
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("snsd")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DeviceName,
        [Alias('deviceSupported')]
        [bool]$Supported,
        [Alias('devicePlatform')]
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $UrlEncodedDeviceName = [uri]::EscapeDataString($DeviceName)

        $URI = "https://$Global:NectarCloud/aapi/supported/device?deviceName=$UrlEncodedDeviceName&platform=$Platform&tenant=$TenantName"

        Write-Verbose $URI

        $Body = @{
            deviceSupported = $Supported
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to apply changes for device $DeviceName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



#################################################################################################################################################
# #
# Supported Client Functions #
# #
#################################################################################################################################################

Function Get-NectarSupportedClient {
    <#
        .SYNOPSIS
        Get information about 1 or more Nectar DXP supported client versions.
         
        .DESCRIPTION
        Get information about 1 or more Nectar DXP supported client versions.
 
        .PARAMETER SearchQuery
        A full or partial match of the client versions's name
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarSupportedClient -SearchQuery Skype
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnsc")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/supported/client/versions?q=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
        Try {
            $JSON = Invoke-RestMethod -Method GET -uri $URI     -Headers $Global:NectarAuthHeader
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.ClientList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "Unable to get client version details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarSupportedClient {
    <#
        .SYNOPSIS
        Update 1 or more Nectar DXP supported client versions.
         
        .DESCRIPTION
        Update 1 or more Nectar DXP supported client versions.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER Identity
        The numerical identity of the supported client version.
         
        .EXAMPLE
        Set-NectarSupportedClient Identity 233 -Supported $FALSE
        Sets the device with identity 233 to unsupported
     
        .NOTES
        Version 1.1
    #>

    
    [Alias("snsc")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("version")]
        [string]$ClientVersion,
        [Alias("clientVersionSupported")]
        [bool]$Supported,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("versionId")]
        [int]$Identity,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Platform
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($ClientVersion -and !$Identity) {
            $ClientInfo = Get-NectarSupportedClient -SearchQuery $ClientVersion -Tenant $TenantName -ResultSize 1
            
            $ClientVersion = $ClientInfo.version
            If ($NULL -eq $Supported) {$Supported = $ClientInfo.clientVersionSupported}
            $Identity = $ClientInfo.versionId
            $Platform = $ClientInfo.platform
        }
            
        $URI = "https://$Global:NectarCloud/aapi/supported/client/version?versionName=$ClientVersion&tenant=$TenantName"

        $Body = @{
             clientVersionSupported = $Supported
             platform = $Platform
             versionId = $Identity
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            If ($ClientVersion) {
                $IDText = $ClientVersion
            }
            Else {
                $IDText = "with ID $Identity"
            }
            
            Write-Error "Unable to apply changes for client version $IDText."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



#################################################################################################################################################
# #
# User Functions #
# #
#################################################################################################################################################

Function Get-NectarUser {
    <#
        .SYNOPSIS
        Get information about 1 or more users via Nectar DXP.
         
        .DESCRIPTION
        Get information about 1 or more users via Nectar DXP.
 
        .PARAMETER SearchQuery
        A full or partial match of the user name. Will do an 'includes' type search by default. So searching for user@domain.com would return Auser@domain.com, Buser@domain.com etc.
        For a specific match, enclose the name in square brackets IE: [user@domain.com]
 
        .PARAMETER FilterSearch
        Uses an alternate API for returning users. Has better searching capabilities and should be used in conjunction with commands that allow user filtering
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarUser -SearchQuery tferguson
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnu")]
    Param (
        [Parameter(Mandatory=$True)]
        [string]$SearchQuery,
        [Parameter(Mandatory=$False)]
        [switch]$FilterSearch,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$ResultSize = 10000
    )    
    
    Begin {
        Connect-NectarCloud
        
        # Get the encoding for resolving special characters
        $TextEncoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $EncodedSearchQuery = [System.Web.HttpUtility]::UrlEncode($SearchQuery)
        
        # Filtersearch query should be used in support of a filter query for use in session searches. Has better searching ability than the other method.
        # For example, the FilterSearch API allows for searching with alternate characters like periods, while the other method seems to ignore it selectively
        # The other method presents more data, which may be useful for other purposes.
        If ($FilterSearch) { 
            $URI = "https://$Global:NectarCloud/dapi/info/session/users?q=$EncodedSearchQuery&usersForAlerts=false&pageSize=$ResultSize&tenant=$TenantName"
            $UserFilterSession = Set-NectarFilterParams -Scope $Scope
            $WebContent = Invoke-WebRequest -Method GET -uri $URI -Headers $Global:NectarAuthHeader -WebSession $UserFilterSession 
        }
        Else {
            $URI = "https://$Global:NectarCloud/dapi/user/search?q=$EncodedSearchQuery&pageSize=$ResultSize&tenant=$TenantName"
            $WebContent = Invoke-WebRequest -Method GET -uri $URI -Headers $Global:NectarAuthHeader
        }

        Write-Verbose $URI
        
        Try {
            $JSON = ([System.Text.Encoding]::UTF8).GetString($TextEncoding.GetBytes($WebContent.Content)) | ConvertFrom-Json

            If ($JSON.totalElements -eq 0) {
                Write-Error "Cannot find user with name $SearchQuery."
            }
            If ($TenantName) { # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty
            } 
            Return $JSON.elements
        }
        Catch {
            Write-Error "Cannot find user with name $SearchQuery."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarUserOverview {
    <#
        .SYNOPSIS
        Returns a usage overview about a given user
 
        .DESCRIPTION
        Returns a usage overview about a given user
         
        .PARAMETER EmailAddress
        The email address of the user.
     
        .PARAMETER Identity
        The numerical ID of the user. Can be obtained via Get-NectarUser and pipelined to Get-NectarUserOverview
 
        .PARAMETER ExcludeBaseInfo
        Exclude basic user information from the output. Saves an API call.
 
        .PARAMETER ExcludeCallCount
        Exclude call count information from the output. Saves an API call.
 
        .PARAMETER ExcludeHealth
        Exclude user health information from the output. Saves an API call.
         
        .EXAMPLE
        Get-NectarUserOverview tferguson@nectarcorp.com
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("Get-NectarUserDetails")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('name')]
        [string]$SearchQuery, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('userId')]
        [string]$Identity,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Email,
        [Parameter(Mandatory=$False)]
        [switch]$ExcludeBaseInfo,
        [Parameter(Mandatory=$False)]
        [switch]$ExcludeCallCount,
        [Parameter(Mandatory=$False)]
        [switch]$ExcludeHealth,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        Write-Verbose "Search Query: $SearchQuery"

        If ($SearchQuery -and !$Identity) {
            Write-Verbose "Searching..."
            $UserInfo = Get-NectarUser -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1 -ErrorVariable GetUserError
            $Identity = $UserInfo.UserId
            If ($UserInfo.email) { $Email = $UserInfo.email }
            Write-Verbose "Identity: $Identity Email: $Email"
        }
            
        If (!$GetUserError) {
            $BaseURI = "https://$Global:NectarCloud/dapi/user/$($Identity)?tenant=$TenantName"
            $CallCountURI = "https://$Global:NectarCloud/dapi/user/$($Identity)/summary/calls?tenant=$TenantName"
            $HealthURI = "https://$Global:NectarCloud/dapi/user/$($Identity)/health/score?tenant=$TenantName"
            
            Try {
                If (!$ExcludeBaseInfo) {
                    $JSON = Invoke-RestMethod -Method GET -uri $BaseURI -Headers $Global:NectarAuthHeader
                }
                Else {
                    $JSON = [pscustomobject]@{}
                }

                If (!$ExcludeCallCount) { $CallCountJSON = Invoke-RestMethod -Method GET -uri $CallCountURI -Headers $Global:NectarAuthHeader }
                If (!$ExcludeHealth) { $HealthJSON = Invoke-RestMethod -Method GET -uri $HealthURI -Headers $Global:NectarAuthHeader }
                
                # Add the call count info to the output
                If ($CallCountJSON) {
                    If ($ExcludeBaseInfo) { 
                        $JSON | Add-Member -Name 'Id' -Value $Identity -MemberType NoteProperty
                        $JSON | Add-Member -Name 'Email' -Value $Email -MemberType NoteProperty
                    }
                    $CallCountJSON.psobject.Properties | ForEach-Object {
                        $JSON | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value -Force
                    }
                }
                    
                # Add the health info to the output
                If ($HealthJSON) {
                    If ($ExcludeBaseInfo) { 
                        $JSON | Add-Member -Name 'Id' -Value $Identity -MemberType NoteProperty -ErrorAction SilentlyContinue
                        $JSON | Add-Member -Name 'Email' -Value $Email -MemberType NoteProperty -ErrorAction SilentlyContinue
                    }
                    $HealthJSON.psobject.Properties | ForEach-Object {
                        $JSON | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value -Force
                    }
                }
                
                If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
                $Identity = $NULL
                Return $JSON
            }
            Catch {
                Write-Error "Unable to find user $EmailAddress. Ensure you typed the name of the user correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



Function Get-NectarUserAdvancedInfo {
    <#
        .SYNOPSIS
        Returns a usage overview about a given user
 
        .DESCRIPTION
        Returns a usage overview about a given user
         
        .PARAMETER EmailAddress
        The email address of the user.
     
        .PARAMETER Identity
        The numerical ID of the user. Can be obtained via Get-NectarUser and pipelined to Get-NectarUserAdvanced
         
        .EXAMPLE
        Get-NectarUserAdvancedInfo tferguson@nectarcorp.com
oioioioioioioioioioioioioioio
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("email")]
        [string]$EmailAddress, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("userId")]
        [string]$Identity,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($EmailAddress -and !$Identity) {
            $Identity = (Get-NectarUser -SearchQuery $EmailAddress -Tenant $TenantName -ResultSize 1 -ErrorVariable GetUserError).UserId
            Write-Verbose "Identity: $Identity"
        }
            
        If (!$GetUserError) {
            $URI = "https://$Global:NectarCloud/dapi/user/$($Identity)/advanced?tenant=$TenantName"
            
            Try {
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
                
                If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
                Return $JSON
            }
            Catch {
                Write-Error "Unable to find user $EmailAddress. Ensure you typed the name of the user correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# Session Detail Functions #
# #
#################################################################################################################################################

Function Set-NectarFilterParams {
    <#
        .SYNOPSIS
        Sets the filter parameters used for querying session data
         
        .DESCRIPTION
        Sets the filter parameters used for querying session data. This is called before any API used for filtering session data
         
        .OUTPUTS
        WebSession cookie to use in other JSON requests
 
        .EXAMPLE
        Set-NectarFilterParams
 
        .NOTES
        Version 1.1
    #>


    [Alias("snfp")]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [string]$NectarScore,    
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,608)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$ExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CallerExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CalleeExtConnectionTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('EXTERNAL','EXTERNAL_FEDERATED','EXTERNAL_INTERNAL','FEDERATED','FEDERATED_EXTERNAL','FEDERATED_INTERNAL','INTERNAL','INTERNAL_EXTERNAL','INTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY', IgnoreCase=$False)]
        [string[]]$Insights,
        [Parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN','INCOMPLETE','INCOMPLETE', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [switch]$ShowQualityDetails    
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # Convert to all-caps
        If ($Scope) { $Scope = $Scope.ToUpper() }
        If ($TimePeriod) { $TimePeriod = $TimePeriod.ToUpper() }    
        
        $FilterParams = @{
            'TimePeriod' = $TimePeriod
        }
    
        # Convert any PowerShell array objects to comma-separated strings to add to the GET querystring
        # Convert multi-string variables into a comma-delimited list and add to the FilterParams array. For variables that need to be in upper-case
        $MultiVarListUpper = 'SessionQualities','SessionScenarios','SessionTypes','Modalities','Platforms','Scenarios',
                            'VPN','CallerVPN','CalleeVPN',
                            'FeedbackRating','Insights','TestTypes','TestResults'
            
        ForEach ($MultiVar in $MultiVarListUpper) {
            If ((Get-Variable $MultiVar).Value) {
                (Get-Variable $MultiVar).Value.ToUpper() | ForEach-Object { $MultiVarString += ($(if($MultiVarString){","}) + $_) }
                $FilterParams.Add($MultiVar,$MultiVarString)
                Write-Verbose "$MultiVar`: $MultiVarString"
                Remove-Variable MultiVarString
            }
        }
        
        # Convert multi-string variables into a comma-delimited list and add to the FilterParams array. For variables that do not need to be in upper-case
        $MultiVarList = 'Protocols','ResponseCodes','Servers',
                        'Codecs','CallerCodecs','CalleeCodecs',
                        'Devices','CallerDevices','CalleeDevices',
                        'DeviceVersions','CallerDeviceVersions','CalleeDeviceVersions',
                        'Locations','CallerLocations','CalleeLocations',
                        'ExtCities','CallerExtCities','CalleeExtCities',
                        'ExtCountries','CallerExtCountries','CalleeExtCountries',
                        'ExtISPs','CallerExtISPs','CalleeExtISPs',
                        'extConnectionTypes','callerExtConnectionTypes','calleeExtConnectionTypes',
                        'NetworkTypes','CallerNetworkTypes','CalleeNetworkTypes',
                        'ipAddresses','callerIpAddresses','calleeIpAddresses',
                        'endpointTypes','callerEndpointTypes','calleeEndpointTypes'
                        
        ForEach ($MultiVar in $MultiVarList) {
            If ((Get-Variable $MultiVar).Value) {
                (Get-Variable $MultiVar).Value | ForEach-Object { [string]$MultiVarString += ($(if([string]$MultiVarString){","}) + $_) }

                # Update name of parameters if necessary
                Switch ($MultiVar) {
                    'Servers' { $MultiVar = 'platformServersOrDataCenters' }
                }
                $FilterParams.Add($MultiVar,$MultiVarString)
                Write-Verbose "$MultiVar`: $MultiVarString"
                Remove-Variable MultiVarString
            }
        }
        
        # Get user IDs and convert into a comma-delimited list and add to the FilterParams array.
        $UserVarList = 'Users','FromUsers','ToUsers'
        
        ForEach ($UserVar in $UserVarList) {
            If ((Get-Variable $UserVar).Value) {
                $UserList = (Get-Variable $UserVar).Value
                
                $UserIDList = @()
                $PlatformUserNames = @()
                ForEach ($User in $UserList) {
                    Write-Verbose "UserSearch: $User"
                    $UserInfo = Get-NectarUser $User -Scope $Scope -TenantName $TenantName -FilterSearch -ResultSize 10000
                    If ($UserInfo.id) { 
                        $UserIDList += $UserInfo.id 
                    }
                    ElseIf ($UserInfo.platformUserName) {
                        $PlatformUserNames += $UserInfo.platformUserName
                    }
                }
                
                If ($UserIDList) {    
                    $UserIDList | ForEach-Object { $UserIDString += ($(if($UserIDString){","}) + $_) } 
                    $FilterParams.Add($UserVar,$UserIDString)
                    Write-Verbose "UserID $UserVar`: $UserIDString"
                    Remove-Variable UserIDString
                }
                
                If ($PlatformUserNames) { 
                    $PlatformUserNames | ForEach-Object { $PlatformUserNameString += ($(if($PlatformUserNameString){","}) + $_) }
                    $FilterParams.Add($UserVar.Replace('Users','PlatformUserNames'),$PlatformUserNameString)
                    Write-Verbose "Platform $UserVar`: $PlatformUserNameString"
                    Remove-Variable PlatformUserNameString
                }
                
                If (!$UserIDList -And !$PlatformUserNames) { Write-Error 'No matching users found'; Return }
            }
        }

        # Do the same for UserIDs
        $UserIDVarList = 'UserIDs','FromUserIDs','ToUserIDs'
        
        ForEach ($UserIDVar in $UserIDVarList) {
            If ((Get-Variable $UserIDVar).Value) {
                $UserIDList = (Get-Variable $UserIDVar).Value
                
                If ($UserIDList) {    
                    $UserIDList | ForEach-Object { $UserIDString += ($(if($UserIDString){","}) + $_) } 
                    $FilterParams.Add($UserIDVar.Replace('ID',''),$UserIDString)
                    Write-Verbose "UserID $($UserIDVar.Replace('ID',''))`: $UserIDString"
                    Remove-Variable UserIDString
                }
            }
        }

        
        # Get agent versions and convert into a comma-delimited list and add to the FilterParams array.
        $AgentVarList = 'AgentVersions','CallerAgentVersions','CalleeAgentVersions'
        
        ForEach ($AgentVar in $AgentVarList) {
            If ((Get-Variable $AgentVar).Value) {
                $AgentList = (Get-Variable $AgentVar).Value
                
                # Parse through each entry and search for results
                $FinalAgentList = @()
                ForEach ($Agent in $AgentList) {
                    Write-Verbose "AgentSearch: $Agent"
                    $AgentInfo = Get-NectarAgentVersion -SearchQuery $Agent -TenantName $TenantName
                    If ($AgentInfo) { 
                        $FinalAgentList += $AgentInfo
                    }
                }
                
                # Convert to comma-delimited list and add to FilterParams array
                If ($FinalAgentList) {    
                    $FinalAgentList | ForEach-Object { $AgentString += ($(if($AgentString){","}) + $_) } 
                    # Make var match with the API (ie replace AgentVersions with AgentVersion)
                    $FilterParams.Add($AgentVar.Substring(0,$AgentVar.Length-1),$AgentString)
                    Write-Verbose "Agent $AgentVar.Substring(0,$AgentVar.Length-1)`: $AgentString"
                    Remove-Variable AgentString
                }
                
                If (!$FinalAgentList) { Write-Error 'No matching agent versions found'; Return }
            }
        }


        # Add single parameter variables to the FilterParams array.
        $VarList = 'NectarScore','DurationFrom','DurationTo','ParticipantsMinCount','ParticipantsMaxCount'
        
        ForEach ($Var in $VarList) {
            If ((Get-Variable $Var).Value) {
                $FilterParams.Add($Var,(Get-Variable $Var).Value)
                Write-Verbose "$Var`: (Get-Variable $Var).Value"
            }
        }

        # Add time-based parameter variables to the FilterParams array. This converts to UNIX timestamp
        
        $TimeVarList = 'TimePeriodFrom','TimePeriodTo'

        # Set TimePeriodTo to NOW if not explicitly set
        If ($TimePeriodFrom -And !$TimePeriodTo) {
            [String]$TimePeriodTo = Get-Date
        }
        
        ForEach ($TimeVar in $TimeVarList) {
            If ((Get-Variable $TimeVar).Value) {
                [decimal]$TimePeriodUnix = Get-Date -Date (Get-Variable $TimeVar).Value -UFormat %s
                [long]$TimePeriodUnix = $TimePeriodUnix * 1000
                $FilterParams.Add($TimeVar.Replace('TimePeriod','StartDate'),$TimePeriodUnix)
                Write-Verbose "$TimeVar`: $TimePeriodUnix"
            }
        }
        
        If ($ConfOrganizers) {
            $ConfOrganizerIDs = ForEach($Organizer in $ConfOrganizers) {
                (Get-NectarUser $Organizer -Scope $Scope -TenantName $TenantName -ErrorAction:Stop).Userid
            }
            $ConfOrganizerIDs | ForEach-Object { $ConfOrganizerIDsStr += ($(if($ConfOrganizerIDsStr){","}) + $_) }
            $FilterParams.Add('organizersOrSpaces',$ConfOrganizerIDsStr)
        }

        
        If ($ShowQualityDetails) {
            $FilterParams.Add('sessionQualitySources','CDS,CDR_CDS')
            $FilterParams.Add('excludeIncompleteRecords','true')
        }

        Try {
            # Run the filter POST and obtain the session cookie for the GET
            $URI = "https://$Global:NectarCloud/dapi/filter/apply?scope=$Scope&tenant=$TenantName"
            $NULL = Invoke-WebRequest -Method POST -Headers $Global:NectarAuthHeader -uri $URI -Body $FilterParams -UseBasicParsing -SessionVariable Session 
            $Cookie = $Session.Cookies.GetCookies($URI) | Where-Object {$_.Name -eq 'SESSION'} 
            $FilterSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
            $FilterSession.Cookies.Add($Cookie)
            Write-Verbose "Successfully set filter parameters."    
            Write-Verbose $Uri
            ForEach($k in $FilterParams.Keys){ Write-Verbose "$k`: $($FilterParams[$k])" }
            Return $FilterSession
        }
        Catch {
            Write-Error "Unable to set filter parameters."
            (Get-JSONErrorStream -JSONResponse $_).Replace("startDate","TimePeriod")
        }
    }
}



Function Get-NectarSession {
    <#
        .SYNOPSIS
        Returns all session information.
 
        .DESCRIPTION
        Returns all session information as presented on the SESSION LIST section of the CALL DETAILS page
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER NectarScore
        Show sessions that match the given range of NectarScores. Use format aa-xx or aa.bb-xx.yy, where aa is less than xx, and xx is less than 100. Eg. 70-90 or 95.00-95.59
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER ExtConnectionTypes
        Show sessions that match the caller or callee external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
 
        .PARAMETER CallerExtConnectionTypes
        Show sessions that match the caller's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER CalleeExtConnectionTypes
        Show sessions that match the callee's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER Insights
        Show sessions that match one or more given insights. Choose from NORMAL_SESSION, VOICE_MAIL, HIGH_JITTER, HIGH_PACKET_LOSS, HIGH_ROUNDTRIP_DELAY
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarSession -TimePeriod LAST_HOUR -Platforms TEAMS -Modalities AUDIO -NectarScore 50-70
        Returns a list of all Teams audio sessions for the past hour where the NectarScore is between 50% and 70%
         
        .EXAMPLE
        (Get-NectarSession -SessionTypes CONFERENCE -TimePeriod CUSTOM -TimePeriodFrom '2021-05-06' -TimePeriodTo '2021-05-07').Count
        Returns a count of all conferences between May 6 and 7 (all times/dates UTC)
         
        .EXAMPLE
        Get-NectarSession -SessionTypes PEER2PEER,PEER2PEER_MULTIMEDIA -TimePeriod CUSTOM -TimePeriodFrom '2021-05-06 14:00' -TimePeriodTo '2021-05-06 15:00'
        Returns a list of all P2P calls between 14:00 and 15:00 UTC on May 6
 
        .EXAMPLE
        Get-NectarSession -TimePeriod LAST_WEEK -SessionTypes CONFERENCE | Select-Object confOrganizerOrSpace | Group-Object confOrganizerOrSpace | Select-Object Name, Count | Sort-Object Count -Descending
        Returns a list of conference organizers and a count of the total conferences organized by each, sorted by count.
         
        .NOTES
        Version 1.2
    #>

    
    [Alias('gns', 'Get-NectarSessions')]
    [CmdletBinding(PositionalBinding=$False)]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match '^\d?\d(.\d+)?\-(\d?\d|100)(.\d+)?$') {
                $True
            } 
            Else {
                Throw 'NectarScore must be in the format aa-xx (ie. 80-90) or aa.bb-xx.yy (ie 77.32-80.99), where aa is less than xx and xx cannot be greater than 100.'
            }
        })]
        [string]$NectarScore,    
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$ExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CallerExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CalleeExtConnectionTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY', IgnoreCase=$False)]
        [string[]]$Insights,
        [switch]$ShowQualityDetails,
        [Parameter(Mandatory=$False)]
        [string]$OrderByField,
        [Parameter(Mandatory=$False)]
        [ValidateSet('ASC','DESC', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'CALL_DETAILS_HISTORIC',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,10000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,50000)]
        [int]$ResultSize
    )
    
    Begin {
        # Get the encoding for resolving special characters
        $TextEncoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($PageSize)       { $PSBoundParameters.Remove('PageSize') | Out-Null }
        If ($ResultSize)     { $PSBoundParameters.Remove('ResultSize') | Out-Null }
        If ($OrderByField)   { $PSBoundParameters.Remove('OrderByField') | Out-Null }
        If ($OrderDirection) { $PSBoundParameters.Remove('OrderDirection') | Out-Null }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters
        $URI = "https://$Global:NectarCloud/dapi/session"
        Write-Verbose $URI        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) {
            $Params = @{ 'pageSize' = $ResultSize }
        }
        Else {
            $Params = @{ 'pageSize' = $PageSize }
        }
        
        If ($OrderByField)   { $Params.Add('OrderByField',$OrderByField) }
        If ($OrderDirection) { $Params.Add('OrderDirection',$OrderDirection) }
        If ($TenantName)     { $Params.Add('Tenant',$TenantName) }
        
        # Return results in pages
        # Try {
            $WebContent = Invoke-WebRequest -Method GET -Uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession 
            $JSON = ([System.Text.Encoding]::UTF8).GetString($TextEncoding.GetBytes($WebContent.Content)) | ConvertFrom-Json
            
            #$JSON = Invoke-RestMethod -Method GET -Uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    
                    $WebContent = Invoke-WebRequest -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
                    $JSON = ([System.Text.Encoding]::UTF8).GetString($TextEncoding.GetBytes($WebContent.Content)) | ConvertFrom-Json
                    
                    If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }
                    
                    $JSON.elements
                    $PageNum++
                }
            }
        # }
        # Catch {
        # Write-Error "No results. Try specifying a less-restrictive filter"
        # Get-JSONErrorStream -JSONResponse $_
        # }
    }
}



Function Get-NectarSessionCount {
    <#
        .SYNOPSIS
        Returns session counts broken down by hour/day (depending on selected time filter) for a given timeframe. Includes breakdown by quality and NectarScore by hour/day.
         
        .DESCRIPTION
        Returns session counts broken down by hour/day (depending on selected time filter) for a given timeframe. This returns the numbers used to build the chart in the SESSIONS section of the CALL DETAILS screen
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
 
        .PARAMETER NectarScore
        Show sessions that match the given range of NectarScores. Use format aa-xx or aa.bb-xx.yy, where aa is less than xx, and xx is less than 100. Eg. 70-90 or 95.00-95.59
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER ExtConnectionTypes
        Show sessions that match the caller or callee external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
 
        .PARAMETER CallerExtConnectionTypes
        Show sessions that match the caller's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER CalleeExtConnectionTypes
        Show sessions that match the callee's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
         
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER Insights
        Show sessions that match one or more given insights. Choose from NORMAL_SESSION, VOICE_MAIL, HIGH_JITTER, HIGH_PACKET_LOSS, HIGH_ROUNDTRIP_DELAY
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionCount -TimePeriod LAST_DAY
        Returns an hour-by-hour count of the number of sessions occuring over the past day along with quality breakdown
 
        .EXAMPLE
        Get-NectarSessionCount -TimePeriod LAST_WEEK -Platform TEAMS
        Returns an day-by-day count of the number of sessions occuring over the past week along with quality breakdown for MS Teams
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("Get-NectarSessionHistogram")]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match '^\d\d(.\d+)?\-(\d\d|100)(.\d+)?$') {
                $True
            } 
            Else {
                Throw 'NectarScore must be in the format aa-xx (ie. 80-90) or aa.bb-xx.yy (ie 77.32-80.99), where aa is less than xx and xx cannot be greater than 100.'
            }
        })]
        [string]$NectarScore,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities = 'TOTAL',
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$ExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CallerExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CalleeExtConnectionTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY', IgnoreCase=$False)]
        [string[]]$Insights,
        [switch]$ShowQualityDetails,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'CALL_DETAILS_HISTORIC'
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Params = @{}
            
            If ($Modalities) { $Modalities | ForEach-Object { $ModalitiesStr += ($(if($ModalitiesStr){","}) + $_) }; $Params.Add('Modalities',$ModalitiesStr) }
            
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            #Remove Modalities from FilterSession POST. For some reason, TOTAL results come back as empty if this is set
            $PSBoundParameters.Remove('Modalities') | Out-Null
            $FilterSession = Set-NectarFilterParams @PsBoundParameters
            $URI = "https://$Global:NectarCloud/dapi/quality/nectarscore/bar/graph?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession

            # Flatten the output so that session summary details are at the top level instead of nested under "sessions"
            $FinalList = @()
            $CultureTextInfo = (Get-Culture).TextInfo

            ForEach ($Row in $JSON) {
                $NewArray = New-Object PSObject
                $NewArray | Add-Member -NotePropertyName 'StartDate' -NotePropertyValue $Row.StartDate
                $NewArray | Add-Member -NotePropertyName 'EndDate' -NotePropertyValue $Row.EndDate
                ForEach ($Item in $Row.sessions) {
                    $ObjectProperties = $Item.PSObject.Properties
                    ForEach ($Property in $ObjectProperties) {
                        If ($Property.Name -ne 'avgnectarscore') {
                            $NewArray | Add-Member -NotePropertyName $CultureTextInfo.ToTitleCase($Property.Name) -NotePropertyValue $Property.Value
                        }
                        Else {
                            $NewArray | Add-Member -NotePropertyName 'AvgNectarScore' -NotePropertyValue ([math]::Round($Property.Value, 3))
                        }
                    }
                    If ($TenantName) {$NewArray | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                }

                $FinalList += $NewArray
            }

            Return $FinalList
        }
        Catch {
            Write-Error 'Session count not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionSummary {
    <#
        .SYNOPSIS
        Returns call summary information for a given session
         
        .DESCRIPTION
        Returns call summary information for a given session. This is used to populate the top few sections of an individual session on the session OVERVIEW screen.
        UI_ELEMENT
 
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionSummary 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS
        Returns summary information for a specific Teams session
         
        .EXAMPLE
        Get-NectarSession -Platform TEAMS -Users tferguson@contoso.com -SessionTypes PEER2PEER -TimePeriod LAST_DAY | Get-NectarSessionSummary -Platform TEAMS
        Returns summary information for all Teams peer-to-peer calls for TFerguson for the last day.
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnss")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/session/$SessionID/summary?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            $SessionSummary = [pscustomobject][ordered]@{
                SessionID = $SessionID
                StartTime = $JSON.startTime
                EndTime = $JSON.endTime
                Duration = $JSON.duration
                Quality = $JSON.quality
                CallerRenderDevice = $JSON.caller.renderDevice.value
                CallerCaptureDevice = $JSON.caller.captureDevice.value
                CallerClientVersion = $JSON.caller.clientVersion.value
                CallerNetworkType = $JSON.caller.networkType.value
                CallerNetworkWarning = $JSON.caller.networkType.warning
                CallerServer = $JSON.caller.server.value
                CallerServerAlertLevel = $JSON.caller.server.alertLevel
                CalleeRenderDevice = $JSON.callee.renderDevice.value
                CalleeCaptureDevice = $JSON.callee.captureDevice.value
                CalleeClientVersion = $JSON.callee.clientVersion.value
                CalleeNetworkType = $JSON.callee.networkType.value
                CalleeNetworkWarning = $JSON.callee.networkType.warning
                CalleeServer = $JSON.callee.server.value
                CalleeServerAlertLevel = $JSON.callee.server.alertLevel
            }

            Return $SessionSummary
        }
        Catch {
            Write-Error 'Session diagnostics not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionParticipant {
    <#
        .SYNOPSIS
        Returns session participant information for a given session
         
        .DESCRIPTION
        Returns session participant information for a given session. This is used to populate the participants view in an individual session on the session OVERVIEW screen.
        UI_ELEMENT
 
        .PARAMETER SessionID
        The session ID of the selected session
 
        .PARAMETER CallerOrCallee
        Which participant to return participant information from. Select either Caller or Callee
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionParticipant 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS -CallerOrCallee Caller
        Returns caller information for a specific Teams session
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnsp")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('Caller','Callee', IgnoreCase=$True)]
        [string]$CallerOrCallee,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/session/$SessionID/$($CallerOrCallee.ToLower())?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            Return $JSON
        }
        Catch {
            Write-Error 'Session participant info not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Get-NectarSessionDetails {
    <#
        .SYNOPSIS
        Returns details for a given session
         
        .DESCRIPTION
        Returns details for a given session. This is used to populate the session ADVANCED screen for a given session.
        UI_ELEMENT
         
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
         
        .PARAMETER IncludeMetrics
        The name of one or more metrics separated by commas to include in the output. All other metrics will be excluded.
        Useful in situations where only a few metrics are required in large pipelining scripts, where the time to execute can be reduced by up to 33%
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionDetails 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS
        Returns detailed information for a specific Teams session
         
        .EXAMPLE
        Get-NectarSession -Platform TEAMS -Users tferguson@contoso.com -SessionTypes PEER2PEER,PEER2PEER_MULTIMEDIA -TimePeriod LAST_DAY | Get-NectarSessionDetails
        Returns detailed information for all Teams peer-to-peer and multimedia P2P calls for TFerguson for the last day.
 
        .NOTES
        Version 1.2
    #>

    [Alias("gnsd")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("ID")]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter( Mandatory=$False)]
        [string[]]$IncludeMetrics,
        [Parameter(ValueFromPipelineByPropertyName=$True, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/session/$SessionID/advanced?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            $SessionDetails = [pscustomobject][ordered]@{
                SessionID = $SessionID
                SessionType = $JSON.type
                Caller = $JSON.caller
                Callee = $JSON.callee
            }

            If ($IncludeMetrics) { # Only pull the described metrics
                ForEach ($DataGroup in $JSON.groups) {
                    ForEach ($DataElement in $DataGroup.data | Where-Object {$_.AlertName -In $IncludeMetrics}) {
                        $SessionDetails | Add-Member -NotePropertyName $DataElement.AlertName -NotePropertyValue $DataElement.Value
                    }
                }                
            }
            Else {  # Otherwise, pull all metrics
                ForEach ($DataGroup in $JSON.groups) {
                    ForEach ($DataElement in $DataGroup.data | Where-Object {$_.AlertName -ne 'SessionType'}) {
                        $SessionDetails | Add-Member -NotePropertyName $DataElement.AlertName -NotePropertyValue $DataElement.Value
                    }
                }
            }
            Return $SessionDetails
        }
        Catch {
            Write-Error 'Session details not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionAlerts {
    <#
        .SYNOPSIS
        Returns alerts for a given session
         
        .DESCRIPTION
        Returns alerts for a given session. This is used to populate the SESSION ALERTS portion of the session OVERVIEW screen.
        UI_ELEMENT
                 
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionAlerts 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS
        Returns alert information for a specific Teams session
         
        .EXAMPLE
        Get-NectarSession -Platform TEAMS -Users tferguson@contoso.com -SessionTypes PEER2PEER -TimePeriod LAST_DAY | Get-NectarSessionAlerts -Platform TEAMS
        Returns session alerts for all Teams peer-to-peer calls for TFerguson for the last day.
 
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnsa")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/session/$SessionID/alerts?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            $UserList = 'Caller','Callee'
            
            ForEach ($User in $UserList) {    
                ForEach ($Alert in $JSON.$User.alerts.PsObject.Properties) {
                    $AlertDetails = [pscustomobject][ordered]@{
                        SessionID = $SessionID
                        User = $User
                        Parameter = $Alert.Name
                        Value = $Alert.Value.Value
                        AlertLevel = $Alert.Value.AlertLevel
                    }
                    $AlertDetails
                }
            }
        }
        Catch {
            Write-Error 'Session alerts not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionDiagnostics {
    <#
        .SYNOPSIS
        Returns call diagnostics for a given session
         
        .DESCRIPTION
        Returns call diagnostics for a given session. This is used to populate the session DIAGNOSTICS screen for a given session.
        UI_ELEMENT
         
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionDetails 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform SKYPE
        Returns diagnostic information for a specific Skype for Business session
         
        .EXAMPLE
        Get-NectarSession -Platform SKYPE -Users tferguson@contoso.com -SessionTypes PEER2PEER -TimePeriod LAST_DAY | Get-NectarSessionDetails -Platform SKYPE
        Returns detailed information for all Skype for Business peer-to-peer calls for TFerguson for the last day.
 
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnsd")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/session/$SessionID/diagnostic?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            $JSON.elements | Add-Member -NotePropertyName 'sessionId' -NotePropertyValue $SessionID
            Return $JSON.elements
        }
        Catch {
            Write-Error 'Session diagnostics not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarConference {
    <#
        .SYNOPSIS
        Returns a list of conferences that match a given conference ID
         
        .DESCRIPTION
        Returns a list of conferences that match a given conference ID
         
        .PARAMETER SearchQuery
        The conference ID or Teams Join link of the conference to search for
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10
 
        .EXAMPLE
        Get-NectarConference -SearchQuery 'https://teams.microsoft.com/l/meetup-join/19%3ameeting_OWM1MTExZDMtYTBlMS00NzM1LWI0YjEtYjE'
        Returns conference summary information for a Teams conference where the Join URL contains the above text
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999)]
        [int]$ResultSize = 10
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()

            $Params = @{
                'q'         = $SearchQuery
                'tenant'    = $TenantName
                'pageSize'    = $ResultSize
            }

            If ($Platform) { $Params.Add('platform', $Platform) }

            $URI = "https://$Global:NectarCloud/dapi/conference/search"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params

            Return $JSON.elements
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}






Function Get-NectarConferenceSummary {
    <#
        .SYNOPSIS
        Returns session summaries for a given conference
         
        .DESCRIPTION
        Returns session summaries for a given conference. This is used to populate the CONFERENCE section of the details of a specific conference.
        UI_ELEMENT
         
        .PARAMETER ConferenceID
        The conference ID of the selected conference
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarConferenceSummary 2021-05-06T13:30:34.795296_*_*_*_*_*_*_173374c1-a15a-47dd-b11c-d32ab5442774_*_*
        Returns conference summary information for a specific conference
         
        .EXAMPLE
        Get-NectarSession -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes CONFERENCE -Platforms TEAMS | Get-NectarConferenceSummary -Platform TEAMS
        Returns the conference summary information for all Teams conferences participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("gncs")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$ConferenceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/conference/$ConferenceID/?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            $ConferenceSummary = [pscustomobject][ordered]@{
                ConferenceID = $ConferenceID
                StartTime = $JSON.conference.startTime
                EndTime = $JSON.conference.endTime
                Duration = $JSON.duration
                AverageMOS = $JSON.avgMos
                Participants = $JSON.participants
                TotalSessions = $JSON.sessions.total
                GoodSessions = $JSON.sessions.good
                PoorSessions = $JSON.sessions.poor
                UnknownSessions = $JSON.sessions.unknown
            }

            Return $ConferenceSummary
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarConferenceTimeline {
    <#
        .SYNOPSIS
        Returns session timeline details for a given conference
         
        .DESCRIPTION
        Returns session timeline details for a given conference. This is used to build the Gantt chart view of a specific conference.
        UI_ELEMENT
         
        .PARAMETER ConferenceID
        The conference ID of the selected conference
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarConferenceTimeline 2021-05-06T13:30:34.795296_*_*_*_*_*_*_173374c1-a15a-47dd-b11c-d32ab5442774_*_* -Platform TEAMS
        Returns conference summary information for a specific conference
         
        .EXAMPLE
        Get-NectarSession -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes CONFERENCE -Platforms TEAMS | Get-NectarConferenceTimeline -Platform TEAMS
        Returns the conference timeline information for all Teams conferences participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnct")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$ConferenceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/conference/$ConferenceID/timeline?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            $JSON | Add-Member -NotePropertyName 'conferenceId' -NotePropertyValue $ConferenceID
            $JSON | Add-Member -TypeName 'Nectar.Conference.Timeline'
            Return $JSON
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarConferenceParticipants {
    <#
        .SYNOPSIS
        Returns session participant details for a given conference
         
        .DESCRIPTION
        Returns session participant details for a given conference. This is used to build the PARTICIPANTS section of a specific conference.
        UI_ELEMENT
                 
        .PARAMETER ConferenceID
        The conference ID of the selected conference
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarConferenceParticipants 2021-05-06T13:30:34.795296_*_*_*_*_*_*_173374c1-a15a-47dd-b11c-d32ab5442774_*_* -Platform TEAMS
        Returns conference participant information for a specific conference
         
        .EXAMPLE
        Get-NectarSession -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes CONFERENCE -Platforms TEAMS | Get-NectarConferenceParticipants -Platform TEAMS
        Returns the conference participant information for all Teams conferences participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gncp")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$ConferenceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $URI = "https://$Global:NectarCloud/dapi/conference/$ConferenceID/participants?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            $JSON | Add-Member -NotePropertyName 'conferenceId' -NotePropertyValue $ConferenceID
            $JSON | Add-Member -TypeName 'Nectar.Conference.Participants'
            Return $JSON
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionMultiTimeline {
    <#
        .SYNOPSIS
        Returns multimedia session timeline details for a given P2P multimedia session
         
        .DESCRIPTION
        Returns multimedia session (an audio/video P2P session or an audio/appsharing P2P session) timeline details for a given session
        Used to build the Gantt chart view for a given P2P multimedia session on the Session Details screen.
        UI_ELEMENT
                 
        .PARAMETER SessionID
        The sessionID of a multimedia P2P session
         
        .PARAMETER Platform
        The platform where the P2P session took place
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarSessionMultiTimeline 2021-05-05T19:17:14.324027_1_1_*_*_*_6_*_6efc12345-4229-4c11-9001-9a00667a761e9_6efc8348-4809-4caf-9141-9afa87a761e9_* -Platform TEAMS
        Returns multimedia session timeline information for a specific multimedia P2P session
         
        .EXAMPLE
        Get-NectarSession -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes PEER2PEER_MULTIMEDIA -Platforms TEAMS | Get-NectarSessionMultiTimeline -Platform TEAMS
        Returns the multimedia session timeline information for all Teams P2P multimedia sessions participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.3
    #>

    
    [Alias("gnsmt")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/session/$SessionID/multi/timeline?platform=$Platform&tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            #$JSON | Add-Member -NotePropertyName 'sessionId' -NotePropertyValue $SessionID
            Return $JSON
        }
        Catch {
            Write-Error 'Multimedia session not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarQualitySummary {
    <#
        .SYNOPSIS
        Returns summary quality information for Avaya/Cisco quality records
 
        .DESCRIPTION
        Returns summary quality information for Avaya/Cisco quality records. Used to build the QUALITY section of the CALL DETAILS Quality Details screen.
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER CallerPlatforms
        Show sessions where the selected platform was used by the caller. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
         
        .PARAMETER CalleePlatforms
        Show sessions where the selected platform was used by the callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarQualitySummary -TimePeriod LAST_DAY
        Returns the quality summary for the past day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnmqs")]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match '^\d\d(.\d+)?\-(\d\d|100)(.\d+)?$') {
                $True
            } 
            Else {
                Throw 'NectarScore must be in the format aa-xx (ie. 80-90) or aa.bb-xx.yy (ie 77.32-80.99), where aa is less than xx and xx cannot be greater than 100.'
            }
        })]
        [string]$NectarScore,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("SiteName")]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CISCO','CISCO_CMS','CISCO_VKM','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'QUALITY_DETAILS',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $PSBoundParameters.Add('ShowQualityDetails',$TRUE)
    
        Write-Verbose $PsBoundParameters
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters
        
        $QualitySummaryURI = "https://$Global:NectarCloud/dapi/quality/summary"
        $QualitySummaryDetailsURI = "https://$Global:NectarCloud/dapi/quality/summary/data"
        
        Write-Verbose $QualitySummaryDetailsURI
        
        If ($TenantName) { $Params = @{'tenant' = $TenantName} }

        Try {
            $QS_JSON = Invoke-RestMethod -Method GET -uri $QualitySummaryURI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            $QSD_JSON = Invoke-RestMethod -Method GET -uri $QualitySummaryDetailsURI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            
            $QualitySummary = [pscustomobject][ordered]@{}
            
            $QualitySummary | Add-Member -NotePropertyName 'TOTAL_GOOD' -NotePropertyValue $QS_JSON.Good
            $QualitySummary | Add-Member -NotePropertyName 'TOTAL_POOR' -NotePropertyValue $QS_JSON.Poor
            $QualitySummary | Add-Member -NotePropertyName 'TOTAL_UNKNOWN' -NotePropertyValue $QS_JSON.Unknown
            
            ForEach ($QualityMetric in $QSD_JSON.QualityMetrics) {
                $QualitySummary | Add-Member -NotePropertyName $QualityMetric.name -NotePropertyValue $QualityMetric.value
                $TrendMetricName = $QualityMetric.name + '_TREND'
                $QualitySummary | Add-Member -NotePropertyName $TrendMetricName -NotePropertyValue $QualityMetric.trends
            }

            If ($TenantName) { $QualitySummary | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }    
        
            $QualitySummary | Add-Member -TypeName 'Nectar.ModalityQuality'
            Return $QualitySummary
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarModalityQualitySummary {
    <#
        .SYNOPSIS
        Returns summary quality information for different modalities over a given timeperiod.
 
        .DESCRIPTION
        Returns summary quality information for audio, video and appsharing modalities. Used to build the QUALITY section of the CALL DETAILS screen.
        UI_ELEMENT
         
        .PARAMETER Modality
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APPSHARING'
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER NectarScore
        Show sessions that match the given range of NectarScores. Use format aa-xx or aa.bb-xx.yy, where aa is less than xx, and xx is less than 100. Eg. 70-90 or 95.00-95.59
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER ExtConnectionTypes
        Show sessions that match the caller or callee external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
 
        .PARAMETER CallerExtConnectionTypes
        Show sessions that match the caller's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER CalleeExtConnectionTypes
        Show sessions that match the callee's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER Insights
        Show sessions that match one or more given insights. Choose from NORMAL_SESSION, VOICE_MAIL, HIGH_JITTER, HIGH_PACKET_LOSS, HIGH_ROUNDTRIP_DELAY
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarModalityQualitySummary -Modality AUDIO -TimePeriod LAST_DAY
        Returns the modality quality summary for the past day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnmqs")]
    Param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('AUDIO','VIDEO','APPSHARE', IgnoreCase=$True)]
        [string]$Modality,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match '^\d\d(.\d+)?\-(\d\d|100)(.\d+)?$') {
                $True
            } 
            Else {
                Throw 'NectarScore must be in the format aa-xx (ie. 80-90) or aa.bb-xx.yy (ie 77.32-80.99), where aa is less than xx and xx cannot be greater than 100.'
            }
        })]
        [string]$NectarScore,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("SiteName")]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$ExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CallerExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CalleeExtConnectionTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY', IgnoreCase=$False)]
        [string[]]$Insights,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'CALL_DETAILS_HISTORIC',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $PSBoundParameters.Remove('Modality') | Out-Null
        $FilterSession = Set-NectarFilterParams @PsBoundParameters

        $Modality = $Modality.ToLower()    
        $URI = "https://$Global:NectarCloud/dapi/quality/session/$Modality"
        
        If ($TenantName) { $Params = @{'Tenant' = $TenantName} }

        Try {
            Write-Verbose $PsBoundParameters
            $QualitySummaryList = @()
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            
            ForEach ($SessionType in $JSON) {
                If ($SessionType.QualityType -in $SessionTypes -or !$SessionTypes) {
                    $QualitySummary = [pscustomobject][ordered]@{}
                    
                    If ($Modality -ne 'appshare') { $QualitySummary | Add-Member -NotePropertyName 'SessionType' -NotePropertyValue $SessionType.QualityType }
                    
                    ForEach ($QualityMetric in $SessionType.QualityMetrics) {
                        $QualitySummary | Add-Member -NotePropertyName $QualityMetric.name -NotePropertyValue $QualityMetric.value
                        $TrendMetricName = $QualityMetric.name + '_TREND'
                        $QualitySummary | Add-Member -NotePropertyName $TrendMetricName -NotePropertyValue $QualityMetric.trends
                    }

                    If ($TenantName) { $QualitySummary | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }    
                    If ($PSItem.SiteName) { $QualitySummary | Add-Member -NotePropertyName 'SiteName' -NotePropertyValue $PSItem.SiteName }
                
                    $QualitySummary | Add-Member -TypeName 'Nectar.ModalityQuality'
                    $QualitySummaryList += $QualitySummary
                }
            }
            Return $QualitySummaryList            
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarLocationQualitySummary {
    <#
        .SYNOPSIS
        Returns summary information for different locations over a given timeperiod.
 
        .DESCRIPTION
        Returns summary information for different locations over a given timeperiod. This is used to build the SUMMARY page in Nectar DXP.
        UI_ELEMENT
                 
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER NectarScore
        Show sessions that match the given range of NectarScores. Use format aa-xx or aa.bb-xx.yy, where aa is less than xx, and xx is less than 100. Eg. 70-90 or 95.00-95.59
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
                 
        .PARAMETER ExtConnectionTypes
        Show sessions that match the caller or callee external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
 
        .PARAMETER CallerExtConnectionTypes
        Show sessions that match the caller's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER CalleeExtConnectionTypes
        Show sessions that match the callee's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
 
        .PARAMETER Insights
        Show sessions that match one or more given insights. Choose from NORMAL_SESSION, VOICE_MAIL, HIGH_JITTER, HIGH_PACKET_LOSS, HIGH_ROUNDTRIP_DELAY
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarLocationQualitySummary -TimePeriod LAST_DAY
        Returns the quality summary for each location for the last day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnlqs")]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match '^\d\d(.\d+)?\-(\d\d|100)(.\d+)?$') {
                $True
            } 
            Else {
                Throw 'NectarScore must be in the format aa-xx (ie. 80-90) or aa.bb-xx.yy (ie 77.32-80.99), where aa is less than xx and xx cannot be greater than 100.'
            }
        })]
        [string]$NectarScore,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY', IgnoreCase=$False)]
        [string[]]$Insights,
        [switch]$ShowQualityDetails,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'CALL_DETAILS_HISTORIC',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters

        If ($TenantName) { $Params = @{'Tenant' = $TenantName} }
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -uri "https://$Global:NectarCloud/dapi/quality/locations" -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            $JSONGoodPoor = Invoke-RestMethod -Method GET -uri "https://$Global:NectarCloud/dapi/quality/map" -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession

            ForEach ($Location in $JSON) {
                $LocGoodPoor = $JSONGoodPoor | Where-Object {$_.name -eq $Location.Name}
                $QualitySummary = [pscustomobject][ordered]@{
                    'LocationName' = $Location.Name
                    'Sessions' = $Location.Data.Sessions
                    'CurrentSessions' = $Location.Data.currentSession
                    'GoodSessions' = $LocGoodPoor.Sessions.Good
                    'PoorSessions' = $LocGoodPoor.Sessions.Poor
                    'UnknownSessions' = $LocGoodPoor.Sessions.Unknown
                    'AvgMOS' = $Location.Data.avgMos
                    'MOSTrend' = $Location.Data.mos
                }
                
                If ($TenantName) { $QualitySummary | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                
                $QualitySummary | Add-Member -TypeName 'Nectar.LocationQuality'
                
                If ($Location.Name -ne 'All') { $QualitySummary }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





Function Get-NectarAgentVersion {
    <#
        .SYNOPSIS
        Returns a list of agent versions
         
        .DESCRIPTION
        Returns a list of agent versions
 
        .PARAMETER SearchQuery
        Allows filtering of the final results
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarAgentVersion -SearchQuery Sonus
        Returns all the agent versions that contain 'Sonus' in the name
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gns")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/agent/version?tenant=$TenantName"
        
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            If ($SearchQuery) {
                $JSON = $JSON | Where-Object {$_ -like "*$SearchQuery*"}
            }
            
            If (!$JSON) {
                Write-Error "Agent version list could not be found."
            }
            Else {
                Return $JSON
            }
        }
        Catch {
            Write-Error "Unable to get agent version list."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}








#################################################################################################################################################
# #
# Room/Device Functions #
# #
#################################################################################################################################################

Function Get-NectarRoom {
    <#
        .SYNOPSIS
        Return a list of Nectar monitored rooms
         
        .DESCRIPTION
        Return a list of Nectar monitored rooms
 
        .PARAMETER SearchQuery
        Limit the results to the specified search query. Will match against all fields.
         
        .PARAMETER OrderByField
        Order the resultset by the specified field. Choose from id, type, lastTime, displayName, deviceName, description, eventId, time, delay, source, location, sourceId
 
        .PARAMETER OrderDirection
        Sort order. Pick from ASC or DESC
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
 
        .EXAMPLE
        Get-NectarRoom
        Returns a list of all rooms
         
        .EXAMPLE
        Get-NectarRoom -OrderByField HealthStatus -OrderDirection Descending
        Returns a list of rooms sorted by Health Status
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('HealthStatus', 'Name', 'BusinessUnits', 'NetworkName', 'SiteName', 'City', 'Region', 'Country', 'State', 'DeviceCount', IgnoreCase=$False)]
        [string]$OrderByField = 'Name',
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
            
            If ($TenantName) { 
                $NULL = $PSBoundParameters.Add('Tenant',$TenantName)
                $NULL = $PSBoundParameters.Remove('TenantName') 
            }
            
            # Convert to camelCase if used
            If ($OrderByField) {
                $PSBoundParameters.OrderByField = $OrderByField -Replace "^$($OrderByField[0])", "$($OrderByField[0].ToString().ToLower())"
            }

            $PSBoundParameters.PageSize = $PageSize

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/rooms"
            
            Write-Verbose $URI
            If ($PSBoundParameters['Verbose']) {
                ForEach ($boundParam in $PSBoundParameters.GetEnumerator()) {
                    '{0} = {1}' -f $boundParam.Key, $boundParam.Value
                }
            }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $PSBoundParameters
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            Write-Verbose "Page size: $PageSize"
            Write-Verbose "Total pages: $TotalPages"

            If ($TotalPages -gt 1) {
                $PageNum = 2
                $PSBoundParameters.Add('pageNumber', 2)
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PSBoundParameters.pageNumber = $PageNum
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $PSBoundParameters

                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "Error pulling data."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarRoom {
    <#
        .SYNOPSIS
        Update an existing Nectar monitored room
         
        .DESCRIPTION
        Update an existing Nectar monitored room. Can change the name and the associated business units
 
        .PARAMETER ID
        The ID of the room to update. Can be obtained via Get-NectarRoom and will accept pipeline input
 
        .PARAMETER Name
        The new name of the room
         
        .PARAMETER LocationID
        The ID of a Nectar location to assign to the room. Location IDs can be obtained via Get-NectarLocation
 
        .PARAMETER BusinessUnitID
        One or more business unit IDs to assign to the room. Business Unit IDs can be obtained via Get-NectarBusinessUnit
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Set-NectarRoom -RoomID 1 -RoomName 'Secondary Boardroom'
        Changes the room name with ID 1 to 'Secondary Boardroom'
 
        .EXAMPLE
        Get-NectarRoom -SearchQuery 'Main Boardroom' | Set-NectarRoom -RoomName 'Secondary Boardroom'
        Changes the room name from 'Main Boardroom' to 'Secondary Boardroom'
 
        .EXAMPLE
        Get-NectarRoom -SearchQuery Dallas | Set-NectarRoom -BusinessUnitID 3
        Changes all rooms that have Dallas in any field to BusinessUnitID 3
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('businessUnits')]
        [string[]]$BusinessUnitID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Body = @{
                roomName = $Name
            }

            If ($BusinessUnitID) {
                # If using BusinessUnitIDs from pipeline, it contains text. We have to strip it down to just the number for updating
                If ($BusinessUnitID[0] -like '*:*') {
                    ForEach ($i in $BusinessUnitID) { 
                        $BusinessUnitID[$BusinessUnitID.IndexOf($i)] = $($i.Substring(0, $i.IndexOf(':')))
                    }
                }
                $Body.Add('bUnitIds', $BusinessUnitID)
            }

            $JSONBody = $Body | ConvertTo-Json

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/room/$ID/?tenant=$TenantName"
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Return $JSON
        }
        Catch {
            Write-Error "Error updating room."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarRoom {
    <#
        .SYNOPSIS
        Create a new Nectar monitored room
         
        .DESCRIPTION
        Create a new Nectar monitored room
 
        .PARAMETER RoomName
        The name of the room to create
         
        .PARAMETER LocationID
        The ID of a Nectar location to assign to the room. Location IDs can be obtained via Get-NectarLocation
 
        .PARAMETER BusinessUnitID
        One or more business unit IDs to assign to the room. Business Unit IDs can be obtained via Get-NectarBusinessUnit
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarRoom 'Main Boardroom'
        Creates a room called 'Main Boardroom'
 
        .EXAMPLE
        New-NectarRoom 'Main Boardroom' -BusinessUnitID 1,3
        Creates a room called 'Main Boardroom' and assign business units 1 and 3 to the room
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$RoomName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int[]]$BusinessUnitID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Body = @{
                roomName = $RoomName
            }

            If ($BusinessUnitID) {
                $Body.Add('bUnitIds', $BusinessUnitID)
            }

            $JSONBody = $Body | ConvertTo-Json

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/room?tenant=$TenantName"
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Return $JSON
        }
        Catch {
            Write-Error "Error creating room."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarRoom {
    <#
        .SYNOPSIS
        Deletes a Nectar monitored room
         
        .DESCRIPTION
        Deletes a new Nectar monitored room
 
        .PARAMETER ID
        The numeric ID of the room to delete. Accepts pipeline input
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarRoom -SearchQuery Boardroom | Remove-NectarRoom
        Removes all rooms with the name 'Boardroom' anywhere in the name
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$ID,            
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/room/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error deleting room with ID $ID."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Connect-NectarRoomDevice {
    <#
        .SYNOPSIS
        Connects one or more devices to a Nectar Room
         
        .DESCRIPTION
        Connects one or more devices to a Nectar Room
 
        .PARAMETER RoomID
        The numeric ID of the room to connect the device to.
 
        .PARAMETER DeviceUnitedId
        The numeric ID of the device to connect to the room. Accepts pipeline input from Get-NectarDevice
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarDevice -SearchQuery Dallas | Connect-NectarRoomDevice -RoomName DallasBoardroom
        Connects all devices with 'Dallas' in the name to the DallasBoardroom
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$RoomName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$DeviceUnitedId,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud

        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        $RoomID = (Get-NectarRoom -SearchQuery $RoomName).id

        If ($RoomID.Count -gt 1) {
            Throw "Room name must be unique enough to return a single value. Your query returned $($RoomID.Count) results for search '$RoomName'."
        }
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/dapi/room-and-device/device/connect?device_united_id=$DeviceUnitedID&room_id=$RoomID&tenant=$TenantName"
            Write-Verbose $URI
            
            $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error adding device with ID $DeviceUnitedID to $RoomName. Is this device already associated with a room?"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Disconnect-NectarRoomDevice {
    <#
        .SYNOPSIS
        Disonnects one or more devices from a Nectar Room
         
        .DESCRIPTION
        Disonnects one or more devices from a Nectar Room
 
        .PARAMETER DeviceUnitedId
        The numeric ID of the device to disconnect from a room. Accepts pipeline input from Get-NectarDevice
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarDevice -SearchQuery Dallas | Disconnect-NectarRoomDevice
        Disconnects all devices with 'Dallas' in any field from any associated room
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$DeviceUnitedId,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud

        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/dapi/room-and-device/device/disconnect/$DeviceUnitedID/?tenant=$TenantName"
            Write-Verbose $URI
            
            $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error disconnecting device with ID $DeviceUnitedID. The device may already be disconnected from a room."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarDevice {
    <#
        .SYNOPSIS
        Return a list of Nectar monitored devices
         
        .DESCRIPTION
        Return a list of Nectar monitored devices
 
        .PARAMETER SearchQuery
        Limit the results to the specified search query. Will match against all fields.
         
        .PARAMETER OrderByField
        Order the resultset by the specified field. Choose from id, type, lastTime, displayName, deviceName, description, eventId, time, delay, source, location, sourceId
 
        .PARAMETER OrderDirection
        Sort order. Pick from ASC or DESC
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
 
        .EXAMPLE
        Get-NectarDevice
        Returns a list of all devices
         
        .EXAMPLE
        Get-NectarDevice -SearchQuery Dallas
        Returns a list of devices that has 'Dallas' in any field
 
        .EXAMPLE
        Get-NectarDevice -OrderByField HealthStatus -OrderDirection Descending
        Returns a list of devices sorted by Health Status
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('HealthStatus', 'DeviceDisplayName', 'DeviceType', 'Vendor', 'Model', 'Platform', 'ActivityState', 'RoomName', 'BusinessUnits', 'NetworkName', IgnoreCase=$False)]
        [string]$OrderByField,
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
            
            If ($TenantName) { 
                $NULL = $PSBoundParameters.Add('Tenant',$TenantName)
                $NULL = $PSBoundParameters.Remove('TenantName') 
            }
            
            # Convert to camelCase if used
            If ($OrderByField) {
                $PSBoundParameters.OrderByField = $OrderByField -Replace "^$($OrderByField[0])", "$($OrderByField[0].ToString().ToLower())"
            }

            $PSBoundParameters.PageSize = $PageSize
            $PSBoundParameters.Add('pageNumber', 1)

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/devices"
            
            Write-Verbose $URI
            If ($PSBoundParameters['Verbose']) {
                ForEach ($boundParam in $PSBoundParameters.GetEnumerator()) {
                    '{0} = {1}' -f $boundParam.Key, $boundParam.Value
                }
            }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $PSBoundParameters
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            Write-Verbose "Page size: $PageSize"
            Write-Verbose "Total pages: $TotalPages"

            If ($TotalPages -gt 1) {
                $PageNum = 2
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PSBoundParameters.PageNumber = $PageNum
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $PSBoundParameters
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "Error pulling data."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarBusinessUnit {
    <#
        .SYNOPSIS
        Return a list of Nectar business units
         
        .DESCRIPTION
        Return a list of Nectar business units
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 10000
 
        .EXAMPLE
        Get-NectarBusinessUnit
        Returns a list of business units
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 10000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
            
            If ($TenantName) { 
                $NULL = $PSBoundParameters.Add('Tenant',$TenantName)
                $NULL = $PSBoundParameters.Remove('TenantName') 
            }
            
            $PSBoundParameters.PageSize = $PageSize
            $PSBoundParameters.Add('pageNumber', 1)

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/business-unit?pageSize=$PageSize&tenant=$TenantName"
            
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
        If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON
        }
        Catch {
            Write-Error "Error pulling data."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function New-NectarBusinessUnit {
    <#
        .SYNOPSIS
        Create a new Nectar business unit to be assigned to a room
         
        .DESCRIPTION
        Create a new Nectar business unit to be assigned to a room
 
        .PARAMETER Name
        The name of the business unit to create
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarBusinessUnit 'Sales'
        Creates a business unit called 'Sales'
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Name,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Body = @{
                unitName = $Name
            }

            $JSONBody = $Body | ConvertTo-Json

            $URI = "https://$Global:NectarCloud/dapi/room-and-device/business-unit?tenant=$TenantName"
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Return $JSON
        }
        Catch {
            Write-Error "Error creating business unit. Does the business unit already exist?"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




#################################################################################################################################################
# #
# Report/Analytics Functions #
# #
#################################################################################################################################################

Function Get-NectarReport {
    <#
        .SYNOPSIS
        Gets basic information about all available reports
         
        .DESCRIPTION
        Gets basic information about all available reports
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReport
        Returns information about all reports
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(Mandatory=$False)]
        [ValidateSet('name', 'status', 'createdDate', 'lastRun', 'scheduledRun', IgnoreCase=$True)]
        [string]$OrderByField = 'name',
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Params = @{
                'pageNumber'        = 1
                'pageSize'             = $PageSize
                'orderByField'        = $OrderByField
                'orderDirection'    = $OrderDirection
            }

            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }

            If ($ResultSize) {
                $Params.pageSize = $ResultSize
            } 

            $URI = "https://$Global:NectarCloud/rapi/client/report/config/list?tenant=$TenantName"
            Write-Verbose $URI
            Write-Verbose "** Body Parameters **"
            ForEach($k in $Params.Keys){Write-Verbose "$k`: $($Params[$k])"}

            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params

            $TotalPages = $JSON.totalPages
            
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $Params.pageNumber = $PageNum
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "Error getting report info"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function New-NectarReport {
    <#
        .SYNOPSIS
        Creates a new report
         
        .DESCRIPTION
        Creates a new report using previously defined widgets from New-NectarReportWidgetConfig
 
        .PARAMETER Name
        The name of the report
 
        .PARAMETER Description
        A description to go with the report
 
        .PARAMETER Type
        The report type (or scope). Select from User or Tenant. User reports are visible only to the creator.
        Tenant reports are visible to all users.
         
        .PARAMETER WidgetList
        An arraylist containing the definitions for all the widgets to put in the report. Widgets are defined
        using New-NectarReportWidget
 
        .PARAMETER ReportTimeZone
        The timezone used for report display
 
        .PARAMETER ScheduleMode
        How often to run the report when on a schedule
 
        .PARAMETER SchedDayOfMonth
        If report is run on a schedule, what day of the month to run the report. Only works with ScheduleMode MONTHLY
 
        .PARAMETER SchedWeekdays
        The days of the week to run the report. Only works with ScheduleMode WEEKLY
 
        .PARAMETER SchedStartDate
        When the report schedule should start
 
        .PARAMETER SchedEndDate
        When the report schedule should end
 
        .PARAMETER SchedStartTime
        The time of day that the report should start processing
 
        .PARAMETER SchedTimeZone
        The timezone the schedule should run on
 
        .PARAMETER SchedNotifyMethod
        How to notify users when the report is ready
 
        .PARAMETER SchedEmail
        One or more email addresses, separated by commas to notify upon report completion.
        Only usable when SchedNotifyMethod is EMAIL
 
        .PARAMETER SchedEmailSubject
        The subject line of the email.
        Only usable when SchedNotifyMethod is EMAIL
 
        .PARAMETER SchedEmailMessage
        The content of the email message.
        Only usable when SchedNotifyMethod is EMAIL
 
        .PARAMETER SchedRepeatEveryNumDays
        How often to repeat a daily schedule
 
        .PARAMETER SchedRepeatEveryNumWeeks
        How often to repeat a weekly schedule
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarReport -Name 'Test Report -Description 'This is a test report' -WidgetList $WidgetList -Type User
        Creates a user-level report using a pre-defined set of widgets stored in the $WidgetList variable
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('User','Tenant', IgnoreCase=$True)]
        [string]$Type = 'User',
        [Parameter(Mandatory=$True)]
        [System.Collections.ArrayList]$WidgetList,
        [Parameter(Mandatory=$False)]
        [string]$ReportTimeZone = "Etc/GMT$((Get-TimeZone).BaseUTCOffset.hours)",
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('ONCE','DAILY','WEEKLY','MONTHLY', IgnoreCase=$False)]
        [string]$ScheduleMode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(1,31)]
        [nullable[int]]$SchedDayOfMonth,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY','SUNDAY', IgnoreCase=$False)]
        [string[]]$SchedWeekdays,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedStartDate = ((Get-Date).AddDays(1)).ToShortDateString(),
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedEndDate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [datetime]$SchedStartTime = '00:00',
        [Parameter(Mandatory=$False)]
        [string]$SchedTimeZone = "Etc/GMT$((Get-TimeZone).BaseUTCOffset.hours)",
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('NONE','EMAIL', IgnoreCase=$False)]
        [string]$SchedNotifyMethod = 'NONE',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$SchedEmail,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedEmailSubject = $Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedEmailMessage,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [AllowNull()]
        [nullable[int]]$SchedRepeatEveryNumDays,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [AllowNull()]
        [nullable[int]]$SchedRepeatEveryNumWeeks,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        Switch ($Type) {
            'User'            { $ReportTypeID = 1 }
            'Tenant'        { $ReportTypeID = 2 }
        }

        If ($ScheduleMode) {
            If ($SchedNotifyMethod -eq 'EMAIL') {
                $EmailParams = @{
                    'recipients'         = $SchedEmail
                    'subject'            = $SchedEmailSubject  #If (!$SchedEmailSubject) { $Name } Else { $SchedEmailSubject }
                    'message'            = $SchedEmailMessage
                }
            }

            $NotificationParams = @{
                'method'                = $SchedNotifyMethod
                'email'                    = $EmailParams
            }

            $ScheduleParams = @{
                'scheduleMode'            = $ScheduleMode
                'dayOfMonth'             = $SchedDayOfMonth
                'startDate'             = $SchedStartDate
                'startTime'                = Get-Date $SchedStartTime -UFormat %R
                'endDate'                 = If (!$SchedEndDate) { $Null } Else { $SchedEndDate }
                'repeatEveryNumDays'    = $SchedRepeatEveryNumDays
                'repeatEveryNumWeeks'    = $SchedRepeatEveryNumWeeks
                'weekDayOfMonth'        = $SchedWeekDayOfMonth
                'weekDays'                = $SchedWeekDays
                'weekOfMonth'            = $SchedWeekOfMonth
                'timeZone'                = $SchedTimeZone
                'notification'            = $NotificationParams
            }
        }
        
        $Params = @{
            'name'                = $Name
            'description'        = $Description
            'schedule'            = $ScheduleParams
            'timeZone'            = $ReportTimeZone
            'visibilityType'    = $ReportTypeID
            'widgets'            = $WidgetList
        }

        $JSONParams = $Params | ConvertTo-Json -Depth 7

        $URI = "https://$Global:NectarCloud/rapi/client/report/config?tenant=$TenantName"
        Write-Verbose $URI
        Write-Verbose $JSONParams

        Try {
            $Null = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONParams -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Error creating report"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function New-NectarReportWidget {
    <#
        .SYNOPSIS
        Creates a report widget to be added to a report
         
        .DESCRIPTION
        Creates a report widget to be added to a report. One ore more widgets have to be defined before creating a report
 
        .PARAMETER WidgetType
        The type of widget to create
 
        .PARAMETER WidgetDescription
        A description to go with the widget
 
        .PARAMETER WidgetPosition
        The numeric position to place the widget in the report. Starts at 0
 
        .PARAMETER WidgetGroupVarName
        The name of a arraylist variable to add the widget to.
        If the variable does not exist, it will be created. Makes creating reports a bit easier.
 
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER NectarScore
        Show sessions that match the given range of NectarScores. Use format aa-xx or aa.bb-xx.yy, where aa is less than xx, and xx is less than 100. Eg. 70-90 or 95.00-95.59
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER ExtConnectionTypes
        Show sessions that match the caller or callee external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
 
        .PARAMETER CallerExtConnectionTypes
        Show sessions that match the caller's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER CalleeExtConnectionTypes
        Show sessions that match the callee's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS'
 
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER Insights
        Show sessions that match one or more given insights. Choose from NORMAL_SESSION, VOICE_MAIL, HIGH_JITTER, HIGH_PACKET_LOSS, HIGH_ROUNDTRIP_DELAY
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarReportWidget -WidgetType 'Call Details, Sessions' -WidgetDescription "Conference Session Summary for New York" -WidgetPosition 0 -WidgetGroupVarName WidgetList -TimePeriod LAST_MONTH -Modalities AUDIO -Platforms TEAMS -SessionTypes CONFERENCE_SESSION -ExtCities 'New York' -ExtCountries 'US'
        Creates a Call Details widget to be used later in New-NectarReport
        .NOTES
        Version 1.0
    #>

    Param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('Call Details, Sessions','Call Details, Quality Audio','Call Details, Quality Video','Call Details, Quality App Sharing','Call Details, Session List','Quality Details, Sessions','Quality Details, Quality Summary','Quality Details, Session List', IgnoreCase=$True)]
        [string]$WidgetType,
        [Parameter(Mandatory=$False)]
        [string]$WidgetDescription,
        [Parameter(Mandatory=$False)]
        [int]$WidgetPosition,
        [Parameter(Mandatory=$False)]
        [string]$WidgetGroupVarName,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('YESTERDAY','LAST_WEEK','LAST_MONTH','LAST_03_MONTHS','LAST_12_MONTHS','MONTH_TO_DATE','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_MONTH',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [string]$NectarScore,    
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,608)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$ExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CallerExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CalleeExtConnectionTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('EXTERNAL','EXTERNAL_FEDERATED','EXTERNAL_INTERNAL','FEDERATED','FEDERATED_EXTERNAL','FEDERATED_INTERNAL','INTERNAL','INTERNAL_EXTERNAL','INTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY', IgnoreCase=$False)]
        [string[]]$Insights,
        [Parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN','INCOMPLETE','INCOMPLETE', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [switch]$ShowQualityDetails    
    )    
    Begin {
        # Check for the existance of the global widget group variable. Create it if it doesn't exist.
        If ($WidgetGroupVarName) {
            If (Get-Variable $WidgetGroupVarName -Scope Global -ErrorAction SilentlyContinue) {
            }
            Else {
                [System.Collections.ArrayList]$TempVar = @()
                Set-Variable -Name $WidgetGroupVarName -Value (Get-Variable -Name TempVar -ValueOnly) -Scope Global
                Remove-Variable TempVar
            }
        }
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # Convert to all-caps
        If ($Scope) { $Scope = $Scope.ToUpper() }
        If ($TimePeriod) { $TimePeriod = $TimePeriod.ToUpper() }    
        
        $FilterParams = @{
            'analyticsTimePeriod' = $TimePeriod
        }

        # Map widget names to their corresponding IDs
        Switch ($WidgetType) {
            'Call Details, Sessions'            { $WidgetID = 1 }
            'Call Details, Quality Audio'        { $WidgetID = 5 }
            'Call Details, Quality Video'        { $WidgetID = 6 }
            'Call Details, Quality App Sharing'    { $WidgetID = 7 }
            'Call Details, Session List'        { $WidgetID = 3 }
            'Quality Details, Sessions'            { $WidgetID = 2 }
            'Quality Details, Quality Summary'    { $WidgetID = 8 }
            'Quality Details, Session List'        { $WidgetID = 4 }
        }

    
        # Convert any PowerShell array objects to comma-separated strings to add to the GET querystring
        # Convert multi-string variables into a comma-delimited list and add to the FilterParams array. For variables that need to be in upper-case
        $MultiVarListUpper = 'SessionQualities','SessionScenarios','SessionTypes','Modalities','Platforms','Scenarios',
                            'VPN','CallerVPN','CalleeVPN',
                            'FeedbackRating','Insights','TestTypes','TestResults'
            
        ForEach ($MultiVar in $MultiVarListUpper) {
            If ((Get-Variable $MultiVar).Value) {
                # Make sure all variable names are in camel-case
                $FilterParams.Add($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1),(Get-Variable $MultiVar).Value)
                Write-Verbose "$($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1))`: $((Get-Variable $MultiVar).Value)"
            }
        }
        
        # Convert multi-string variables into a comma-delimited list and add to the FilterParams array. For variables that do not need to be in upper-case
        $MultiVarList = 'Protocols','ResponseCodes','Servers',
                        'Codecs','CallerCodecs','CalleeCodecs',
                        'Devices','CallerDevices','CalleeDevices',
                        'DeviceVersions','CallerDeviceVersions','CalleeDeviceVersions',
                        'Locations','CallerLocations','CalleeLocations',
                        'ExtCities','CallerExtCities','CalleeExtCities',
                        'ExtCountries','CallerExtCountries','CalleeExtCountries',
                        'ExtISPs','CallerExtISPs','CalleeExtISPs',
                        'extConnectionTypes','callerExtConnectionTypes','calleeExtConnectionTypes',
                        'NetworkTypes','CallerNetworkTypes','CalleeNetworkTypes',
                        'ipAddresses','callerIpAddresses','calleeIpAddresses',
                        'endpointTypes','callerEndpointTypes','calleeEndpointTypes'
                        
        ForEach ($MultiVar in $MultiVarList) {
            If ((Get-Variable $MultiVar).Value) {
                $MultiVarValue = (Get-Variable $MultiVar).Value

                # Update name of parameters if necessary
                Switch ($MultiVar) {
                    'Servers' { $MultiVar = 'platformServersOrDataCenters' }
                }

                If ($MultiVar -like '*ISPs') { $MultiVar = $MultiVar.Replace('ISPs', 'Isps') }

                # Make sure all variable names are in camel-case
                $FilterParams.Add($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1),$MultiVarValue)
                Write-Verbose "$($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1))`: $MultiVarValue)"
            }
        }
        
        # Get user IDs and convert into a comma-delimited list and add to the FilterParams array.
        $UserVarList = 'Users','FromUsers','ToUsers'
        
        ForEach ($UserVar in $UserVarList) {
            If ((Get-Variable $UserVar).Value) {
                $UserList = (Get-Variable $UserVar).Value
                
                $UserIDList = @()
                $PlatformUserNames = @()
                ForEach ($User in $UserList) {
                    Write-Verbose "UserSearch: $User"
                    $UserInfo = Get-NectarUser $User -Scope $Scope -TenantName $TenantName -FilterSearch -ResultSize 10000
                    If ($UserInfo.id) { 
                        $UserIDList += $UserInfo.id 
                    }
                    ElseIf ($UserInfo.platformUserName) {
                        $PlatformUserNames += $UserInfo.platformUserName
                    }
                }
                
                If ($UserIDList) {    
                    $UserIDList | ForEach-Object { $UserIDString += ($(if($UserIDString){","}) + $_) } 
                    $FilterParams.Add($UserVar.substring(0,1).ToLower()+$UserVar.substring(1),$UserIDString)
                    Write-Verbose "UserID $UserVar`: $UserIDString"
                    Remove-Variable UserIDString
                }
                
                If ($PlatformUserNames) { 
                    $PlatformUserNames | ForEach-Object { $PlatformUserNameString += ($(if($PlatformUserNameString){","}) + $_) }
                    $FilterParams.Add($UserVar.Replace('Users','PlatformUserNames'),$PlatformUserNameString)
                    Write-Verbose "Platform $UserVar`: $PlatformUserNameString"
                    Remove-Variable PlatformUserNameString
                }
                
                If (!$UserIDList -And !$PlatformUserNames) { Write-Error 'No matching users found'; Return }
            }
        }

        # Do the same for UserIDs
        $UserIDVarList = 'UserIDs','FromUserIDs','ToUserIDs'
        
        ForEach ($UserIDVar in $UserIDVarList) {
            If ((Get-Variable $UserIDVar -ErrorAction SilentlyContinue).Value) {
                $UserIDList = (Get-Variable $UserIDVar).Value
                
                If ($UserIDList) {    
                    $UserIDList | ForEach-Object { $UserIDString += ($(if($UserIDString){","}) + $_) } 
                    $FilterParams.Add($UserIDVar.substring(0,1).ToLower()+$UserIDVar.substring(1).Replace('ID',''),$UserIDString)  # Convert to camelCase
                    Write-Verbose "UserID $($UserIDVar.substring(0,1).ToLower()+$UserIDVar.substring(1).Replace('ID',''))`: $UserIDString"  
                    Remove-Variable UserIDString
                }
            }
        }

        # Get agent versions and convert into a comma-delimited list and add to the FilterParams array.
        $AgentVarList = 'AgentVersions','CallerAgentVersions','CalleeAgentVersions'
                
        ForEach ($AgentVar in $AgentVarList) {
            If ((Get-Variable $AgentVar).Value) {
                $AgentList = (Get-Variable $AgentVar).Value
                
                # Parse through each entry and search for results
                $FinalAgentList = @()
                ForEach ($Agent in $AgentList) {
                    Write-Verbose "AgentSearch: $Agent"
                    $AgentInfo = Get-NectarAgentVersion -SearchQuery $Agent -TenantName $TenantName
                    If ($AgentInfo) { 
                        $FinalAgentList += $AgentInfo
                    }
                }
                
                # Convert to comma-delimited list and add to FilterParams array
                If ($FinalAgentList) {    
                    $FilterParams.Add($AgentVar.Substring(0,1).ToLower()+$AgentVar.Substring(1,$AgentVar.Length-2),$FinalAgentList)   # Convert to camelCase
                    Write-Verbose "Agent $AgentVar.Substring(0,1).ToLower()+$AgentVar.Substring(1,$AgentVar.Length-2)`: $FinalAgentList"
                }
                
                If (!$FinalAgentList) { Write-Error 'No matching agent versions found'; Return }
            }
        }
        
        # Add single parameter variables to the FilterParams array.
        $VarList = 'NectarScore','DurationFrom','DurationTo','ParticipantsMinCount','ParticipantsMaxCount'
        
        ForEach ($Var in $VarList) {
            If ((Get-Variable $Var).Value) {
                $FilterParams.Add($Var.Substring(0,1).ToLower()+$Var.Substring(1),(Get-Variable $Var).Value)   # Convert to camelCase
                Write-Verbose "$Var`: (Get-Variable $Var).Value"
            }
        }

        # Add time-based parameter variables to the FilterParams array. This converts to UNIX timestamp
        $TimeVarList = 'TimePeriodFrom','TimePeriodTo'

        # Set TimePeriodTo to NOW if not explicitly set
        If ($TimePeriodFrom -And !$TimePeriodTo) {
            [String]$TimePeriodTo = Get-Date
        }
        
        ForEach ($TimeVar in $TimeVarList) {
            If ((Get-Variable $TimeVar).Value) {
                [decimal]$TimePeriodUnix = Get-Date -Date (Get-Variable $TimeVar).Value -UFormat %s
                [long]$TimePeriodUnix = $TimePeriodUnix * 1000
                $FilterParams.Add($TimeVar.Replace('TimePeriod','startDate'),$TimePeriodUnix)
                Write-Verbose "$TimeVar`: $TimePeriodUnix"
            }
        }
        
        If ($ConfOrganizers) {
            $ConfOrganizerIDs = ForEach($Organizer in $ConfOrganizers) {
                (Get-NectarUser $Organizer -Scope $Scope -TenantName $TenantName -ErrorAction:Stop).Userid
            }
            $ConfOrganizerIDs | ForEach-Object { $ConfOrganizerIDsStr += ($(if($ConfOrganizerIDsStr){","}) + $_) }
            $FilterParams.Add('organizersOrSpaces',$ConfOrganizerIDsStr)
        }
        
        If ($ShowQualityDetails) {
            $FilterParams.Add('sessionQualitySources','CDS,CDR_CDS')
            $FilterParams.Add('excludeIncompleteRecords','true')
        }

        $SessionQualitySources = 'CDR','CDR_CDS'
        $FilterParams.Add('sessionQualitySources',$SessionQualitySources)
        $FilterParams.Add('scope', $Scope)
        $FilterParams.Add('excludeAvayaPpm', $True)
        
        $WidgetConfig = @{
            description        = $WidgetDescription
            exportFormat    = 'pdf'
            filter            = $FilterParams
            view            = $Null
        }

        # Check for widget position. If not present, use the position from the WidgetGroupVar (if available)
        If (!$WidgetPosition) {
            # If the WidgetGroupVar exists, then set the widget position to be the last numeric value
            If ($WidgetGroupVarName) {
                $WidgetGroupData = (Get-Variable $WidgetGroupVarName).Value
                $WidgetPosition = $WidgetGroupData.Count
            }            
        } 

        $WidgetParams = @{
            id                = $NULL
            name            = $WidgetType
            position        = $WidgetPosition
            widgetId        = $WidgetID
            configuration    = $WidgetConfig
        }
        

        If ($WidgetGroupVarName) {
            (Get-Variable $WidgetGroupVarName).Value += $WidgetParams
        }
        Else {
            Return $WidgetParams
        }
    }
}




Function Remove-NectarReport {
    <#
        .SYNOPSIS
        Removes a report from Nectar DXP
         
        .DESCRIPTION
        Removes a report from Nectar DXP
 
        .PARAMETER ID
        The ID of the report to delete. Can be passed from Get-NectarReport
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReport -SearchQuery 'Test' | Remove-NectarReport
        Removes all reports that contain the word 'Test' in the name/description etc
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        Try {
            $URI = "https://$Global:NectarCloud/rapi/client/report/config/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $Null = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error deleting report #$($ID)"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Start-NectarReport {
    <#
        .SYNOPSIS
        Triggers the processing of a report
         
        .DESCRIPTION
        Triggers the processing of a report
 
        .PARAMETER ID
        The numeric ID of the report to start
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Start-NectarReport -ID 31
        Triggers the execution of report with ID 31
 
        Get-NectarReport Test | Start-NectarReport
        Triggers the execution of any report with 'Test' in the name
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        Try {
            $URI = "https://$Global:NectarCloud/rapi/client/report/runnow/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $Null = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error running report #$($ID)"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarReportPreview {
    <#
        .SYNOPSIS
        Gets information about the report widgets used for a given report
         
        .DESCRIPTION
        Gets information about the report widgets used for a given report
 
        .PARAMETER ID
        The numeric ID of the report to get information about
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReportPreview -ID 31
        Returns information about the report with ID 31
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        Try {
            $URI = "https://$Global:NectarCloud/rapi/client/report/config/detail/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader

            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}

            Return $JSON
        }
        Catch {
            Write-Error "Error getting report info for ID $ID. Does the report exist?"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarReportDetail {
    <#
        .SYNOPSIS
        Returns detail about a given widget within a report
         
        .DESCRIPTION
        Returns detail about a given widget within a report
 
        .PARAMETER ReportID
        The numeric ID of the report that contains the desired widget
         
        .PARAMETER ReportWidgetID
        The numeric ID of the report widget to get details about
 
        .PARAMETER WidgetType
        The type of widget to report on. If not supplied, will be obtained by running Get-NectarReportPreview
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReportDetail -ReportID 31 -ReportWidgetID 84
        Returns details about the widget with ID 84 on report ID 31
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ReportID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ReportWidgetID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('SESSIONS', 'QUALITY_AUDIO', 'QUALITY_VIDEO', 'QUALITY_APPSHARE', 'QUALITY_SUMMARY', 'SESSION_LIST', IgnoreCase=$False)]
        [string]$WidgetType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$WidgetDescription,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        # Get the widget type if it wasn't entered as a parameter
        If (!$WidgetType) {
            $WidgetType = ((Get-NectarReportPreview -ID $ReportID).Widgets | Where-Object {$_.ReportWidgetID -eq $ReportWidgetID}).widgetType
        }

        Switch ($WidgetType) {
            'GRAPH'             { $URLPath = 'graph' }
            'SESSIONS'            { $URLPath = 'graph' }
            'QUALITY_AUDIO'        { $URLPath = 'session/audio' }
            'QUALITY_VIDEO'        { $URLPath = 'session/video' }
            'QUALITY_APPSHARE'    { $URLPath = 'session/appshare' }
            'QUALITY_SUMMARY'    { $URLPath = 'quality/summary' }
        }

        $URI = "https://$Global:NectarCloud/rapi/client/report/data/$URLPath/$($ReportWidgetID)?tenant=$TenantName"
        Write-Verbose $URI

        Try {
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            If ($WidgetDescription) { $JSON | Add-Member -Name 'WidgetDescription' -Value $WidgetDescription -MemberType NoteProperty }
            Return $JSON
        }
        Catch {
            Write-Error "Error getting report widget detail for ID $ReportWidgetID. Does the report widget exist?"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





#################################################################################################################################################
# #
# Event Functions #
# #
#################################################################################################################################################

Function Get-NectarEvent {
    <#
        .SYNOPSIS
        Return a list of current or historic Nectar monitored device events
         
        .DESCRIPTION
        Return a list of current or historic Nectar monitored device events
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER LastTimeAfter
        Only return results that occurred more recently than the entered value. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER EventAlertLevels
        Return only events that meet the specified alert level. Choose one or more from CRITICAL, MAJOR, MINOR, WARNING, GOOD, NO_ACTIVITY
 
        .PARAMETER Locations
        Show alerts for one or more specified locations
 
        .PARAMETER SearchQuery
        Search for events that contain the specified string
         
        .PARAMETER OrderByField
        Order the resultset by the specified field. Choose from id, type, lastTime, displayName, deviceName, description, eventId, time, delay, source, location, sourceId
         
        .PARAMETER EventState
        Return either current events or previously acknowledged events
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarEvent -EventAlertLevels CRITICAL,MAJOR
        Returns a list of current events in the last hour that are either critical or major
         
        .EXAMPLE
        Get-NectarEvent -SearchQuery BadServer -EventState Historic -TimePeriod LAST_WEEK
        Returns a list of historical events from the last week that include the word 'badserver'
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [string]$LastTimeAfter,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('CRITICAL', 'MAJOR', 'MINOR', 'WARNING', 'GOOD', 'NO_ACTIVITY', IgnoreCase=$True)]
        [string[]]$EventAlertLevels,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("SiteName")]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(Mandatory=$False)]
        [ValidateSet('id', 'type', 'lastTime', 'displayName', 'deviceName', 'description', 'eventId', 'time', 'delay', 'source', 'location', 'sourceId', IgnoreCase=$True)]
        [string]$OrderByField,
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Current', 'Historic', IgnoreCase=$True)]
        [string]$EventState = 'Current',        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        $Params = @{
            'TimePeriod' = $TimePeriod
        }
    
        # Convert any PowerShell array objects to comma-separated strings to add to the GET querystring
        If ($LastTimeAfter) { $Params.Add('LastTimeAfter',$LastTimeAfter) } 
        If ($EventAlertLevels) { $EventAlertLevels | ForEach-Object { $EventAlertLevelsStr += ($(if($EventAlertLevelsStr){","}) + $_) }; $Params.Add('EventAlertLevels',$EventAlertLevelsStr) }
        If ($Locations) { $Locations | ForEach-Object { $LocationsStr += ($(if($LocationsStr){","}) + $_) }; $Params.Add('Locations',$LocationsStr) }
        If ($SearchQuery) { $Params.Add('q',$SearchQuery) }
        If ($OrderByField) { $Params.Add('OrderByField',$OrderByField) }
        If ($OrderDirection) { $Params.Add('OrderDirection',$OrderDirection) }
        If ($TenantName) { $Params.Add('Tenant',$TenantName) }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }

        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) {
            $Params.Add('pageSize',$ResultSize)
        } Else {
            $Params.Add('pageSize',$PageSize)
        }

        If ($EventState -eq 'Current') {
            $URI = "https://$Global:NectarCloud/dapi/event/current/all"
        } Else {
            $URI = "https://$Global:NectarCloud/dapi/event/historic/all"
        }
        
        Write-Verbose $URI

        Try {
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarEventCount {
    <#
        .SYNOPSIS
        Return a sampling of event counts for a given time period grouped by event severity.
         
        .DESCRIPTION
        Return a sampling of event counts for a given time period grouped by event severity. This is only a sample and is not intended to provide a true count of the number of events in a given period.
        Its useful for situations where you want to get a sense of event distribution without having to pull the entire list of events, which can take a lot of time.
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarEventCount -TimePeriod LAST_WEEK
        Returns a count of events grouped by severity for the last week
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $URI = "https://$Global:NectarCloud/dapi/event/alerts/summary?timePeriod=$TimePeriod&tenant=$TenantName"
            
            Write-Verbose $URI        
            
            Try {
                $RawCount = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            }
            Catch {  # DXP versions less than 1.15.0 use a trend, which is removed in 1.15.0. This allows this function to still work during the transition.
                $URI = "https://$Global:NectarCloud/dapi/event/alerts/trend?timePeriod=$TimePeriod&tenant=$TenantName"
                Write-Verbose $URI
                $RawCount = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            }

            $ResultSummary = [System.Collections.ArrayList]@()

            ForEach ($Event in $RawCount) {
                $EventSummary = [pscustomobject][ordered]@{
                    'EventType' = $Event.Type
                    'Count'        = $Event.data.count
                }
                $ResultSummary += $EventSummary
            }
            
            Return $ResultSummary        
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarEventDetail {
    <#
        .SYNOPSIS
        Return information about a specific event
         
        .DESCRIPTION
        Return information about a specific event
 
        .PARAMETER EventID
        The ID of the event to return details about
         
        .PARAMETER EventState
        Return either current events or previously acknowledged events
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-MSTeamsCallRecord -CallRecordID ed672235-5417-40ce-8425-12b8b702a505
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ID,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Current', 'Historic', IgnoreCase=$True)]
        [string]$EventState = 'Current',        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Params = @{
                'eventId' = $ID
            }
        
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            If ($EventState -eq 'Current') {
                $URI = "https://$Global:NectarCloud/dapi/event/current/view"
            }
            Else {
                $URI = "https://$Global:NectarCloud/dapi/event/historic/view"
            }
            
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON | Add-Member -TypeName 'Nectar.EventDetail'
            $JSON
        }
        Catch {
            Write-Error "Could not find event with ID $EventID"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}






#################################################################################################################################################
# #
# Cisco Platform Functions #
# #
#################################################################################################################################################

Function Get-NectarCiscoCluster {
    <#
        .SYNOPSIS
        Return base information about all Cisco clusters
         
        .DESCRIPTION
        Return base information about all Cisco clusters
 
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK'.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarCiscoCluster
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            $FilterSession = Set-NectarFilterParams -Scope DEFAULT -Platform CISCO -TenantName $TenantName -TimePeriod $TimePeriod

            $URI = "https://$Global:NectarCloud/dapi/platform/clusters?pageSize=$PageSize&platform=CISCO&tenant=$TenantName"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -WebSession $FilterSession
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON.elements
        }
        Catch {
            Write-Error "Could not get platform items"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarCiscoClusterInventory {
    <#
        .SYNOPSIS
        Return inventory information for a given Cisco cluster
         
        .DESCRIPTION
        Return inventory information for a given Cisco cluster
 
        .PARAMETER ClusterID
        The Nectar-defined ClusterID for the given cluster. Accepts pipelined results
 
        .PARAMETER InventorySet
        What specified inventory set to pull information about
 
        .PARAMETER SearchQuery
        A string to search for. Will search for match against all fields
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort ordered output in ascending or descending order
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
     
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 10000
         
        .PARAMETER ResultSize
        The total number of results to return. Maximum result size is 9,999,999 results
     
        .EXAMPLE
        Get-NectarCiscoClusterInventory -ClusterID 39_1 -InventorySet CallManagers
        Returns information about the call managers for the specified clusterID
 
        .EXAMPLE
        Get-NectarCiscoCluster | Get-NectarCiscoClusterInventory -InventorySet Gateways
        Returns information about gateways from all Cisco clusters
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [Parameter(Mandatory=$True)]
        [ValidateSet('Annunciators','ApplicationServers','AssignedPresenceServers','CallManagers','ConferenceBridges','DevicePools','Gatekeepers','Gateways','GatewayEndpoints','H323Gateways','HuntLists','HuntPilots','InteractiveVoiceResponses','LicenseUsers','Locations','Mtp','MusicOnholdServers','Phones','RecordingProfiles','Services','SipTrunk','Transcoders','Users','VoiceMailPilots','VoiceMailPorts', IgnoreCase=$True)]
        [string]$InventorySet,
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,        
        [Parameter(Mandatory=$False)]
        [string]$OrderByField = 'idx',
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$PageSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,999999)]
        [int]$ResultSize,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        # The URLs for each inventory set is usually split on capital letters
        # This little function takes care of that
        # EG: MySampleTests turns into /my/sample/tests
        $InventoryURL = ($InventorySet -CReplace '([A-Z])', '/$1').ToLower()

        # Take care of any cases that don't match the above rule
        Switch ($InventorySet) {
            'HuntLists' { $InventoryURL = '/hunts'; Break}
        }

        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) { $PageSize = $ResultSize }

        $Params = @{
            'pageNumber'         = 1
            'pageSize'            = $PageSize
            'orderByField'        = $OrderByField
            'orderDirection'    = $OrderDirection
            'platform'            = 'CISCO'
            'tenant'            = $TenantName                
        }

        If ($SearchQuery) { $Params.Add('q', $SearchQuery) }

        $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/inventory$InventoryURL"
        Write-Verbose $URI        
        
        $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
        
        $TotalPages = $JSON.totalPages

        If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }

        If ($Name) { $JSON.elements | Add-Member -Name 'ClusterName' -Value $Name -MemberType NoteProperty }
        $JSON.elements

        If ($TotalPages -gt 1 -and !($ResultSize)) {
            $PageNum = 2
            Write-Verbose "Page size: $PageSize"
            While ($PageNum -le $TotalPages) {
                Write-Verbose "Working on page $PageNum of $TotalPages"
                $Params.PageNumber = $PageNum
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
                If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                If ($Name) { $JSON.elements | Add-Member -Name 'ClusterName' -Value $Name -MemberType NoteProperty }
                $JSON.elements
                $PageNum++
            }
        }
    }
}





#################################################################################################################################################
# #
# Platform Functions #
# #
#################################################################################################################################################

Function Get-NectarPlatformItems {
    <#
        .SYNOPSIS
        Return information about all the platforms installed
         
        .DESCRIPTION
        Return information about all the platforms installed
 
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItems -Platform CISCO
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/clusters"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON.elements
        }
        Catch {
            Write-Error "Could not get platform items"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformItemSummary {
    <#
        .SYNOPSIS
        Return summary information about a specific platform item
         
        .DESCRIPTION
        Return summary information about a specific platform item
         
        .PARAMETER ClusterID
        The ID of a cluster to return summary information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER Source
        Show information about either events, current status or both
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItemSummary -Platform CISCO -ClusterID 3_1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [Parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Events','Current','All', IgnoreCase=$True)]
        [string]$Source = 'All',    
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/summary"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            If ($Source -ne 'All') {
                $JSON.$Source
            }
            Else {
                $JSON
            }
        }
        Catch {
            Write-Error "Could not get platform item summary"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformItemResources {
    <#
        .SYNOPSIS
        Return resource information about a specific platform item
         
        .DESCRIPTION
        Return resource information about a specific platform item
         
        .PARAMETER ClusterID
        The ID of a cluster to return resource information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItemResources -Platform CISCO -ClusterID 3_1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [Parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/resources"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON
        }
        Catch {
            Write-Error "Could not get platform item server resources"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformItemServers {
    <#
        .SYNOPSIS
        Return information about a specific platform item's servers
         
        .DESCRIPTION
        Return information about a specific platform item's servers
         
        .PARAMETER ClusterID
        The ID of a cluster to return server information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER Type
        Show information about either publishers, subscribers or both
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItemServers -Platform CISCO -ClusterID 3_1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [Parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Publisher','Subscribers','All', IgnoreCase=$True)]
        [string]$Type = 'All',    
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/servers"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            If ($Type -ne 'All') {
                $JSON.$Type
            }
            Else {
                $JSON
            }
        }
        Catch {
            Write-Error "Could not get platform item servers"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformServerServices {
    <#
        .SYNOPSIS
        Return service information about a specific platform item
         
        .DESCRIPTION
        Return service information about a specific platform item
         
        .PARAMETER ClusterID
        The ID of a cluster to return service information
         
        .PARAMETER ServerID
        The ID of a server within a cluster to return service information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformServerServices -Platform CISCO -ClusterID 3_1 -ServerID 1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("PlatformItemId")]
        [string]$ClusterID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("Id")]
        [string]$ServerID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('EDGE','FE','MEDIATION','GATEWAY','PUBLISHER','SUBSCRIBER','CUBE','IM_PRESENCE','VG224','CUCM','CMS','HW_CFB','CUCM_SW_CFB', IgnoreCase=$False)]
        [string]$Type,
        [Parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','JABRA','MISCELLANEOUS', IgnoreCase=$False)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$False)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
            'type' = $Type
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/server/$ServerID/services"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON
        }
        Catch {
            Write-Error "Could not get platform item server services"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarPlatformDashboard {
    <#
        .SYNOPSIS
        Return information about all custom Platform dashboards
         
        .DESCRIPTION
        Return information about all custom Platform dashboards
 
        .PARAMETER DashboardName
        The name of a dashboard to return data on
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarPlatformDashboards
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DashboardName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            $URI = "https://$Global:NectarCloud/aapi/client/dashboards?tenant=$TenantName"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            If ($DashboardName) {
                Return $JSON | Where-Object {$_.name -eq $DashboardName}
            }
            Else {
                Return $JSON
            }
        }
        Catch {
            Write-Error "Could not get platform dashboards"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarPlatformDashboardItemPollerSummary {
    <#
        .SYNOPSIS
        Return current poller information about a given device/item
         
        .DESCRIPTION
        Return current poller information about a given device/item
 
        .PARAMETER ItemID
        The ID of the item to return poller information about
 
        .PARAMETER PollerGroup
        The poller group to return poller information on. Choose from Application, Server or Network
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarPlatformDashboardItemPollerSummary -ItemID 320_51
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('ID')]
        [string]$ItemID,
        [Parameter(Mandatory=$True)]
        [ValidateSet('Application','Server','Network', IgnoreCase=$True)]
        [string]$PollerGroup,
        [Parameter(ValueFromPipelineByPropertyName, DontShow)]
        [Alias('Name')]
        [string]$ItemName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            $URI = "https://$Global:NectarCloud/dapi/platform/dashboard/item/$ItemID/summary?tenant=$TenantName&group=$($PollerGroup.ToUpper())"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            # If the command is called as part of a pipeline from Get-NectarPlatformDashboard then add the device name to the output
            If ($ItemName) {$JSON | Add-Member -Name 'ItemName' -Value $ItemName -MemberType NoteProperty}

            # Add the Item ID and PollerGroup to the output
            $JSON | Add-Member -Name 'ItemID' -Value $ItemID -MemberType NoteProperty
            $JSON | Add-Member -Name 'PollerGroup' -Value $PollerGroup -MemberType NoteProperty

            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            Return $JSON
        }
        Catch {
            Write-Error "Could not get platform dashboards"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformDashboardItemPollerDetail {
    <#
        .SYNOPSIS
        Return specific poller information over time about a given device/item
         
        .DESCRIPTION
        Return specific poller information over time about a given device/item
 
        .PARAMETER ItemID
        The ID of the item to return poller information about
 
        .PARAMETER Index
        The numeric index of the poller to return information on
 
        .PARAMETER Type
        The metric type assigned to the poller
 
        .PARAMETER PollerGroup
        The poller group to return poller information on. Choose from Application, Server or Network
 
        .PARAMETER ExpandHistogram
        Show a detailed table with the contents of the histogram output instead of a difficult to parse array
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarPlatformDashboardItemPollerDetail -ItemID 320_51
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ItemID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Index,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Type,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('Application','Server','Network', IgnoreCase=$True)]
        [string]$PollerGroup,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$False)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [switch]$ExpandHistogram,
        [Parameter(ValueFromPipelineByPropertyName, DontShow)]
        [string]$ItemName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
            $Params = @{}
            # Convert date to UNIX timestamp
            If($TimePeriodFrom) {
                $TimePeriodFromUNIX = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
                $Params.Add('StartDateFrom',$TimePeriodFromUNIX)
            }
            
            If($TimePeriodTo) {
                $TimePeriodToUNIX = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
                $Params.Add('StartDateTo',$TimePeriodToUNIX)
            }
    
            $URI = "https://$Global:NectarCloud/dapi/platform/dashboard/item/$ItemID/metric?type=$Type&group=$($PollerGroup.ToUpper())&timePeriod=$TimePeriod&index=$Index&tenant=$TenantName"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($ItemName) {$JSON | Add-Member -Name 'ItemName' -Value $ItemName -MemberType NoteProperty}
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}

            If ($ExpandHistogram) {
                $Histogram = $JSON.histogram
                $HistoFormatted = @()
                ForEach ($Element in $Histogram) { 
                    $RowData = New-Object PsObject
                    $RowData | Add-Member -NotePropertyName 'Date' -NotePropertyValue $Element[0]
                    $RowData | Add-Member -NotePropertyName 'Item' -NotePropertyValue $ItemName
                    $RowData | Add-Member -NotePropertyName 'Poller' -NotePropertyValue $JSON.Name
                    $RowData | Add-Member -NotePropertyName 'Value' -NotePropertyValue $Element[1]
                    $HistoFormatted += $RowData
                }
                Return $HistoFormatted
            } 
            Else {
                Return $JSON
            }
        }
        Catch {
            Write-Error "Could not get item poller detail"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarServers {
    <#
        .SYNOPSIS
        Returns a list of platform servers
         
        .DESCRIPTION
        Returns a list of platform servers like datacenter names (TEAMS), servernames (CISCO etc)
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarServers
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gns")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/platform/servers?tenant=$TenantName"
        
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            If (!$JSON.elements) {
                Write-Error "Server list could not be found."
            }
            Else {
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -TypeName 'Nectar.LocationList'
                Return $JSON.elements
            }
        }
        Catch {
            Write-Error "Unable to get server details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}








#################################################################################################################################################
# #
# Endpoint Client Functions #
# #
#################################################################################################################################################

Function Get-NectarEndpoint {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP endpoints
 
        .DESCRIPTION
        Returns a list of Nectar DXP endpoints
 
        .PARAMETER SearchQuery
        A string to search for. Will search for match against all fields
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort ordered output in ascending or descending order
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 100000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarEndpoint
        Returns the first 100000 endpoints
         
        .EXAMPLE
        Get-NectarEndpoint -ResultSize 100
        Returns the first 100 endpoints
 
        .EXAMPLE
        Get-NectarEndpoint -SearchQuery US
        Returns all endpoints that have US in any of the data fields
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gne")]    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('id','uuid','name','ipAddress','isHub','mapped','userName','userDisplayName','location', IgnoreCase=$False)]
        [string]$OrderByField = 'id',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('asc','desc', IgnoreCase=$False)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 100000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/aapi/testing/entities/"
            
            $Params = @{
                'orderByField' = $OrderByField
                'orderDirection' = $OrderDirection
            }
            
            If ($SearchQuery) { $Params.Add('searchQuery', $SearchQuery) }
            
            If ($ResultSize) { 
                $Params.Add('pageSize', $ResultSize) 
            }
            Else { 
                $Params.Add('pageSize', $PageSize)
            }

            If ($TenantName) { $Params.Add('tenant', $TenantName) }            
            

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
            $JSON.elements
            
            $TotalPages = $JSON.totalPages
        
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params
                    If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "Unable to retrieve endpoint information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}



Function Set-NectarEndpoint {
    <#
        .SYNOPSIS
        Modify properties of a Nectar DXP endpoint
 
        .DESCRIPTION
        Modify properties of a Nectar DXP endpoint
 
        .PARAMETER SearchQuery
        A string to search for to return a specific endpoint. Will search for match against all fields
         
        .PARAMETER UserName
        The username to associate with the given endpoint. Set to $NULL to de-associate a user with an endpoint
         
        .PARAMETER IsHub
        Set the endpoint to be a hub endpoint or not.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarEndpoint -SearchQuery ff4bf84a-04d8-11ec-ad2e-17d4f8d1df89 -UserName tferguson@contoso.com -IsHub $False
        Sets a specific endpoint's associated username to 'tferguson'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gne")]    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('UUID')]
        [string]$SearchQuery,
        [Parameter(Mandatory=$False)]
        [string]$UserName,
        [Parameter(Mandatory=$False)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [bool]$IsHub,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 100000
    )
    
    Begin {
        Connect-NectarCloud

        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $ECSearchURI = "https://$Global:NectarCloud/aapi/testing/entities/"
        $ECUpdateURI = "https://$Global:NectarCloud/aapi/testing/entity/"
        $UserNameURI = "https://$Global:NectarCloud/aapi/testing/entities/usernames/"
    }        
    Process {
        Try {    

            # Search for a single valid endpoint. Throw error if zero or more than 1 endpoint returned
            $ECSearchParams = @{
                'searchQuery' = $SearchQuery
            }
            
            If ($TenantName) { $ECSearchParams.Add('tenant', $TenantName) }

            $ECSearchJSON = Invoke-RestMethod -Method GET -uri $ECSearchURI -Headers $Global:NectarAuthHeader -Body $ECSearchParams
            
            If ($ECSearchJSON.totalElements -gt 1) {
                Write-Error 'Too many endpoints returned. Please refine your search query to return only a single endpoint.'
                Return
            }
            ElseIf ($ECSearchJSON.totalElements -eq 0) {
                Write-Error "Could not find endpoint $SearchQuery"
                Return
            }
            
            $EndpointDetails = $ECSearchJSON.elements
            
            # Search for a single valid username. Throw error if zero or more than 1 username returned
            If ($UserName) {
                $UserNameParams = @{
                    'searchQuery' = $UserName
                }
                
                If ($TenantName) { $UserNameParams.Add('tenant', $TenantName) }

                $UserSearchJSON = Invoke-RestMethod -Method GET -uri $UserNameURI -Headers $Global:NectarAuthHeader -Body $UserNameParams
                
                If ($UserSearchJSON.Count -gt 1) {
                    Write-Error 'Too many usernames returned. Please refine your search query to return only a single username.'
                    Return
                }
                ElseIf ($UserSearchJSON.Count -eq 0) {
                    Write-Error "Could not find user with name $UserName"
                    Return
                }
                
                $UserName = $UserSearchJSON.UserName
            }
            
            If ($PSBoundParameters.Keys.Contains('UserName')) {
                If ($UserName) {
                    $EndpointDetails[0].userName = $UserName
                }
                Else {
                    $EndpointDetails[0].userName = $NULL
                }
            }
            
            If ($DisplayName) { $EndpointDetails[0].userDisplayName = $DisplayName }

            If ($PSBoundParameters.Keys.Contains('IsHub')) { 
                $EndpointDetails[0].IsHub = $IsHub 
                $EndpointDetails[0].userName = $NULL
            }
            
            $EndpointJSON = $EndpointDetails | ConvertTo-Json
            
            Write-Verbose $EndpointJSON
            
            If ($TenantName) { $ECUpdateURI = $ECUpdateURI + "?tenant=$TenantName" }
            
            $NULL = Invoke-RestMethod -Method PUT -uri $ECUpdateURI -Headers $Global:NectarAuthHeader -Body $EndpointJSON -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to set endpoint information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}




Function Remove-NectarEndpoint {
    <#
        .SYNOPSIS
        Remove a Nectar DXP endpoint
 
        .DESCRIPTION
        Remove a Nectar DXP endpoint
 
        .PARAMETER ID
        The ID of the endpoint to remove. Both ID and UUID are required for deletion.
         
        .PARAMETER UUID
        The UUID of the endpoint to remove. Both ID and UUID are required for deletion.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarEndpoint -SearchQuery tferguson | Remove-NectarEndpoint
        Removes the endpoint associated with 'tferguson'
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$UUID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud

        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
    }        
    Process {
        Try {    
            $URI = "https://$Global:NectarCloud/aapi/testing/entity?id=$ID&uuid=$UUID&tenant=$TenantName"
            
            $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Unable to delete endpoint information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}





Function Get-NectarEndpointTest {
    <#
        .SYNOPSIS
        Returns information about Endpoint Client tests
 
        .DESCRIPTION
        Returns information about Endpoint Client tests
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TestTypes
        The types of EPC tests to return. Choose from one or more of 'P2P', 'PING', 'AUDIO', or 'VIDEO'
         
        .PARAMETER TestResults
        The result type to return. Choose from one or more of 'PASSED','FAILED', 'INCOMPLETE' or 'UNKNOWN'
         
        .PARAMETER ResponseCodes
        Show tests that match one or more SIP response codes. Accepts numbers from 0 to 699
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarEndpointTest -TimePeriod LAST_HOUR -TestTypes P2P -TestResults FAILED
        Returns a list of all Endpoint Client P2P tests that failed within the last hour
 
        .EXAMPLE
        Get-NectarEndpointTest -TimePeriod LAST_DAY -Locations 'Head Office', Warehouse
        Returns a list of all Endpoint Client tests where either the caller or callee was in the corporate head office or the warehouse
         
        .EXAMPLE
        Get-NectarEndpointTest -TimePeriod LAST_WEEK -CallerExtCities Chicago, Dallas
        Returns a list of all Endpoint Client tests from the last week where the caller was in either Chicago or Dallas
             
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False)]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN','INCOMPLETE', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,699)]
        [string[]]$ResponseCodes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(Mandatory=$False)]
        [string]$OrderByField,
        [Parameter(Mandatory=$False)]
        [ValidateSet('ASC','DESC', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,10000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,50000)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($PageSize) { $PSBoundParameters.Remove('PageSize') | Out-Null }
        If ($ResultSize) { $PSBoundParameters.Remove('ResultSize') | Out-Null }
        If ($OrderByField) { $PSBoundParameters.Remove('OrderByField') | Out-Null }
        If ($OrderDirection) { $PSBoundParameters.Remove('OrderDirection') | Out-Null }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters -Platform ENDPOINT_CLIENT -Scope ENDPOINT_CLIENT
        
        $URI = "https://$Global:NectarCloud/dapi/testing/results"
        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        $Params = @{
            'Scope' = 'ENDPOINT_CLIENT'
            'Platform' = 'ENDPOINT_CLIENT'
        }
        
        If ($ResultSize) {
            $Params.Add( 'pageSize',$ResultSize)
        }
        Else {
            $Params.Add('pageSize',$PageSize)
        }
        
        If($OrderByField) { $Params.Add('orderByField',$OrderByField) }
        If($OrderDirection) { $Params.Add('orderDirection',$OrderDirection) }
        If ($TenantName) { $Params.Add('tenant',$TenantName) }
        
        # Return results in pages
        Try {
            Write-Verbose $URI
            ForEach($k in $Params.Keys){Write-Verbose "$k $($Params[$k])"}
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarEndpointTestSummary {
    <#
        .SYNOPSIS
        Returns summary information about Endpoint Client tests
 
        .DESCRIPTION
        Returns summary information about Endpoint Client tests
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TestTypes
        The types of EPC tests to return. Choose from one or more of 'P2P', 'PING', 'AUDIO', or 'VIDEO'
         
        .PARAMETER TestResults
        The result type to return. Choose from one or more of 'PASSED','FAILED', 'INCOMPLETE' or 'UNKNOWN'
         
        .PARAMETER ResponseCodes
        Show tests that match one or more SIP response codes. Accepts numbers from 0 to 699
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarEndpointTestSummary -TimePeriod LAST_HOUR -TestTypes P2P -TestResults FAILED
        Returns a summary of all Endpoint Client P2P tests that failed within the last hour grouped by endpoint
 
        .EXAMPLE
        Get-NectarEndpointTestSummary -TimePeriod LAST_DAY -Locations 'Head Office', Warehouse
        Returns a summary of all Endpoint Client tests grouped by endpoint where either the caller or callee was in the corporate head office or the warehouse
         
        .EXAMPLE
        Get-NectarEndpointTestSummary -TimePeriod LAST_WEEK -CallerExtCities Chicago, Dallas
        Returns a list of all Endpoint Client tests from the last week grouped by endpoint where the caller was in either Chicago or Dallas
             
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False)]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN','INCOMPLETE','INCOMPLETE', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(Mandatory=$False)]
        [ValidateSet('HUB','ENDPOINT', IgnoreCase=$False)]
        [string]$HubOrEndpoint = 'ENDPOINT',
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,699)]
        [string[]]$ResponseCodes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TestSuccessRate','TestCount', IgnoreCase=$True)]
        [string]$OrderByField,
        [Parameter(Mandatory=$False)]
        [ValidateSet('ASC','DESC', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,10000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,50000)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        If ($HubOrEndpoint) { $PSBoundParameters.Remove('HubOrEndpoint') | Out-Null }
        If ($PageSize) { $PSBoundParameters.Remove('PageSize') | Out-Null }
        If ($ResultSize) { $PSBoundParameters.Remove('ResultSize') | Out-Null }
        If ($OrderByField) { $PSBoundParameters.Remove('OrderByField') | Out-Null }
        If ($OrderDirection) { $PSBoundParameters.Remove('OrderDirection') | Out-Null }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters -Platform ENDPOINT_CLIENT -Scope ENDPOINT_CLIENT
        
        $URI = "https://$Global:NectarCloud/dapi/testing/entities"
        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        $Params = @{
            'Platform' = 'ENDPOINT_CLIENT'
            'type' = $HubOrEndpoint
        }
        
        If ($ResultSize) {
            $Params.Add( 'pageSize',$ResultSize)
        }
        Else {
            $Params.Add('pageSize',$PageSize)
        }
        
        If($OrderByField) { $Params.Add('orderByField',$OrderByField) }
        If($OrderDirection) { $Params.Add('orderDirection',$OrderDirection) }
        If ($TenantName) { $Params.Add('tenant',$TenantName) }
        
        # Return results in pages
        Try {
            Write-Verbose $URI
            foreach($k in $Params.Keys){Write-Verbose "$k $($Params[$k])"}
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarEndpointTestCount {
    <#
        .SYNOPSIS
        Returns summary information about Endpoint Client tests
 
        .DESCRIPTION
        Returns summary information about Endpoint Client tests
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TestTypes
        The types of EPC tests to return. Choose from one or more of 'P2P', 'PING', 'AUDIO', or 'VIDEO'
         
        .PARAMETER TestResults
        The result type to return. Choose from one or more of 'PASSED','FAILED', 'INCOMPLETE' or 'UNKNOWN'
         
        .PARAMETER ResponseCodes
        Show tests that match one or more SIP response codes. Accepts numbers from 0 to 699
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarEndpointTestCount -TimePeriod LAST_HOUR -TestTypes P2P -TestResults FAILED
        Returns a count of all Endpoint Client P2P tests that failed within the last hour
 
        .EXAMPLE
        Get-NectarEndpointTestCount -TimePeriod LAST_DAY -Locations 'Head Office', Warehouse
        Returns a count of all Endpoint Client tests where either the caller or callee was in the corporate head office or the warehouse
         
        .EXAMPLE
        Get-NectarEndpointTestSummary -TimePeriod LAST_WEEK -CallerExtCities Chicago, Dallas
        Returns a count of all Endpoint Client tests from the last week where the caller was in either Chicago or Dallas
             
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False)]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN','INCOMPLETE','INCOMPLETE', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(Mandatory=$False)]
        [ValidateSet('HUB','ENDPOINT', IgnoreCase=$False)]
        [string]$HubOrEndpoint = 'ENDPOINT',
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,699)]
        [string[]]$ResponseCodes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(Mandatory=$False)]
        [ValidateSet('ASC','DESC', IgnoreCase=$True)]
        [string]$OrderDirection,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        If ($HubOrEndpoint) { $PSBoundParameters.Remove('HubOrEndpoint') | Out-Null }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters -Platform ENDPOINT_CLIENT -Scope ENDPOINT_CLIENT
        
        $URI = "https://$Global:NectarCloud/dapi/testing/counts"
        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        $Params = @{
            'platform' = 'ENDPOINT_CLIENT'
            'tenant' = $TenantName
        }
        
        # Try {
            Write-Verbose $URI
            foreach($k in $Params.Keys){Write-Verbose "$k $($Params[$k])"}
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params -WebSession $FilterSession
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            Return $JSON
        # }
        # Catch {
        # Write-Error "No results. Try specifying a less-restrictive filter"
        # Get-JSONErrorStream -JSONResponse $_
        # }
    }
}





#################################################################################################################################################
# #
# MS Teams Functions #
# #
#################################################################################################################################################

Function Get-MSGraphAccessToken {
    <#
        .SYNOPSIS
        Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries.
         
        .DESCRIPTION
        Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
         
        .PARAMETER CertFriendlyName
        The friendly name of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret
 
        .PARAMETER CertThumbprint
        The thumbprint of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret
 
        .PARAMETER CertPath
        The path to a PFX certificate to be used for certificate authentication. Can be used instead of MSClientSecret
         
        .PARAMETER CertStore
        The certificate store to be used for certificate authentication. Select either LocalMachine or CurrentUser. Used in conjunction with CertThumbprint or CertFriendlyName
        Can be used instead of MSClientSecret.
 
        .PARAMETER Scope
        The scope for the associated access token. Select from GraphAPI or Teams. Defaults to GraphAPI
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Obtains an authtoken for the given tenant using secret-based auth and saves the results for use in other commands in a variable called $AuthToken
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 029834092-234234-234234-23442343 -MSTenantID 234234234-234234-234-23-42342342 -CertFriendlyName 'CertAuth' -CertStore LocalMachine
        Obtains an authtoken for the given tenant using certificate auth and saves the results for use in other commands in a variable called $AuthToken
 
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSTenantID,
        [Parameter(Mandatory=$False)]
        [switch]$N10Cert,
        [Parameter(Mandatory=$False)]
        [string]$CertFriendlyName,
        [Parameter(Mandatory=$False)]
        [string]$CertThumbprint,
        [Parameter(Mandatory=$False)]
        [string]$CertPath,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LocalMachine','CurrentUser', IgnoreCase=$True)]
        [string]$CertStore = 'CurrentUser',
        [Parameter(Mandatory=$False)]
        [ValidateSet('GraphAPI','TeamsPS','TeamsAPI','Test', IgnoreCase=$True)]
        [string]$Scope = 'GraphAPI'
    )
    
    Begin {
        Switch ($Scope) {
            'GraphAPI'    { $AppliedScope = 'https://graph.microsoft.com/.default' }
            'TeamsPS'    { $AppliedScope = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default' }  # Can be used to connect to TeamsPS module along with Graph API token: Connect-MicrosoftTeams -AccessTokens @("$graphToken", "$teamsToken")
            'TeamsAPI'    { $AppliedScope = 'https://ring0.api.interfaces.records.teams.microsoft.com/.default' }  # Requires Azure app with permission 'Skype and Teams Tenant Admin API/application_access'
            'Test'        { $AppliedScope = 'https://api.interfaces.records.teams.microsoft.com/user_impersonation/.default' } 
        }
    }
    Process {
        If ($MSClientSecret) {
            # Get the Azure Graph API auth token
            $AuthBody = @{
                grant_type         = 'client_credentials'
                client_id         = $MSClientID
                client_secret    = $MSClientSecret
                scope             = $AppliedScope
            }
            
            $URI = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token"
            Write-Verbose $URI
            $JSON_Auth = Invoke-RestMethod -Method POST -uri $URI -Body $AuthBody
            $AuthToken = $JSON_Auth.access_token
            
            Return $AuthToken
        }
        Else {
            <#
            Needs access to the full certificate stored in Nectar DXP, and can be exported to PEM via Get-NectarMSTeamsSubscription (only if you have global admin privs)
              Get-NectarMSTeamsSubscription -ExportCertificate
             
            Need to create a certificate from the resulting PEM files using the following command:
            openssl pkcs12 -export -in TeamsCert.pem -inkey TeamsPriv.key -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -out FullCert.pfx
            Requires that OpenSSL is installed
 
            To convert a PFX to a Kubernetes secret in Base64 format, run the following:
              $fileContentBytes = get-content FullCert.pfx -AsByteStream
              [System.Convert]::ToBase64String($fileContentBytes) | Out-File pfx-encoded-bytes.txt
 
            Then use the resulting certificate to obtain an access token:
            $GraphToken = Get-MSGraphAccessToken -MSTenantID <tenantID> -MSClientID <Client/AppID> -CertPath .\FullCert.pfx
             
            Then use the resulting token in other commands like:
            Test-MSTeamsConnectivity -AuthToken $GraphToken
            #>

                        
            # Get the certificate information via one of several methods
            If ($CertThumbprint) { $Certificate = Get-Item Cert:\$CertStore\My\$CertThumbprint }
            If ($CertFriendlyName) { $Certificate = Get-ChildItem Cert:\$CertStore\My | Where-Object {$_.FriendlyName -eq $CertFriendlyName} }
            If ($CertPath) { $Certificate = Get-PfxCertificate -FilePath $CertPath }
            If ($N10Cert) { 
                # Get certificate BASE64 encoding from N10
                $CertBlob = (Get-NectarMSTeamsSubscription).msClientCertificateDto.certificate
                $CertRaw = $CertBlob -replace "-----BEGIN CERTIFICATE-----", $NULL -replace "-----END CERTIFICATE-----", $NULL
                $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                $Certificate.Import([Convert]::FromBase64String($CertRaw))            
            }
            
            If ($Certificate) {
                # Adapted from https://adamtheautomator.com/microsoft-graph-api-powershell/
                # Create base64 hash of certificate
                $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
                
                # Create JWT timestamp for expiration
                $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
                $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
                $JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)

                # Create JWT validity start timestamp
                $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
                $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

                # Create JWT header
                $JWTHeader = @{
                    alg = "RS256"
                    typ = "JWT"
                    # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
                    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
                }

                # Create JWT payload
                $JWTPayload = @{
                    # What endpoint is allowed to use this JWT
                    aud = "https://login.microsoftonline.com/$MSTenantID/oauth2/token"

                    # Expiration timestamp
                    exp = $JWTExpiration

                    # Issuer = your application
                    iss = $MSClientID

                    # JWT ID: random guid
                    jti = [guid]::NewGuid()

                    # Not to be used before
                    nbf = $NotBefore

                    # JWT Subject
                    sub = $MSClientID
                }

                # Convert header and payload to base64
                $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
                $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

                $JWTPayloadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
                $EncodedPayload = [System.Convert]::ToBase64String($JWTPayloadToByte)

                # Join header and Payload with "." to create a valid (unsigned) JWT
                $JWT = $EncodedHeader + "." + $EncodedPayload

                # Get the private key object of your certificate
                $PrivateKey = $Certificate.PrivateKey
                
                # Define RSA signature and hashing algorithm
                $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
                $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 

                # Create a signature of the JWT
                ##### Breaks down here. Only works with full cert downloaded/installed #####
                $Signature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)) -replace '\+','-' -replace '/','_' -replace '='

                # Join the signature to the JWT with "."
                $JWT = $JWT + "." + $Signature
                
                # Create a hash with body parameters
                $Body = @{
                    client_id                 = $MSClientID
                    client_assertion         = $JWT
                    client_assertion_type     = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
                    scope                     = $AppliedScope
                    grant_type                 = 'client_credentials'
                }

                $Uri = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token"

                # Use the self-generated JWT as Authorization
                $Header = @{
                    Authorization = "Bearer $JWT"
                }

                # Splat the parameters for Invoke-Restmethod for cleaner code
                $PostSplat = @{
                    ContentType = 'application/x-www-form-urlencoded'
                    Method = 'POST'
                    Body = $Body
                    Uri = $Uri
                    Headers = $Header
                }

                $Request = Invoke-RestMethod @PostSplat
                
                $AuthToken = $Request.access_token
                Return $AuthToken
            }
            Else {
                Write-Error "Could not find certificate in $CertStore"
            }
        }
    }
}



Function Test-MSTeamsConnectivity {
    <#
        .SYNOPSIS
        Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant.
         
        .DESCRIPTION
        Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
                 
        .PARAMETER SkipUserCount
        Skips the user count
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-NectarMSTeamsSubscription -TenantName contoso | Test-MSAzureADAccess
 
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(Mandatory=$False)]
        [switch]$SkipUserCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideOutput,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken        
    )
    
    Process {
        If ($MSTenantID) { 
            $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
        }
        ElseIf (!$AuthToken) {
            $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
        }
    
        $Headers = @{
            Authorization = "Bearer $AuthToken"
        }

        $AccessResults = [pscustomobject][ordered]@{
            [string]'TeamsCallRecords'    = 'FAIL'
            [string]'TeamsDevices'        = 'FAIL'
            [string]'AzureUsers'        = 'FAIL'
            [string]'AzureGroups'        = 'FAIL'
        }
        
        # Test MS Teams call record access
        Try {
            $FromDateTime = (Get-Date -Format 'yyyy-MM-dd')
            $ToDateTime = ((Get-Date).AddDays(+1).ToString('yyyy-MM-dd'))
            $URI = "https://graph.microsoft.com/beta/communications/callRecords/getDirectRoutingCalls(fromDateTime=$FromDateTime,toDateTime=$ToDateTime)"
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Teams CR Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.TeamsCallRecords = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
        }

        # Test MS Teams device access
        Try {
            $URI = 'https://graph.microsoft.com/beta/teamwork/devices'
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Teams Device Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.TeamsDevices = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
        }

        # Test Azure AD user access
        Try {
            $URI = 'https://graph.microsoft.com/v1.0/users'
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Azure AD User Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.AzureUsers = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
            Get-JSONErrorStream -JSONResponse $_
            $SkipUserCount = $True
        }

        # Test Azure AD group access
        Try {
            $URI = 'https://graph.microsoft.com/v1.0/groups'
            
            If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            If (!$HideOutput) { Write-Host 'Azure AD Group Status: ' -NoNewLine }
        
            $NULL = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

            If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green }
            $AccessResults.AzureGroups = 'PASS'
        }
        Catch {
            If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red }
        }

        If (!$SkipUserCount) { Get-MSTeamsUserLicenseCount -TenantName $TenantName }
        
        Clear-Variable AuthToken
        Return $AccessResults
    }
}



Function Get-MSAzureGraphSubscriptions {
    <#
        .SYNOPSIS
        Return data about the subscriptions for a given Azure tenant.
         
        .DESCRIPTION
        Return data about the subscriptions for a given Azure tenant.
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-MSAzureGraphSubscriptions
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken    
    )
    
    Process {
        Try {
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }

            # Test Azure AD access
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            $URI = "https://graph.microsoft.com/v1.0/subscriptions"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            Return $JSON.value
        }
        Catch {
            Write-Error "Could not obtain subscription information."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarMSTeamsSubscription {
    <#
        .SYNOPSIS
        Return data about the MSTeams subscription for a given tenant.
         
        .DESCRIPTION
        Return data about the MSTeams subscription for a given tenant. Requires a global admin account. Not available to tenant-level admins.
 
        .EXAMPLE
        Get-NectarMSTeamsSubscription
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnmtai","Get-MSTeamsAppInfo")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$ExportCertificate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/subscription?tenant=$TenantName"

            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            If ($ExportCertificate) {
                Try {
                    $JSON.data.msClientCertificateDto.certificate | Out-File TeamsCert.pem -Encoding ASCII
                    $JSON.data.msClientCertificateDto.privateKey | Out-File TeamsPriv.key -Encoding ASCII
                    Write-Host 'Successfully exported certificate to .\TeamsCert.pem and .\TeamsPriv.key'
                }
                Catch {
                    Get-JSONErrorStream -JSONResponse $_
                }
            }
            Else {
                Return $JSON.data
            }                
        }
        Catch {
            Write-Error "TenantName: $TenantName - No MS Teams information found or insufficient permissions."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarAzureADSubscription {
    <#
        .SYNOPSIS
        Return data about the Azure AD subscription for a given tenant.
         
        .DESCRIPTION
        Return data about the Azure AD subscription for a given tenant. Requires a global admin account. Not available to tenant-level admins.
 
        .EXAMPLE
        Get-NectarAzureADSubscription
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnads")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$ExportCertificate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }
            
            $URI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread/subscription?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            If ($ExportCertificate) {
                Try {
                    $JSON.data.msClientCertificateDto.certificate | Out-File TeamsCert.pem
                    $JSON.data.msClientCertificateDto.privateKey | Out-File TeamsPriv.key
                    Write-Host 'Successfully exported certificate to .\TeamsCert.pem and .\TeamsPriv.key'
                }
                Catch {
                    Get-JSONErrorStream -JSONResponse $_
                }
            }
            Else {
                Return $JSON.data
            }                
        }
        Catch {
            Write-Error "TenantName: $TenantName - No Azure AD information found or insufficient permissions."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarMSTeamsConfig {
    <#
        .SYNOPSIS
        Return information about an existing MS Teams call data integration with Nectar DXP
         
        .DESCRIPTION
        Return information about an existing MS Teams call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarMSTeamsConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing MS Teams configurations
        $TeamsURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr?tenant=$TenantName"
        $TeamsBody = (Invoke-RestMethod -Method GET -uri $TeamsURI -Headers $Global:NectarAuthHeader).data

        If ($TenantName) { $TeamsBody | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining

        Return $TeamsBody
    }
}



Function Get-NectarMSAzureConfig {
    <#
        .SYNOPSIS
        Return information about an existing MS Azure AD integration with Nectar DXP
         
        .DESCRIPTION
        Return information about an existing MS Azure AD integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarMSAzureConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing MS Teams configurations
        $AzureURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread?tenant=$TenantName"
        $AzureBody = (Invoke-RestMethod -Method GET -uri $AzureURI -Headers $Global:NectarAuthHeader).data

        If ($TenantName) { $AzureBody | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining

        Return $AzureBody
    }
}



Function Set-NectarMSTeamsConfig {
    <#
        .SYNOPSIS
        Modify an existing MS Teams call data integration with Nectar DXP
         
        .DESCRIPTION
        Modify an existing MS Teams call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD. Use either this or CertID, but not both.
         
        .PARAMETER CertID
        The Nectar DXP certificate ID. Use either this or MSClientSecret but not both. Obtain the cert ID by running Get-NectarMSTeamsCertificate.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER MTRMonitoring
        Enable/disable Microsoft Teams Room device monitoring
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarMSTeamsConfig -CertID
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$CertID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [bool]$MTRMonitoring,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$ConfigID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing MS Teams configurations
        $TeamsURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr?tenant=$TenantName"
        $AzureURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread?tenant=$TenantName"
        
        $TeamsBody = (Invoke-RestMethod -Method GET -uri $TeamsURI -Headers $Global:NectarAuthHeader).data
        $AzureBody = (Invoke-RestMethod -Method GET -uri $AzureURI -Headers $Global:NectarAuthHeader).data

        If ($ConfigID) { 
            $TeamsBody = $TeamsBody | Where-Object {$_.ID -eq $ConfigID} 
            $AzureBody = $AzureBody | Where-Object {$_.ID -eq $ConfigID} 
        }

        If ($TeamsBody.Count -gt 1) {
            Throw "Multiple Teams configurations detected. Please specify config ID. Use Get-NectarMSTeamsConfig to locate."
        }

        # Build body for updating
        If (!$MSTenantID) { $MSTenantID = $TeamsBody.MSTenantID }
        If (!$MSClientID) { $MSClientID = $TeamsBody.msClientID }
        If (!$MSClientSecret -and !$CertID) { $MSClientSecret = $TeamsBody.msClientSecret; $CertID = $TeamsBody.msClientCertificateId }
        
        $TeamsUpdateBody = @{
            tenant = $TenantName
            cloudAgentName             = 'cloudconnector_agent'
            displayName             = $TeamsBody.displayName
            msTenantId                 = $MSTenantID
            msClientId                 = $MSClientID
            msClientSecret             = $MSClientSecret
            msClientCertificateId    = $CertID
            kafkaTopic                 = $TeamsBody.kafkaTopic
            sourceId                 = $TeamsBody.sourceId
            loadDevices                = $MTRMonitoring
        }
        
        $AzureUpdateBody = @{
            tenant = $TenantName
            cloudAgentName = 'cloudconnector_agent'
            displayName = $AzureBody.displayName
            msTenantId = $MSTenantID
            msClientId = $MSClientID
            msClientSecret = $MSClientSecret
            msClientCertificateId = $CertID
            kafkaTopic = $AzureBody.kafkaTopic
            sourceId = $AzureBody.sourceId
            primaryAD = $AzureBody.isPrimary
        }
        
        If ($CertID -and !$MSClientSecret) {
            $TeamsUpdateBody.msClientSecret = $NULL
            $AzureUpdateBody.msClientSecret = $NULL
        }
        ElseIf ($MSClientSecret -and !$CertID) {
            $TeamsUpdateBody.msClientCertificateId = $NULL
            $AzureUpdateBody.msClientCertificateId = $NULL
        }
        
        $TeamsJSONBody = $TeamsUpdateBody | ConvertTo-Json
        $AzureJSONBody = $AzureUpdateBody | ConvertTo-Json
        
        $TeamsUpdateURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr/$($TeamsBody.id)?tenant=$TenantName"
        $AzureUpdateURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread/$($AzureBody.id)?tenant=$TenantName"
        
        Write-Verbose $TeamsUpdateURI
        Write-Verbose $TeamsJSONBody
        Write-Verbose $AzureUpdateURI
        Write-Verbose $AzureJSONBody
        
        If ($PSCmdlet.ShouldProcess(("Updating Nectar DXP MS Teams config on tenant {0}" -f $TenantName), ("Update Nectar DXP MS Teams config on tenant {0}?" -f $TenantName), 'Nectar DXP MS Teams Config Update')) {
            Try {
                Invoke-RestMethod -Method PUT -uri $TeamsUpdateURI -Headers $Global:NectarAuthHeader -Body $TeamsJSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                Get-JSONErrorStream -JSONResponse $_
            }
        
            Try {
                Invoke-RestMethod -Method PUT -uri $AzureUpdateURI -Headers $Global:NectarAuthHeader -Body $AzureJSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



Function New-NectarMSTeamsConfig {
    <#
        .SYNOPSIS
        Enables MS Teams call data and Azure AD integration with Nectar DXP
         
        .DESCRIPTION
        Enables MS Teams call data and Azure AD integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD. Use either this or CertID, but not both.
         
        .PARAMETER CertID
        The Nectar DXP certificate ID. Use either this or MSClientSecret but not both. Obtain the cert ID by running Get-NectarMSTeamsCertificate.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER MTRMonitoring
        Enable/disable Microsoft Teams Room device monitoring
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarMSTeamsConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$CertID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [bool]$MTRMonitoring,
        [Parameter(Mandatory=$False)]
        [switch]$AzureOnly,
        [Parameter(Mandatory=$False)]
        [switch]$TeamsOnly
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $TeamsURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr"
        $AzureURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread"
        
        # Get the cloud agent name
        $CloudAgentURI = "https://$Global:NectarCloud/aapi/cloudagents"
        $CloudAgentName = (Invoke-RestMethod -Method GET -uri $CloudAgentURI -Headers $Global:NectarAuthHeader)[0].cloudAgentName
        
        # Check for existing Teams/Azure config
        # Will increment the sourceID if one already exists
        [int]$TeamsSourceID = ((Get-NectarMSTeamsConfig).sourceID | Measure-Object -Maximum).Maximum + 1
        [int]$AzureSourceID = ((Get-NectarMSAzureConfig).sourceID | Measure-Object -Maximum).Maximum + 1
        
        # Build the JSON body for creating the config
        $TeamsBody = @{
            tenant             = $TenantName
            cloudAgentName    = $CloudAgentName
            displayName     = "$TenantName MS Teams"
            msTenantId         = $MSTenantID
            msClientId         = $MSClientID
            kafkaTopic         = 'msteams'
            sourceId         = $TeamsSourceID
            loadDevices     = $MTRMonitoring
        }
        
        $AzureBody = @{
            tenant             = $TenantName
            cloudAgentName    = $CloudAgentName
            displayName     = "$TenantName Azure AD"
            msTenantId         = $MSTenantID
            msClientId         = $MSClientID
            kafkaTopic         = 'azuread'
            sourceId         = $AzureSourceID
            primaryAD         = $TRUE
        }        

        If ($CertID) {
            $TeamsBody.Add('msClientCertificateId',$CertID)
            $AzureBody.Add('msClientCertificateId',$CertID)
        }
        Else {
            $TeamsBody.Add('msClientSecret',$MSClientSecret)
            $AzureBody.Add('msClientSecret',$MSClientSecret)            
        }
            
        $TeamsJSONBody = $TeamsBody | ConvertTo-Json
        $AzureJSONBody = $AzureBody | ConvertTo-Json
        
        Write-Verbose $TeamsURI
        Write-Verbose $TeamsJSONBody
        Write-Verbose $AzureURI
        Write-Verbose $AzureJSONBody
        
        If (!$AzureOnly) {
            $NULL = Invoke-RestMethod -Method POST -uri $TeamsURI -Headers $Global:NectarAuthHeader -Body $TeamsJSONBody -ContentType 'application/json; charset=utf-8'
        }
        
        If (!$TeamsOnly) {
            $NULL = Invoke-RestMethod -Method POST -uri $AzureURI -Headers $Global:NectarAuthHeader -Body $AzureJSONBody -ContentType 'application/json; charset=utf-8'
        }
    }
}



Function Get-NectarMSTeamsCertificate {
    <#
        .SYNOPSIS
        Returns information about a public/private key pair for Azure app certificate-based authentication
         
        .DESCRIPTION
        MS Azure allows for either client secret or certificate-based authentication for Azure applications. This shows the certificate details for a given tenant.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER CertID
        The Nectar DXP certificate ID. Typically starts at 1 and increments from there
         
        .PARAMETER CertFilePath
        Exports the certificate to a .cer file at the listed path
         
        .EXAMPLE
        Get-NectarMSTeamsCertificate -CertID 1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$CertID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CertFilePath
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/client/certificate?id=$CertID"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            
            If ($CertFilePath) {
                $JSON.data.certificate | Out-File -FilePath $CertFilePath
                Write-Host "Certificate file written to $CertFilePath"
            }
            
            Return $JSON.data
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarMSTeamsCertificate {
    <#
        .SYNOPSIS
        Creates a new public/private key pair for Azure app certificate-based authentication
         
        .DESCRIPTION
        MS Azure allows for either client secret or certificate-based authentication for Azure applications. This command will create a public/private key pair to use for this purpose.
        It will create the private key, associate it with the Nectar DXP tenant and present the public portion to be added to the MS Azure app. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER CertDisplayName
        The display name to assign to the certificate
         
        .PARAMETER ValidityPeriod
        The number of days the certificate should be valid for. Defaults to 365 days (1 year)
         
        .EXAMPLE
        New-NectarMSTeamsCertificate -CertDisplayName 'CompanyName Certificate for Nectar DXP' -ValidityPeriod 730
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CertDisplayName = 'Nectar DXP Certificate for Azure App',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$ValidityPeriod = 365
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$CertDisplayName) { }
            $URI = "https://$Global:NectarCloud/aapi/client/certificate?name=$CertDisplayName&validityDays=$ValidityPeriod"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON.data
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarMSTeamsCertificate {
    <#
        .SYNOPSIS
        Removes a previously created public/private key pair used for MS Teams Azure app authentication
         
        .DESCRIPTION
        MS Azure allows for either client secret or certificate-based authentication for Azure applications. This command will delete a previously created public/private key pair.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER CertID
        The Nectar DXP certificate ID. Typically starts at 1 and increments from there
         
        .EXAMPLE
        Remove-NectarMSTeamsCertificate -CertID 1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$CertID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/client/certificate?id=$CertID"
            Write-Verbose $URI
            $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarMSTeamsPartialMonitoringUsers {
    <#
        .SYNOPSIS
        Return a list of users who are currently manually assigned to Teams Partial User Monitoring
         
        .DESCRIPTION
        Return a list of users who are currently manually assigned to Teams Partial User Monitoring. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .EXAMPLE
        Get-NectarMSTeamsPartialMonitoringUsers
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $URI = "https://$Global:NectarCloud/aapi/client/teams/partial-monitoring?tenant=$TenantName"
            Write-Verbose $URI
            
            $Params = @{
                'pageSize' = $PageSize
            }

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
            
            If ($TotalPages -gt 1) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "&pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Headers $Global:NectarAuthHeader -Body $Params
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results."
        }
    }
}



Function Add-NectarMSTeamsPartialMonitoringUser {
    <#
        .SYNOPSIS
        Adds a user account to the Teams Partial User Monitoring list
         
        .DESCRIPTION
        Adds a user account to the Teams Partial User Monitoring list
         
        .PARAMETER UserPrincipalName
        The UPN of the user account(s) to add. Separate multiple UPNs with comma
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Add-NectarMSTeamsPartialMonitoringUser tferguson@contoso.com
        Adds tferguson@nectarcorp.com to the Teams Partial Monitoring User list
 
        .EXAMPLE
        Add-NectarMSTeamsPartialMonitoringUser tferguson@contoso.com,tjones@contoso.com
        Adds tferguson@nectarcorp.com and tjones@contoso.com to the Teams Partial Monitoring User list
 
        .EXAMPLE
        Import-Csv PartialUserList.csv | Add-NectarMSTeamsPartialMonitoringUser
        Adds the contents of a CSV file called PartialUserList.csv to the Teams Partial Monitoring User list.
        The CSV to import must have UserPrincipalName as the column header and include a list of UPNs
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string[]]$UserPrincipalName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
        $SourceID = (Get-NectarMSTeamsConfig -Tenant $TenantName).SourceID
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        $URI = "https://$Global:NectarCloud/aapi/client/teams/partial-monitoring?tenant=$TenantName"

        Write-Verbose $URI
        
        ForEach ($UPN in $UserPrincipalName) {
            $Body = @{
                userPrincipalName = $UPN
                sourceId = $SourceID
            }
            
            $JSONBody = $Body | ConvertTo-Json

            Try {
                $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
                Write-Verbose $JSONBody
            }
            Catch {
                Write-Error "Unable to add user $UPN."
                Get-JSONErrorStream -JSONResponse $_

                # Check to see if the user exists in Azure. Make sure the secret exists and is accessible to the user before checking.
                If ((Get-NectarMSTeamsConfig).msClientSecret) {
                    $AzureUserInfo = Get-MSAzureUser -UPN $UPN -ErrorAction SilentlyContinue
                    If ($AzureUserInfo) {
                        Write-Warning "$UPN exists in Azure AD. This means there is an issue with the Azure AD sync within Nectar DXP."
                    }
                    Else {
                        Write-Warning "$UPN does not exist in Azure AD. Please check the name."
                    }
                }
            }
        }
    }
}



Function Remove-NectarMSTeamsPartialMonitoringUser {
    <#
        .SYNOPSIS
        Removes a user account from the Teams Partial User Monitoring list
         
        .DESCRIPTION
        Removes a user account from the Teams Partial User Monitoring list
         
        .PARAMETER UserPrincipalName
        The UPN of the user account(s) to remove. Separate multiple UPNs with comma
 
        .PARAMETER UserID
        The GUID of the user account(s) to remove. Separate multiple GUIDs with comma
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarMSTeamsPartialMonitoringUser tferguson@contoso.com,tjones@contoso.com
        Removes tferguson@contoso.com and tjones@contoso.com from the Partial User Monitoring list
 
        .EXAMPLE
        Get-NectarMSTeamsPartialMonitoringUser | Remove-NectarMSTeamsPartialMonitoringUser
        Removes all users from the Teams Partial User Monitoring list
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserPrincipalName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
        $UserList = Get-NectarMSTeamsPartialMonitoringUsers 
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { 
            $TenantName = $Global:NectarTenantName 
        }

        If ($UserID) {
            ForEach ($ID in $UserID) {
                $URI = "https://$Global:NectarCloud/aapi/client/teams/partial-monitoring/$($ID)?sourceId=$($UserList[0].SourceID)&tenant=$TenantName"
                Write-Verbose $URI
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
            }
        }
        ElseIf ($UserPrincipalName) {
            ForEach ($UPN in $UserPrincipalName) {
                $UserDeleteList = $UserList | Where-Object {$_.UserPrincipalName -eq $UPN}
                
                If (!$UserDeleteList)  {
                    Throw "Could not find a user with UPN $UPN"
                }

                Try {
                    ForEach ($UserDelete in $UserDeleteList) {
                        $URI = "https://$Global:NectarCloud/aapi/client/teams/partial-monitoring/$($UserDelete.UserId)?sourceId=$($UserDelete.SourceID)&tenant=$TenantName"
                        Write-Verbose $URI
                        $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
                    }
                }
                Catch {
                    Write-Error "Unable to remove user $UPN."
                    Get-JSONErrorStream -JSONResponse $_
                }
            }
        }
    }
}



Function Set-NectarMSTeamsPartialMonitoringLimit {
    <#
        .SYNOPSIS
        Configure a limit to the number of users that are monitored in Nectar DXP
         
        .DESCRIPTION
        Configure a limit to the number of users that are monitored in Nectar DXP. Uses membership in a specified Azure AD group to determine the users we pull call records.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER MaxUsers
        The maximum number of users that will be monitored. Any group members above MaxUsers will not be monitored.
 
        .PARAMETER GroupName
        The display name of the group that contains the users who will be monitored. If the group contains spaces, enclose the name in quotes
 
        .PARAMETER SkipValidation
        Does not attempt to validate the existence of the Azure AD group before enabling. Use with caution.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarMSTeamsUserMonitoringLimit -MaxUsers 1000 -GroupName N10Users -TenantName contoso
        Sets the maximum number of monitored users on the Contoso tenant to 1000 and uses the members of the N10Users group as the list of users to be monitored
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("Set-NectarMSTeamsUserMonitoringLimit")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateRange(1,999999)]
        [int]$MaxUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$GroupName,
        [Parameter(Mandatory=$False)]
        [bool]$Enabled = $True,
        [Parameter(Mandatory=$False)]
        [switch]$SkipValidation,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        If ($Enabled -And $GroupName -And $GroupName -ne 'Unknown' -And !$SkipValidation) {
            # Validate the group exists in Azure AD
            $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
        
            $URI = "https://graph.microsoft.com/v1.0/groups?`$filter=displayName eq '$GroupName'"
            Write-Verbose $URI
            
            Try {
                Write-Host 'Validating group...'
                $GroupData = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                
                If ($GroupData.Value.Count -eq 1) {
                    Write-Host "Found Azure AD group " -NoNewLine
                    Write-Host $GroupName -ForegroundColor Green
                    $CountURI = "https://graph.microsoft.com/v1.0/groups/$($GroupData.Value.id)/transitivemembers/microsoft.graph.user/$count"
                    
                    # Add header
                    $Headers.Add('ConsistencyLevel', 'eventual')
                    Write-Verbose $CountURI
                    $MemberCount = Invoke-RestMethod -Method GET -uri $CountURI -Headers $Headers
                    $TotalCount = $MemberCount.value.count
                    
                    While ($MemberCount.'@odata.nextLink') {
                        $NextURI = $MemberCount.'@odata.nextLink'
                        $MemberCount = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $TotalCount += $MemberCount.value.count
                    }
                    
                    Write-Host "Group has $TotalCount members"
                }
                Else {
                    Write-Host "No group called '$GroupName' could be found in Azure AD" -ForegroundColor Red
                    Break
                }
            }
            Catch {
                Write-Host "There was an error while attempting to locate the group $GroupName in Azure AD" -ForegroundColor Red
                Break
            }
        }
        ElseIf ($Enabled -And ($GroupName -eq '' -Or $GroupName -eq 'Unknown')) {
            $GroupName = 'Unknown'
        }

        # Get the MS Teams Config ID for constructing the UrlEncode
        $MSTeamsConfigID = (Get-NectarMSTeamsSubscription).msTeamsConfigId
        
        $URI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr/partialmonitoring?id=$MSTeamsConfigID"
        
        Write-Verbose $URI
        
        $Body = @{
            tenant = $TenantName
            enabled = $Enabled
            azureAdGroup = $GroupName
            maxUsers = $MaxUsers
        }
        
        $JSONBody = $Body | ConvertTo-Json
        
        Write-Verbose $JSONBody

        Try {
            $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            If ($Enabled) {
                Write-Host "Successfully enabled Teams partial user monitoring on " -NoNewLine
                Write-Host $TenantName -NoNewLine -ForegroundColor Green 
                Write-Host " for up to " -NoNewLine
                Write-Host $MaxUsers -NoNewLine -ForegroundColor Green
                If ($GroupName -ne 'Unknown') {
                    Write-Host " users using group named " -NoNewLine
                    Write-Host $GroupName -NoNewLine -ForegroundColor Green
                } 
                Else {
                    Write-Host " manually managed users."
                }
            } 
            Else {
                Write-Host "Successfully disabled Teams partial user monitoring on " -NoNewLine
                Write-Host $TenantName -ForegroundColor Green
            }
        }
        Catch {
            Write-Error "Error while setting parameters"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-MSTeamsCallRecord {
    <#
        .SYNOPSIS
        Return a specific call record details directly from MS Azure tenant.
         
        .DESCRIPTION
        Return a specific call record details directly from MS Azure tenant.
         
        .PARAMETER CallRecordID
        The MS call record ID
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-MSTeamsCallRecord -CallRecordID ed672235-5417-40ce-8425-12b8b702a505 -AuthToken $AuthToken
        Returns the given MS Teams call record using a previously-obtained authtoken
         
        .EXAMPLE
        Get-MSTeamsCallRecord -CallRecordID ed672235-5417-40ce-8425-12b8b702a505 -AuthToken $AuthToken -TextOutput | Out-File Call.json
        Returns the given MS Teams call record using a previously-obtained authtoken and saves the results to a .JSON file.
 
        .NOTES
        Version 1.3
    #>

    
    [Alias("gmtcr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("calldCallRecordId")]
        [string]$CallRecordID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [switch]$TextOutput,
        [Parameter(Mandatory=$False)]
        [switch]$BetaCallRecord
    )
    
    Begin {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Where-Object {$_.SubscriptionStatus -eq 'ACTIVE'} | Get-MSGraphAccessToken }    
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
    Process {
        Try {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            # Change case to lower
            $CallRecordID = $CallRecordID.ToLower()

            If ($BetaCallRecord) {
                $URI = "https://graph.microsoft.com/beta/communications/callRecords/$CallRecordID/?`$expand=sessions(`$expand=segments)"
            }
            Else {
                $URI = "https://graph.microsoft.com/v1.0/communications/callRecords/$CallRecordID/?`$expand=sessions(`$expand=segments)"
            }
            
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            
            If ($JSON.'sessions@odata.nextLink') {
                $NextURI = $JSON.'sessions@odata.nextLink'
                Do  {
                    Write-Verbose "Getting next session page: $NextURI"
                    $NextJSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                    $JSON.sessions += $NextJSON.value
                    Write-Verbose "Session count: $($NextJSON.value.count)"
                    $NextURI = $NextJSON.'@odata.nextLink'
                } Until (!$NextURI)
            }
            
            If ($TextOutput) {
                $JSON = $JSON | ConvertTo-Json -Depth 8
            }

            Return $JSON

        }
        Catch {
            Write-Error "Could not get call record."
            Write-Error $_
        }
    }
}



Function Get-MSTeamsPSTNCalls {
    <#
        .SYNOPSIS
        Return a list of MS Teams PSTN calls
         
        .DESCRIPTION
        Return a list of MS Teams PSTN calls
         
        .PARAMETER FromDateTime
        Start of time range to query. UTC, inclusive. Time range is based on the call start time. Defaults to today
 
        .PARAMETER ToDateTime
        End of time range to query. UTC, inclusive. Defaults to today
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsPSTNCalls -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Returns all Teams PSTN calls for the specified tenant/clientID/secret combination
         
        .EXAMPLE
        Get-MSTeamsPSTNCalls -AuthToken $AuthToken
        Returns all Teams PSTN calls using a previously obtained authtoken
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [datetime]$FromDateTime = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'),
        [Parameter(Mandatory=$False)]
        [datetime]$ToDateTime = ((Get-Date).AddDays(+1).ToString('yyyy-MM-ddTHH:mm:ssZ')),        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize
    )

    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($FromDateTime) { [string]$FromDateTimeString = $FromDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ') }
        If ($ToDateTime) { [string]$ToDateTimeString = $ToDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ') }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://graph.microsoft.com/v1.0/communications/callRecords/getPstnCalls(fromDateTime=$FromDateTimeString,toDateTime=$ToDateTimeString)"
            
            Write-Verbose $URI
            
            $Params = @{}
            If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.value
            
            $PageSize = $JSON.value.count
            
            While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                $NextURI = $JSON.'@odata.nextLink'
                $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                $JSON.value
                $PageSize += $JSON.value.count
            }
            Clear-Variable -Name AuthToken
        }
    }
}



Function Get-MSTeamsDRCalls {
    <#
        .SYNOPSIS
        Return a list of MS Teams Direct Routing calls
         
        .DESCRIPTION
        Return a list of MS Teams Direct Routing calls
         
        .PARAMETER FromDateTime
        Start of time range to query. UTC, inclusive. Time range is based on the call start time. Defaults to today
 
        .PARAMETER ToDateTime
        End of time range to query. UTC, inclusive. Defaults to today
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsDRCalls -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Returns all Teams DR calls for the specified tenant/clientID/secret combination
         
        .EXAMPLE
        Get-MSTeamsDRCalls -AuthToken $AuthToken
        Returns all Teams DR calls using a previously obtained authtoken
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [datetime]$FromDateTime = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'),
        [Parameter(Mandatory=$False)]
        [datetime]$ToDateTime = ((Get-Date).AddDays(+1).ToString('yyyy-MM-ddTHH:mm:ssZ')),        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize
    )

    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($FromDateTime) { [string]$FromDateTimeString = $FromDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ') }
        If ($ToDateTime) { [string]$ToDateTimeString = $ToDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ') }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://graph.microsoft.com/v1.0/communications/callRecords/getDirectRoutingCalls(fromDateTime=$FromDateTimeString,toDateTime=$ToDateTimeString)"
            
            Write-Verbose $URI
            
            $Params = @{}
            If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.value
            
            $PageSize = $JSON.value.count
            
            While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                $NextURI = $JSON.'@odata.nextLink'
                $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                $JSON.value
                $PageSize += $JSON.value.count
            }
            Clear-Variable -Name AuthToken
        }
    }
}



Function Import-MSTeamsCallRecord {
    <#
        .SYNOPSIS
        Import one or more MS Teams call records into Nectar DXP
         
        .DESCRIPTION
        Import one or more MS Teams call records into Nectar DXP
         
        .PARAMETER CallRecordID
        An array of Microsoft call record IDs to import
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Import-MSTeamsCallRecord -CallRecordID aac8d44c-e5f6-4d50-91a0-235d4a9631c9
        Imports a single call record into Nectar DXP
         
        .EXAMPLE
        Import-MSTeamsCallRecord -CallRecordID $CallRecordList
        Imports an array of MS Teams call records into Nectar DXP
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallRecordID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )

    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/command/cdr/reload-by-id"

        # Get the cloud agent name
        $CloudAgentURI = "https://$Global:NectarCloud/aapi/cloudagents"
        $CloudAgentName = (Invoke-RestMethod -Method GET -uri $CloudAgentURI -Headers $Global:NectarAuthHeader)[0].cloudAgentName
        
        # Get the sourceID
        $SourceID = (Get-NectarMSTeamsConfig).sourceID
        
        $Body = @{
            tenant             = $TenantName
            cloudAgentName    = $CloudAgentName
            commandName     = 'ReloadMsTeamsCdrByIdCommand'
            sourceId         = $SourceID
            uuids             = $CallRecordID
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Write-Verbose $URI
        Write-Verbose $JSONBody
        
        Try {
            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Return $JSON.data
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-MSTeamsUser {
    <#
        .SYNOPSIS
        Return a list of Teams user information from Teams admin API.
         
        .DESCRIPTION
        Return a list of Teams user information from Teams admin API.. Azure app requires the 'Skype and Teams Tenant Admin API/application_access' permission for this to work
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER PageSize
        The number of results to return in a page. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsUser
        Returns all MS Teams users
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,1000)]
        [int]$PageSize = 1000
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken -Scope TeamsPS }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization         = "Bearer $AuthToken"
                }
        
                $Body = @{
                    #'$filter' = 'accountEnabled eq true'
                    '$select'    = 'displayName,userPrincipalName,sipAddress,objectId,accountEnabled,lineUri,onPremEnterpriseVoiceEnabled,onlineVoiceRoutingPolicy,enterpriseVoiceEnabled,pstnConnectivity,isResourceAccount'
                    'pagesize'    = $PageSize
                }

                $URI = "https://api.interfaces.records.teams.microsoft.com/Teams.User/users"
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.users
                
                While ($JSON.'@nextLink') {
                    $NextURI = $JSON.'@nextLink'
                    $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                    $JSON.users
                }

                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get user data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}




Function Get-MSTeamsCallQueue {
    <#
        .SYNOPSIS
        Return a list of Teams call queue information from Teams admin API.
         
        .DESCRIPTION
        Return a list of Teams call queue information from Teams admin API. Azure app requires the 'Skype and Teams Tenant Admin API/application_access' permission for this to work
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER PageSize
        The number of results to return in a page. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsCallQueue
        Returns all MS Teams call queues
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,1000)]
        [int]$PageSize = 1000
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken -Scope TeamsPS }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization         = "Bearer $AuthToken"
                }
        
                $Body = @{
                    'FilterInvalidObos'    = 'False'
                    'pagesize'            = $PageSize
                }

                $URI = "https://api.interfaces.records.teams.microsoft.com/Teams.VoiceApps/callqueues"
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.CallQueues
                
                While ($JSON.'@nextLink') {
                    $NextURI = $JSON.'@nextLink'
                    $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                    $JSON.CallQueues
                }

                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get CallQueues data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}




Function Get-MSTeamsAutoAttendant {
    <#
        .SYNOPSIS
        Return a list of Teams call queue information from Teams admin API.
         
        .DESCRIPTION
        Return a list of Teams call queue information from Teams admin API. Azure app requires the 'Skype and Teams Tenant Admin API/application_access' permission for this to work
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER PageSize
        The number of results to return in a page. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsCallQueue
        Returns all MS Teams call queues
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,1000)]
        [int]$PageSize = 1000
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken -Scope TeamsPS }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization         = "Bearer $AuthToken"
                }
        
                $Body = @{
                    'pagesize'            = $PageSize
                }

                $URI = "https://api.interfaces.records.teams.microsoft.com/Teams.VoiceApps/auto-attendants"
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.AutoAttendants
                
                While ($JSON.'@nextLink') {
                    $NextURI = $JSON.'@nextLink'
                    $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                    $JSON.AutoAttendants
                }

                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get AutoAttendants data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureUser {
    <#
        .SYNOPSIS
        Return a list of users from Azure AD.
         
        .DESCRIPTION
        Return a list of users from Azure AD.
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties
         
        .PARAMETER UPN
        Return user details for the given UserPrincipalName
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TotalCount
        Only return a total count of objects
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSAzureUser
        Returns all MS Azure user accounts
         
        .EXAMPLE
        Get-MSAzureUser -Properties 'id,userPrincipalName' -AuthToken $AuthToken
        Returns a list of Azure users' ID and userPrincipalNames using a previously-obtained authtoken
         
        .EXAMPLE
        Get-MSAzureUser -Filter "startswith(displayName,'Meeting Room')"
        Returns a list of Azure users whose display names start with 'Meeting Room'
 
        .NOTES
        Version 1.3
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,
        [Parameter(Mandatory=$False)]
        [string]$UPN,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize = 200000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1    
    )

    Process {
        $Body = @{'$count' = 'true'}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization         = "Bearer $AuthToken"
                    ConsistencyLevel     = 'eventual'
                }

                # If UPN is entered, just look for that user and exit.
                If ($UPN) {
                    $URI = "https://graph.microsoft.com/v1.0/users/$UPN"
                    Write-Verbose $URI
    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

                    Return $JSON
                }
                
                If ($Properties) { $Body.Add('$select',$Properties)    }
                If ($Filter) { $Body.Add('$filter',$Filter) }

                If ($TotalCount) {
                    $URI = 'https://graph.microsoft.com/v1.0/users/$count'
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $UserCount = New-Object PsObject
                    $UserCount | Add-Member -NotePropertyName 'UserCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $UserCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $UserCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/users"
                    
                    Write-Verbose $URI
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $TotalUsers = $JSON.'@odata.count'
                    
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    $Message = "Getting $TotalUsers Azure AD users for tenant $TenantName." 
                    
                    If ($HideProgressBar) { Write-Host $Message }
                    
                    $UserCount = 0
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                        
                        If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                            $Percentage = ($PageSize / $TotalUsers) * 100
                                                    
                            If ($HideProgressBar) {
                                $Percentage = [math]::Round($Percentage,1)
                                Write-Host "Retrieving $Percentage`% of $($TotalUsers) users on tenant $TenantName..."
                            }
                            Else { 
                                Write-Progress -Activity $Message -PercentComplete $Percentage -Status 'Retrieving...' 
                            }
                        
                            $Stopwatch.Reset()
                            $Stopwatch.Start()
                        }
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get user data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureUserGroupMembership {
    <#
        .SYNOPSIS
        Return the groups that a user is a member of from Azure AD.
         
        .DESCRIPTION
        Return the groups that a user is a member of from Azure AD.
         
        .PARAMETER ID
        The Azure AD GUID of the user
         
        .PARAMETER Transitive
        Show groups where the user is a member of a group that is a member of another group
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TotalCount
        Only return a total count of objects
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-MSAzureUserGroupMembership -ID abcdefab-1234-1234-1234-abcdabcdabcd
        Returns the groups that the selected user is a member of
         
        .EXAMPLE
        Get-MSAzureUser -Properties 'id,userPrincipalName' -AuthToken $AuthToken
        Returns a list of Azure users' ID and userPrincipalNames using a previously-obtained authtoken
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ID,    
        [Parameter(Mandatory=$False)]
        [switch]$Transitive,
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1    
    )
    
    Begin {
        If ($Transitive) {
            $MemberOfScope = 'transitiveMemberOf'
        }
        Else {
            $MemberOfScope = 'MemberOf'
        }
    }
    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = "https://graph.microsoft.com/v1.0/users/$ID/$MemberOfScope/`$count"
                    Write-Verbose $URI
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $GroupCount = New-Object PsObject
                    $GroupCount | Add-Member -NotePropertyName 'UserCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $GroupCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $GroupCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/users/$ID/$MemberOfScope"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get group membership data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureGroup {
    <#
        .SYNOPSIS
        Return a list of groups from Azure AD.
         
        .DESCRIPTION
        Return a list of groups from Azure AD.
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TotalCount
        Only return a total count of objects
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSAzureGroup
        Returns all MS Azure groups
         
        .EXAMPLE
        Get-MSAzureGroup -Filter "startsWith(displayName,'Global-')"
        Returns all MS Azure groups whose display names start with Global-
         
        .EXAMPLE
        Get-MSAzureGroup -Properties 'id,DisplayName' -AuthToken $AuthToken
        Returns a list of Azure group ID and display names using a previously-obtained authtoken
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1    
    )

    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = 'https://graph.microsoft.com/v1.0/groups/$count'
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $GroupCount = New-Object PsObject
                    $GroupCount | Add-Member -NotePropertyName 'GroupCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $GroupCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $GroupCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/groups"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $TotalGroups = Get-MSAzureGroup @Params -TotalCount
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    $Message = "Getting Azure AD groups for tenant $TenantName." 
                    
                    If ($HideProgressBar) { Write-Host $Message }
                    
                    $GroupCount = 0
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                        
                        If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                            $Percentage = ($PageSize / $TotalGroups.GroupCount) * 100
                                                    
                            If ($HideProgressBar) {
                                $Percentage = [math]::Round($Percentage,1)
                                Write-Host "Retrieving $Percentage`% of $($TotalGroups.GroupCount) users on tenant $TenantName..."
                            }
                            Else { 
                                Write-Progress -Activity $Message -PercentComplete $Percentage -Status 'Retrieving...' 
                            }
                        
                            $Stopwatch.Reset()
                            $Stopwatch.Start()
                        }
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get group data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureGroupMembers {
    <#
        .SYNOPSIS
        Return a list of group members from Azure AD.
         
        .DESCRIPTION
        Return a list of group members from Azure AD.
         
        .PARAMETER ID
        The GUID of the group to return membership info
     
        .PARAMETER Transitive
        Show members where the members are members of a group that is a member of another group
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-MSAzureGroup -Filter "startsWith(displayName,'Global-Sales')" | Get-MSAzureGroupMembers -TotalOnly
        Returns all MS Azure groups whose display names start with Global-Sales
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ID,
        [Parameter(Mandatory=$False)]
        [switch]$Transitive,
        [Parameter(Mandatory=$False)]
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,    
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize = 10000
    )

    Begin {
        If ($Transitive) {
            $MemberScope = 'transitiveMembers'
        }
        Else {
            $MemberScope = 'members'
        }
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
    Process {
        $Body = @{}
        $Params = @{}
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = "https://graph.microsoft.com/v1.0/groups/$ID/$MemberScope/`$count"
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $MemberCount = New-Object PsObject
                    $MemberCount | Add-Member -NotePropertyName 'MemberCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $MemberCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $MemberCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/groups/$ID/$MemberScope"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get group member data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsServiceStatus {
    <#
        .SYNOPSIS
        Return the current MS Teams service status from Office 365
         
        .DESCRIPTION
        Return the current MS Teams service status from Office 365. Requires Graph API permission ServiceHealth.Read.All
         
        .PARAMETER CallRecordID
        The MS call record ID
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-MSTeamsServiceStatus
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
    }    
    Process {
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                $URI = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews/Microsoft Teams"
                
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

                Clear-Variable -Name AuthToken

                Return $JSON

            }
            Catch {
                Write-Error $_.Exception.Response.StatusDescription
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsServiceMessage {
    <#
        .SYNOPSIS
        Return any current service status messages regarding MS Teams cloud issues
         
        .DESCRIPTION
        Return any current service status messages regarding MS Teams cloud issues. Requires Graph API permission ServiceHealth.Read.All
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-MSTeamsServiceMessage
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('advisory','incident','unknownFutureValue', IgnoreCase=$True)]
        [string]$Classification,
        [Parameter(Mandatory=$False)]
        [ValidateSet('serviceOperational','investigating','restoringService','verifyingService','serviceRestored','postIncidentReviewPublished','serviceDegradation','serviceInterruption','extendedRecovery','falsePositive','investigationSuspended','resolved','mitigatedExternal','mitigated','resolvedExternal','confirmed','reported','unknownFutureValue', IgnoreCase=$True)]
        [string]$Status,        
        [Parameter(Mandatory=$False)]
        [switch]$Current
    )

    Begin {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
    }    
    Process {
        If ($AuthToken) {    
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }
                
                $URI = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/issues?`$filter=Service eq 'Microsoft Teams'"                
                
                If ($Current) { $URI = $URI + " and isResolved eq false" }
                
                If ($Classification) { $URI = $URI + " and Classification eq '$Classification'" }
                
                If ($Status) { $URI = $URI + " and Status eq '$Status'" }
                
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                
                Clear-Variable -Name AuthToken

                Return $JSON.value
            }
            Catch {
                Write-Error $_.Exception.Response.StatusDescription
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsLicenseStatus {
    <#
        .SYNOPSIS
        Return the total number of SKUs for a company that have Teams in it
         
        .DESCRIPTION
        Return the total number of SKUs for a company that have Teams in it. Requires Organization.Read.All permission on graph.microsoft.com
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-MSTeamsLicenseStatus
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token for tenant $TenantName."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                $URI = "https://graph.microsoft.com/v1.0/subscribedSkus"
                
                Write-Verbose $URI

                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

                $TotalSKUs = $JSON.value

                $LicenseCount = $TotalSKUs | Where-Object {$_.servicePlans.ServicePlanName -eq 'TEAMS1'} | Select-Object skuPartNumber, ConsumedUnits
                
                If ($TotalOnly) {
                    $LicenseCount = $LicenseCount | Measure-Object -Property consumedUnits -Sum | Select-Object Sum
                }

                If ($TenantName) { $LicenseCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                
                Clear-Variable -Name AuthToken
                
                Return $LicenseCount
            }
            Catch {
                Write-Error "Could not get license data for tenant $TenantName."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsUserLicenseCount {
    <#
        .SYNOPSIS
        Return the total number of Teams licensed users for a given tenant.
         
        .DESCRIPTION
        Return the total number of Teams licensed users for a given tenant. Excludes disabled accounts.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER ProgressUpdateFreq
        How frequently to show progress updates, in seconds. Defaults to 1 second
         
        .PARAMETER UserList
        Use a previously obtained list of Azure user IDs. If this isn't specified, then a list will be downloaded using Get-MSAzureUser
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TestMode
        Sets the mode to run the tests under. Choose from Fast, Basic or Extended. Defaults to Fast mode
        Extended mode checks user O365 Service Plans for both Teams and Enterprise Voice functionality. Most accurate, but slower because it requires pulling
        down about 10x the data. The process ends up taking about twice as long as Basic mode.
        Basic mode looks at the assigned license SKUs, and also checks to see if TEAMS1 has been disabled. Faster because it requires about 10x less data than
        Extended mode, but can't obtain information about Enterprise Voice functionality. It's twice as fast as Extended mode, but only returns a total Teams count.
        It doesn't return the Enterprise Voice users.
        Fast mode just returns a total count of assigned licenses. It doesn't check for users with a license but has Teams explicitly disabled. This may over-count, but historically it amounts to a miniscule fraction of total users.
 
        .PARAMETER ExportCSV
        Exports the list of users and the assigned Teams SKU to a CSV file. Requires Basic or Extended test mode
 
        .EXAMPLE
        Get-MSTeamsUserLicenseCount -TenantName contoso
 
        .NOTES
        Version 1.3
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1,
        [Parameter(Mandatory=$False)]
        $UserList,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Fast','Basic','Extended', IgnoreCase=$True)]
        [string]$TestMode = 'Fast',
        [Parameter(Mandatory=$False)]
        [string]$ExportCSV
        
    )
    
    Begin {
        $TeamsSKUList = @(
            [PSCustomObject]@{SKU = 'e4654015-5daf-4a48-9b37-4f309dddd88b'; Name = 'Microsoft Teams Enterprise'; ShortCode = 'TEAMS_ADVCOMMS' }
            [PSCustomObject]@{SKU = '4b590615-0888-425a-a965-b3bf7789848d'; Name = 'Microsoft 365 Education A3 for Faculty'; ShortCode = 'M365EDU_A3_FACULTY' }
            [PSCustomObject]@{SKU = '7cfd9a2b-e110-4c39-bf20-c6a3f36a3121'; Name = 'Microsoft 365 Education A3 for Students'; ShortCode = 'M365EDU_A3_STUDENT' }
            [PSCustomObject]@{SKU = '18250162-5d87-4436-a834-d795c15c80f3'; Name = 'Microsoft 365 Education A3 for Students Use Benefit'; ShortCode = 'M365EDU_A3_STUUSEBNFT' }
            [PSCustomObject]@{SKU = '1aa94593-ca12-4254-a738-81a5972958e8'; Name = 'Microsoft 365 A3 - Unattended License for students use benefit'; ShortCode = 'M365EDU_A3_STUUSEBNFT_RPA1' }
            [PSCustomObject]@{SKU = 'e97c048c-37a4-45fb-ab50-922fbf07a370'; Name = 'Microsoft 365 Education A5 for Faculty'; ShortCode = 'M365EDU_A5_FACULTY' }
            [PSCustomObject]@{SKU = '46c119d4-0379-4a9d-85e4-97c66d3f909e'; Name = 'Microsoft 365 Education A5 for Students'; ShortCode = 'M365EDU_A5_STUDENT' }
            [PSCustomObject]@{SKU = '31d57bc7-3a05-4867-ab53-97a17835a411'; Name = 'Microsoft 365 A5 for students use benefit'; ShortCode = 'M365EDU_A5_STUUSEBNFT' }
            [PSCustomObject]@{SKU = '81441ae1-0b31-4185-a6c0-32b6b84d419f'; Name = 'Microsoft 365 A5 without Audio Conferencing for students use benefit'; ShortCode = 'M365EDU_A5_NOPSTNCONF_STUUSEBNFT' }
            [PSCustomObject]@{SKU = '3b555118-da6a-4418-894f-7df1e2096870'; Name = 'Microsoft 365 Business Basic'; ShortCode = 'O365_BUSINESS_ESSENTIALS' }
            [PSCustomObject]@{SKU = 'dab7782a-93b1-4074-8bb1-0e61318bea0b'; Name = 'Microsoft 365 Business Basic'; ShortCode = 'SMB_BUSINESS_ESSENTIALS' }
            [PSCustomObject]@{SKU = 'f245ecc8-75af-4f8e-b61f-27d8114de5f3'; Name = 'Microsoft 365 Business Standard'; ShortCode = 'O365_BUSINESS_PREMIUM' }
            [PSCustomObject]@{SKU = 'ac5cef5d-921b-4f97-9ef3-c99076e5470f'; Name = 'Microsoft 365 Business Standard'; ShortCode = 'SMB_BUSINESS_PREMIUM' }
            [PSCustomObject]@{SKU = 'cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46'; Name = 'Microsoft 365 Business Premium'; ShortCode = 'SPB' }
            [PSCustomObject]@{SKU = '05e9a617-0261-4cee-bb44-138d3ef5d965'; Name = 'Microsoft 365 E3'; ShortCode = 'SPB_E3' }
            [PSCustomObject]@{SKU = 'c2ac2ee4-9bb1-47e4-8541-d689c7e83371'; Name = 'Microsoft 365 E3 - Unattended License'; ShortCode = 'SPE_E3_RPA1' }
            [PSCustomObject]@{SKU = '0c21030a-7e60-4ec7-9a0f-0042e0e0211a'; Name = 'Microsoft 365 E3 (500 seats min)_HUB'; ShortCode = 'Microsoft_365_E3' }
            [PSCustomObject]@{SKU = '06ebc4ee-1bb5-47dd-8120-11324bc54e06'; Name = 'Microsoft 365 E5'; ShortCode = 'SPB_E5' }
            [PSCustomObject]@{SKU = 'a91fc4e0-65e5-4266-aa76-4037509c1626'; Name = 'Microsoft 365 E5 with Calling Minutes'; ShortCode = 'SPE_E5_CALLINGMINUTES' }
            [PSCustomObject]@{SKU = 'c42b9cae-ea4f-4ab7-9717-81576235ccac'; Name = 'Microsoft 365 E5 Developer (without Windows and Audio Conferencing)'; ShortCode = 'DEVELOPERPACK_E5' }
            [PSCustomObject]@{SKU = 'cd2925a3-5076-4233-8931-638a8c94f773'; Name = 'Microsoft 365 E5 without Audio Conferencing'; ShortCode = 'SPE_E5_NOPSTNCONF' }
            [PSCustomObject]@{SKU = '44575883-256e-4a79-9da4-ebe9acabe2b2'; Name = 'Microsoft 365 F1'; ShortCode = 'M365_F1' }
            [PSCustomObject]@{SKU = '50f60901-3181-4b75-8a2c-4c8e4c1d5a72'; Name = 'Microsoft 365 F1'; ShortCode = 'M365_F1_COMM' }            
            [PSCustomObject]@{SKU = '66b55226-6b4f-492c-910c-a3b7a3c9d993'; Name = 'Microsoft 365 F3'; ShortCode = 'SPE_F1' }
            [PSCustomObject]@{SKU = '2a914830-d700-444a-b73c-e3f31980d833'; Name = 'Microsoft 365 F3 GCC'; ShortCode = 'M365_F1_GOV' }
            [PSCustomObject]@{SKU = 'e823ca47-49c4-46b3-b38d-ca11d5abe3d2'; Name = 'Microsoft 365 GCC G3'; ShortCode = 'M365_G3_GCC' }
            [PSCustomObject]@{SKU = 'e2be619b-b125-455f-8660-fb503e431a5d'; Name = 'Microsoft 365 GCC G5'; ShortCode = 'M365_G5_GCC' }
            [PSCustomObject]@{SKU = '94763226-9b3c-4e75-a931-5c89701abe66'; Name = 'Office 365 A1 for faculty'; ShortCode = 'STANDARDWOFFPACK_FACULTY' }
            [PSCustomObject]@{SKU = '78e66a63-337a-4a9a-8959-41c6654dfb56'; Name = 'Office 365 A1 Plus for faculty'; ShortCode = 'STANDARDWOFFPACK_IW_FACULTY' }
            [PSCustomObject]@{SKU = '314c4481-f395-4525-be8b-2ec4bb1e9d91'; Name = 'Office 365 A1 for students'; ShortCode = 'STANDARDWOFFPACK_STUDENT' }
            [PSCustomObject]@{SKU = 'e82ae690-a2d5-4d76-8d30-7c6e01e6022e'; Name = 'Office 365 A1 Plus for students'; ShortCode = 'STANDARDWOFFPACK_IW_STUDENT' }
            [PSCustomObject]@{SKU = 'e578b273-6db4-4691-bba0-8d691f4da603'; Name = 'Office 365 A3 for faculty'; ShortCode = 'ENTERPRISEPACKPLUS_FACULTY' }
            [PSCustomObject]@{SKU = '98b6e773-24d4-4c0d-a968-6e787a1f8204'; Name = 'Office 365 A3 for students'; ShortCode = 'ENTERPRISEPACKPLUS_STUDENT' }
            [PSCustomObject]@{SKU = 'a4585165-0533-458a-97e3-c400570268c4'; Name = 'Office 365 A5 for Faculty'; ShortCode = 'ENTERPRISEPREMIUM_FACULTY' }
            [PSCustomObject]@{SKU = 'ee656612-49fa-43e5-b67e-cb1fdf7699df'; Name = 'Office 365 A5 for Students'; ShortCode = 'ENTERPRISEPREMIUM_STUDENT' }
            [PSCustomObject]@{SKU = '18181a46-0d4e-45cd-891e-60aabd171b4e'; Name = 'Office 365 E1'; ShortCode = 'STANDARDPACK' }
            [PSCustomObject]@{SKU = '6634e0ce-1a9f-428c-a498-f84ec7b8aa2e'; Name = 'Office 365 E2'; ShortCode = 'STANDARDWOFFPACK' }
            [PSCustomObject]@{SKU = '6fd2c87f-b296-42f0-b197-1e91e994b900'; Name = 'Office 365 E3'; ShortCode = 'ENTERPRISEPACK' }
            [PSCustomObject]@{SKU = '189a915c-fe4f-4ffa-bde4-85b9628d07a0'; Name = 'Office 365 Developer'; ShortCode = 'DEVELOPERPACK' }
            [PSCustomObject]@{SKU = '1392051d-0cb9-4b7a-88d5-621fee5e8711'; Name = 'Office 365 E4'; ShortCode = 'ENTERPRISEWITHSCAL' }
            [PSCustomObject]@{SKU = 'c7df2760-2c81-4ef7-b578-5b5392b571df'; Name = 'Office 365 E5'; ShortCode = 'ENTERPRISEPREMIUM' }
            [PSCustomObject]@{SKU = '26d45bd9-adf1-46cd-a9e1-51e9a5524128'; Name = 'Office 365 E5 without Audio Conferencing'; ShortCode = 'ENTERPRISEPREMIUM_NOPSTNCONF' }
            [PSCustomObject]@{SKU = '4b585984-651b-448a-9e53-3b10f069cf7f'; Name = 'Office 365 F1/3'; ShortCode = 'DESKLESSPACK' }
            [PSCustomObject]@{SKU = '3f4babde-90ec-47c6-995d-d223749065d1'; Name = 'Office 365 G1 GCC'; ShortCode = 'STANDARDPACK_GOV' }
            [PSCustomObject]@{SKU = '535a3a29-c5f0-42fe-8215-d3b9e1f38c4a'; Name = 'Office 365 G3 GCC'; ShortCode = 'ENTERPRISEPACK_GOV' }
            [PSCustomObject]@{SKU = '8900a2c0-edba-4079-bdf3-b276e293b6a8'; Name = 'Office 365 G5 GCC'; ShortCode = 'ENTERPRISEPREMIUM_GOV' }
            [PSCustomObject]@{SKU = 'b75fa366-5b88-4b3c-9faa-86f44849c1e5'; Name = 'Office 365 F1 GCC'; ShortCode = '' } 
            [PSCustomObject]@{SKU = '6af4b3d6-14bb-4a2a-960c-6c902aad34f3'; Name = 'Microsoft Teams Rooms Basic'; ShortCode = 'Microsoft_Teams_Rooms_Basic' }
            [PSCustomObject]@{SKU = 'a4e376bd-c61e-4618-9901-3fc0cb1b88bb'; Name = 'Microsoft Teams Rooms Basic for EDU'; ShortCode = 'Microsoft_Teams_Rooms_Basic_FAC' }
            [PSCustomObject]@{SKU = '50509a35-f0bd-4c5e-89ac-22f0e16a00f8'; Name = 'Microsoft Teams Rooms Basic without Audio Conferencing'; ShortCode = 'Microsoft_Teams_Rooms_Basic_without_Audio_Conferencing' }    
            [PSCustomObject]@{SKU = '4cde982a-ede4-4409-9ae6-b003453c8ea6'; Name = 'Microsoft Teams Rooms Pro'; ShortCode = 'Microsoft_Teams_Rooms_Pro' }
            [PSCustomObject]@{SKU = 'c25e2b36-e161-4946-bef2-69239729f690'; Name = 'Microsoft Teams Rooms Pro for EDU'; ShortCode = 'Microsoft_Teams_Rooms_Pro_FAC' }
            [PSCustomObject]@{SKU = '31ecb341-2a17-483e-9140-c473006d1e1a'; Name = 'Microsoft Teams Rooms Pro for GCC'; ShortCode = 'Microsoft_Teams_Rooms_Pro_GCC' }
            [PSCustomObject]@{SKU = '21943e3a-2429-4f83-84c1-02735cd49e78'; Name = 'Microsoft Teams Rooms Pro without Audio Conferencing'; ShortCode = 'Microsoft_Teams_Rooms_Pro_without_Audio_Conferencing' }    
            [PSCustomObject]@{SKU = '6070a4c8-34c6-4937-8dfb-39bbc6397a60'; Name = 'Microsoft Teams Rooms Standard'; ShortCode = 'MEETING_ROOM' }
            [PSCustomObject]@{SKU = '61bec411-e46a-4dab-8f46-8b58ec845ffe'; Name = 'Microsoft Teams Rooms Standard without Audio Conferencing'; ShortCode = 'MEETING_ROOM_NOAUDIOCONF' }
            [PSCustomObject]@{SKU = '9571e9ac-2741-4b63-95fd-a79696f0d0ac'; Name = 'Microsoft Teams Rooms Standard for GCC'; ShortCode = 'MEETING_ROOM_GOV' }
            [PSCustomObject]@{SKU = 'b4348f75-a776-4061-ac6c-36b9016b01d1'; Name = 'Microsoft Teams Rooms Standard for GCC without Audio Conferencing'; ShortCode = 'MEETING_ROOM_GOV_NOAUDIOCONF' }
            [PSCustomObject]@{SKU = '4fb214cb-a430-4a91-9c91-4976763aa78f'; Name = 'Teams Rooms Premium'; ShortCode = 'MTR_PREM' }
            [PSCustomObject]@{SKU = '710779e8-3d4a-4c88-adb9-386c958d1fdf'; Name = 'Teams Exploratory'; ShortCode = 'TEAMS_EXPLORATORY' }
            [PSCustomObject]@{SKU = '29a2f828-8f39-4837-b8ff-c957e86abe3c'; Name = 'Microsoft Teams Commercial Cloud'; ShortCode = 'TEAMS_COMMERCIAL_TRIAL' }
            [PSCustomObject]@{SKU = '74fbf1bb-47c6-4796-9623-77dc7371723b'; Name = 'Microsoft Teams Trial'; ShortCode = 'MS_TEAMS_IW' }
            [PSCustomObject]@{SKU = '295a8eb0-f78d-45c7-8b5b-1eed5ed02dff'; Name = 'Microsoft Teams Shared Devices'; ShortCode = 'MCOCAP' }
            [PSCustomObject]@{SKU = 'b1511558-69bd-4e1b-8270-59ca96dba0f3'; Name = 'Microsoft Teams Shared Devices for GCC'; ShortCode = 'MCOCAP_GOV' }
            [PSCustomObject]@{SKU = 'ea126fc5-a19e-42e2-a731-da9d437bffcf'; Name = 'Dynamics 365 Plan 1 Enterprise Edition'; ShortCode = 'DYN365_ENTERPRISE_PLAN1' }
            [PSCustomObject]@{SKU = '8e7a3d30-d97d-43ab-837c-d7701cef83dc'; Name = 'Dynamics 365 for Team Members Enterprise Edition'; ShortCode = 'DYN365_ENTERPRISE_TEAM_MEMBERS' }
            [PSCustomObject]@{SKU = '7a551360-26c4-4f61-84e6-ef715673e083'; Name = 'Dynamics 365 Remote Assist'; ShortCode = 'MICROSOFT_REMOTE_ASSIST' }
            [PSCustomObject]@{SKU = 'e48328a2-8e98-4484-a70f-a99f8ac9ec89'; Name = 'Dynamics 365 Remote Assist HoloLens'; ShortCode = 'MICROSOFT_REMOTE_ASSIST_HOLOLENS' }    
            [PSCustomObject]@{SKU = '7ac9fe77-66b7-4e5e-9e46-10eed1cff547'; Name = 'Dynamics 365 Team Members_wDynamicsRetail'; ShortCode = 'DYN365_TEAM_MEMBERS' }
            [PSCustomObject]@{SKU = '7e74bd05-2c47-404e-829a-ba95c66fe8e5'; Name = 'Microsoft Teams EEA'; ShortCode = 'Microsoft_Teams_EEA_New' }
            [PSCustomObject]@{SKU = '3ab6abff-666f-4424-bfb7-f0bc274ec7bc'; Name = 'Microsoft Teams Essentials'; ShortCode = 'TEAMS_ESSENTIALS_AAD' }
        )
        
        $TeamsPlanID = '57ff2da0-773e-42df-b2af-ffb7a2317929'           # TEAMS1
        $TeamsGOVPlanID = '304767db-7d23-49e8-a945-4a7eb65f9f28'        # TEAMS_GOV
        $TeamsPhoneSystemID = '4828c8ec-dc2e-4779-b502-87ac9ce28ab7'    # MCOEV
        $TeamsGOVPhoneSystemID = 'db23fce2-a974-42ef-9002-d78dd42a0f22' # MCOEV_GOV
        
        # Not currently used, but may be handy at some point
        $TeamsCallingPlanList = 
            '4828c8ec-dc2e-4779-b502-87ac9ce28ab7', # MCOEV
            'db23fce2-a974-42ef-9002-d78dd42a0f22', # MCOEV_GOV
            '4ed3ff63-69d7-4fb7-b984-5aec7f605ca8', # MCOPSTN1
            '5a10155d-f5c1-411a-a8ec-e99aae125390', # MCOPSTN2
            '54a152dc-90de-4996-93d2-bc47e670fc06'  # MCOPSTN5
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID -And !$AuthToken) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token for tenant $TenantName."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        $TeamsCount = 0
        $TeamsEVCount = 0
        
        If ($AuthToken) {
            If ($TestMode -eq 'Basic') {
                $Properties = 'id,assignedLicenses'
            }
            Else {
                $Properties = 'id,assignedPlans'
            }
            
            If ($ExportCSV) { 
                $Properties += ',userPrincipalName,mail,displayName' 
                [System.Collections.ArrayList]$CSVList = @()
                $TestMode = 'Basic'
            }

            $Params = @{
                Properties = $Properties
                Filter = 'accountEnabled eq true and assignedLicenses/$count ne 0'
                AuthToken = $AuthToken
                ResultSize = 300000
            }
            
            If ($TenantName) { $Params.Add('TenantName',$TenantName) }
            
            If ($HideProgressBar) { $Params.Add('HideProgressBar', $HideProgressBar) }
            If ($ProgressUpdateFreq) { $Params.Add('ProgressUpdateFreq', $ProgressUpdateFreq) }
            
            
            Switch ($TestMode) {
                # Fastest approach. Just counts the licenses directly. Preferred for when we only need a total count.
                # May slightly increase counts compared to other methods because there's no way that I know to exclude users where Teams is explicitly disabled
                # We're talking about at most a tiny fraction of total users. Not big enough to worry about.
                'Fast' {
                    # Build the filter. Its simply appending each SKU as an 'or' option on the filter.
                    [String]$Filter = ''

                    ForEach ($SKU In $TeamsSKUList) {
                        $Filter += "assignedLicenses/any(s:s/skuId eq $($SKU.SKU)) or "
                    }
                    $Filter = $Filter.TrimEnd(' or ')
                    $Filter = "accountEnabled eq true and ($Filter)"
                    $TeamsCount = (Get-MSAzureUser -TenantName $TenantName -Filter $Filter -AuthToken $AuthToken -TotalCount).UserCount
                }

                # Pulls the user list and parses it for license counts
                'Basic'{
                    If (!$UserList) { $UserList = Get-MSAzureUser @Params } 
                    # $UserList = $UserList | Where-Object {$_.assignedLicenses -ne ''}
                    $TotalUsers = $UserList.Count
                    $UserCount = 0
                    $LastCount = 0
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    
                    ForEach ($User in $UserList) {
                        $LicenseSKUs = $User.assignedLicenses
    
                        $TeamsSKUs = $LicenseSKUs | Where-Object {$_.skuId -in $TeamsSKUList.SKU}
                        ForEach ($SKU in $TeamsSKUs) {
                            If (!($SKU | Where-Object {$_.disabledPlans -eq $TeamsPlanID -Or $_.disabledPlans -eq $TeamsGOVPlanID})) {
                                #Teams isn't disabled for this user. Increase the Teams user count and break out
                                If ($ExportCSV) { 
                                    $Item = [PSCustomObject][Ordered]@{
                                        DisplayName = $User.DisplayName
                                        UserPrincipalName = $User.UserPrincipalName
                                        Email = $User.Mail
                                        TeamsLicense = ($TeamsSKUList | Where-Object {$_.SKU -eq $SKU.SkuID} | Select-Object Name).Name
                                    }
                                    $CSVList += $Item
                                }
                                $TeamsCount++
                                Break
                            }
                        }
                        
                        If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                            $Percentage = $UserCount / $TotalUsers
                            $Rate = ($UserCount - $LastCount) / $ProgressUpdateFreq
                            $LastCount = $UserCount
                            $Message = "Checking Teams license status for user {0} of {1} ({4} Teams users so far) on tenant {2} at a rate of {3}/sec." -f $UserCount, $TotalUsers, $TenantName, $Rate, $TeamsCount
                            
                            If ($HideProgressBar) {
                                Write-Host $Message
                            }
                            Else {
                                Write-Progress -Activity $Message -PercentComplete ($Percentage * 100) -Status 'Calculating...' 
                            }
                            
                            $Stopwatch.Reset()
                            $Stopwatch.Start()
                        }
                        $UserCount++
                    }
                    
                    If ($ExportCSV) { $CSVList | Export-CSV $ExportCSV -NoTypeInformation -Force }
                }

                'Extended' {
                    If (!$UserList) { $UserList = Get-MSAzureUser @Params } 
                    $UserList = $UserList | Where-Object {$_.assignedPlans -ne ''} 
    
                    $TeamsCount = ($UserList | Select-Object -ExpandProperty assignedPlans | Where-Object {$_.capabilityStatus -eq 'Enabled' -and ($_.servicePlanId -eq $TeamsPlanID -Or $_.servicePlanId -eq $TeamsGOVPlanID)}).Count
                    $TeamsEVCount = ($UserList | Select-Object -ExpandProperty assignedPlans | Where-Object {$_.capabilityStatus -eq 'Enabled' -and ($_.servicePlanId -eq $TeamsPhoneSystemID -Or $_.servicePlanId -eq $TeamsGOVPhoneSystemID)}).Count
                    
                    If ($ExportCSV) {
                        $UserList | Select-Object id,userPrincipalName,mail,displayName -ExpandProperty assignedPlans | Where-Object {$_.capabilityStatus -eq 'Enabled' -and ($_.servicePlanId -eq $TeamsPlanID -Or $_.servicePlanId -eq $TeamsGOVPlanID)} | Export-Csv $ExportCSV -NoTypeInformation -Force
                    }                    
                }
            }

    
            $TeamsLicense = New-Object PsObject
            $TeamsLicense | Add-Member -NotePropertyName 'TeamsCount' -NotePropertyValue $TeamsCount
            
            If ($TestMode -eq 'Extended') { $TeamsLicense | Add-Member -NotePropertyName 'EVCount' -NotePropertyValue $TeamsEVCount }
            
            If ($TenantName) { $TeamsLicense | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
            
            Clear-Variable -Name AuthToken

            Return $TeamsLicense
        }
    }
}



Function Get-MSTeamsDevice {
    <#
        .SYNOPSIS
        Return a list of MS Teams Devices
         
        .DESCRIPTION
        Return a list of MS Teams Devices
         
        .PARAMETER ID
        The GUID of a specific device to query
         
        .PARAMETER DeviceType
        Return all devices that match the given device type. Choose from
        'unknown', 'ipPhone', 'TeamsRoom', 'SurfaceHub', 'CollaborationBar', 'TeamsDisplay', 'TouchConsole', 'LowCostPhone', 'TeamsPanel', 'SIP', 'UnknownFutureValue'
         
        .PARAMETER CurrentUser
        Show devices that are currently logged in by the given user. Use either the user's GUID or UPN
         
        .PARAMETER HardwareUniqueID
        Show device that matches the given unique hardware ID. Different from the GUID formatted ID.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('unknown', 'ipPhone', 'TeamsRoom', 'SurfaceHub', 'CollaborationBar', 'TeamsDisplay', 'TouchConsole', 'LowCostPhone', 'TeamsPanel', 'SIP', 'UnknownFutureValue', IgnoreCase=$True)]
        [string]$DeviceType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CurrentUser,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$HardwareUniqueID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize = 10000
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            If ($ID) {
                $URI = "https://graph.microsoft.com/beta/teamwork/devices/$ID"
                Write-Verbose $URI
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                Return $JSON                
            }
            
            If ($DeviceType) { $Filter | ForEach-Object { $Filter += ($(if($Filter){" and "}) + "deviceType eq '$DeviceType'") } }
            If ($UniqueID) { $Filter | ForEach-Object { $Filter += ($(if($Filter){" and "}) + "hardwareDetail/uniqueId eq '$UniqueID'") } }
            If ($CurrentUser) {
                If ([guid]::TryParse($CurrentUser, $([ref][guid]::Empty))) {  # Check if the CurrentUser param is already set to a GUID
                    $Filter | ForEach-Object { $Filter += ($(if($Filter){" and "}) + "currentUser/id eq '$CurrentUser'") }
                }
                else {  # Attempt to get the GUID of the user if we entered a username/email address
                    $UserGUID = (Get-MSAzureUser -UPN $CurrentUser).id
                    If ($UserGUID -ne '') {
                        $Filter | ForEach-Object { $Filter += ($(if($Filter){" and "}) + "currentUser/id eq '$UserGUID'") }
                    }                    
                }
            }

            $URI = "https://graph.microsoft.com/beta/teamwork/devices"
            
            If ($Filter) {
                $URI = $URI + "?`$filter=$Filter"
            }
            
            Write-Verbose $URI
            
            $Params = @{}
            If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.value
            
            $PageSize = $JSON.value.count
            
            While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                $NextURI = $JSON.'@odata.nextLink'
                $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                $JSON.value
                $PageSize += $JSON.value.count
            }
            Clear-Variable -Name AuthToken
        }
    }
}



Function Get-MSTeamsDeviceActivity {
    <#
        .SYNOPSIS
        Returns activity details for a Microsoft Teams-enabled device, including the active peripheral devices attached to the device.
         
        .DESCRIPTION
        Returns activity details for a Microsoft Teams-enabled device, including the active peripheral devices attached to the device.
         
        .PARAMETER ID
        The GUID of a specific device to query
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateScript ({
            Try { [System.Guid]::Parse($_) | Out-Null; $True } 
            Catch {    Throw 'Invalid GUID format.' }
        })]
        [string]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://graph.microsoft.com/beta/teamwork/devices/$ID/activity"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            Return $JSON                
        }
    }
}



Function Get-MSTeamsDeviceConfiguration {
    <#
        .SYNOPSIS
        Returns configuration details for a Microsoft Teams-enabled device.
         
        .DESCRIPTION
        Returns configuration details for a Microsoft Teams-enabled device.
         
        .PARAMETER ID
        The GUID of a specific device to query
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateScript ({
            Try { [System.Guid]::Parse($_) | Out-Null; $True } 
            Catch {    Throw 'Invalid GUID format.' }
        })]
        [string]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://graph.microsoft.com/beta/teamwork/devices/$ID/configuration"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            Return $JSON                
        }
    }
}



Function Get-MSTeamsDeviceHealth {
    <#
        .SYNOPSIS
        Returns health details for a Microsoft Teams-enabled device.
         
        .DESCRIPTION
        Returns health details for a Microsoft Teams-enabled device.
         
        .PARAMETER ID
        The GUID of a specific device to query
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsDRCalls -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Returns all Teams DR calls for the specified tenant/clientID/secret combination
         
        .EXAMPLE
        Get-MSTeamsDRCalls -AuthToken $AuthToken
        Returns all Teams DR calls using a previously obtained authtoken
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateScript ({
            Try { [System.Guid]::Parse($_) | Out-Null; $True } 
            Catch {    Throw 'Invalid GUID format.' }
        })]
        [string]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://graph.microsoft.com/beta/teamwork/devices/$ID/health"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            Return $JSON                
        }
    }
}



Function Get-MSTeamsDeviceOperations {
    <#
        .SYNOPSIS
        Returns a list of operations that are currently running on a Microsoft Teams-enabled device.
         
        .DESCRIPTION
        Returns a list of operations that are currently running on a Microsoft Teams-enabled device.
         
        .PARAMETER ID
        The GUID of a specific device to query
         
        .PARAMETER OperationID
        The GUID of a specific operation ID for a given device to query
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsDRCalls -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Returns all Teams DR calls for the specified tenant/clientID/secret combination
         
        .EXAMPLE
        Get-MSTeamsDRCalls -AuthToken $AuthToken
        Returns all Teams DR calls using a previously obtained authtoken
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateScript ({
            Try { [System.Guid]::Parse($_) | Out-Null; $True} 
            Catch {    Throw 'Invalid GUID format.' }
        })]
        [string]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateScript ({
            Try { [System.Guid]::Parse($_) | Out-Null; $True } 
            Catch {    Throw 'Invalid GUID format.' }
        })]
        [string]$OperationID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            $URI = "https://graph.microsoft.com/beta/teamwork/devices/$ID/operations"
            
            If ($OperationID) { $URI += "/$OperationID" }
            
            Write-Verbose $URI
            
            $Params = @{}
            If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.value
            
            $PageSize = $JSON.value.count
            
            While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                $NextURI = $JSON.'@odata.nextLink'
                $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                $JSON.value
                $PageSize += $JSON.value.count
            }
            Clear-Variable -Name AuthToken
        }
    }
}







#################################################################################################################################################
# #
# Zoom Functions #
# #
#################################################################################################################################################

Function Get-ZoomAccessToken {
    <#
        .SYNOPSIS
        Get a Zoom OAuth access token for a given Zoom tenant. Needed to run other Zoom API queries.
         
        .DESCRIPTION
        Get a Zoom OAuth access token for a given Zoom tenant. Needed to run other Zoom API queries.
 
        .PARAMETER ClientID
        The client ID for the Zoom app.
         
        .PARAMETER ClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Obtains an authtoken for the given tenant using secret-based auth and saves the results for use in other commands in a variable called $AuthToken
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 029834092-234234-234234-23442343 -MSTenantID 234234234-234234-234-23-42342342 -CertFriendlyName 'CertAuth' -CertStore LocalMachine
        Obtains an authtoken for the given tenant using certificate auth and saves the results for use in other commands in a variable called $AuthToken
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RedirectURI
    )
    
    Process {
        # Get the details from the tenant's Zoom config
        $ZoomConfig = Get-NectarZoomConfig
        
        Try {
            # Get initial authorization code
            $Body = @{
                response_type = 'code'
                redirect_uri = $RedirectURI
                client_id = $ZoomConfig.apiClientID
            }    
            $AuthURI = "https://zoom.us/oauth/authorize?response_type=code&redirect_uri=$RedirectURI&client_id=$($ZoomConfig.apiClientID)"
            Write-Verbose $AuthURI
            $AuthCode = Invoke-RestMethod -Method GET -uri $AuthURI

            Return $AuthCode
            
            # Get Base64-encoded representation of Client_ID:Client_Secret
            $ClientDetailsText = "$($ZoomConfig.apiClientID):$($ZoomConfig.apiClientSecret)"
            Write-Verbose "ClientDetails: $ClientDetailsText"
            $Bytes = [System.Text.Encoding]::UTF8.GetBytes($ClientDetailsText)
            $ClientDetails64 =[Convert]::ToBase64String($Bytes)
        
            $Headers = @{
                Authorization = "Basic $ClientDetails64"
                'Content-Type' = 'application/x-www-form-urlencoded'
            }
            
            Write-Verbose "AuthHeader: Basic $CLientDetails64"
            
            $Body = @{
                code = $ZoomConfig.verificationToken
                grant_type = 'authorization_code'
                redirect_uri = $RedirectURI
            }
            
            $URI = "https://zoom.us/oauth/token"
            Write-Verbose $URI
            $JSONAuth = Invoke-RestMethod -Method POST -uri $URI -Headers $Headers -Body $Body
            $AuthToken = $JSONAuth.access_token
            
            Return $JSONAuth
        }
        Catch {
            Write-Error "Failed to get access token. Ensure the values for ClientID and ClientSecret are correct."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarZoomConfig {
    <#
        .SYNOPSIS
        Returns information about the Nectar DXP Zoom configuration
         
        .DESCRIPTION
        Returns information about the Nectar DXP Zoom configuration.
        Requires a global admin account. Not available to tenant-level admins.
 
        .EXAMPLE
        Get-NectarZoomConfig
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnzc")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/zoom?tenant=$TenantName"
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON.data
        }
        Catch {
            Write-Error 'No tenant Zoom data found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarZoomOAuthApp {
    <#
        .SYNOPSIS
        Returns information about any configured Zoom OAuth applications on the server
         
        .DESCRIPTION
        Returns information about any configured Zoom OAuth applications on the server.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER ID
        The ID of a specific Zoom OAuth application.
 
        .EXAMPLE
        Get-NectarZoomOAuthApp
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnzoa")]
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [int]$ID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/client/oauth/zoom"
            
            If ($ID) { $URI += "/$ID" }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON.data
        }
        Catch {
            Write-Error 'No Zoom OAuth apps found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarZoomOAuthApp {
    <#
        .SYNOPSIS
        Creates a new Zoom OAuth application on the server
         
        .DESCRIPTION
        Creates a new Zoom OAuth application on the server.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER DisplayName
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER ClientID
        The Zoom OAuth application Client ID.
         
        .PARAMETER ClientSecret
        The Zoom OAuth application Client secret.
         
        .PARAMETER VerificationToken
        The Zoom OAuth application verification token
         
        .EXAMPLE
        New-NectarZoomOAuthApp -DisplayName 'Contoso_Global' -ClientID 'abcdefWEwrelj32324' -ClientSecret 'asfd90832jn3mvknaswui3' -VerificationToken '09432jpg43in9024323rsdvf'
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("nnzoa")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$VerificationToken
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Build the JSON body for creating the config
            $ZoomBody = @{
                displayName = $DisplayName
                clientId = $ClientID
                clientSecret = $ClientSecret
                verificationToken = $VerificationToken
            }
            
            $ZoomJSONBody = $ZoomBody | ConvertTo-Json
            
            $URI = "https://$Global:NectarCloud/aapi/client/oauth/zoom"
            
            $JSON = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $ZoomJSONBody -ContentType 'application/json; charset=utf-8'
            Return $JSON.data
        }
        Catch {
            Write-Error 'Cound not create Zoom OAuth application.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Set-NectarZoomOAuthApp {
    <#
        .SYNOPSIS
        Updates an existing Zoom OAuth application on the server
         
        .DESCRIPTION
        Updates an existing Zoom OAuth application on the server.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER ID
        The ID of the OAuth application to modify. Obtain via Get-NectarZoomOAuthApp
 
        .PARAMETER DisplayName
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER ClientID
        The Zoom OAuth application Client ID.
         
        .PARAMETER ClientSecret
        The Zoom OAuth application Client secret.
         
        .PARAMETER VerificationToken
        The Zoom OAuth application verification token
         
        .EXAMPLE
        Set-NectarZoomOAuthApp -ID 1 -DisplayName 'Contoso_Global'
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("snzoa")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$VerificationToken
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Get the existing Zoom OAuth app configuration
            $ZoomBody = Get-NectarZoomOAuthApp -ID $ID
            $ZoomBody = $ZoomBody | Select-Object -Property * -ExcludeProperty ID
            
            # Remove ID and other common params (like Verbose) from PSBoundParameters
            $PSBoundParameters.Remove('ID') | Out-Null

            ForEach ($Param in $PSBoundParameters.GetEnumerator()) {
                # Skip any common parameters (Debug, Verbose, etc)
                If ([System.Management.Automation.PSCmdlet]::CommonParameters -contains $Param.key) {
                    Continue
                }
                $ZoomBody.$($Param.Key) = $Param.Value
            }
            
            $ZoomJSONBody = $ZoomBody | ConvertTo-Json
            Write-Verbose $ZoomJSONBody
            
            $URI = "https://$Global:NectarCloud/aapi/client/oauth/zoom/$ID"
            
            $JSON = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $ZoomJSONBody -ContentType 'application/json; charset=utf-8'
            Return $JSON.data
        }
        Catch {
            Write-Error 'Cound not update Zoom OAuth application.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarZoomOAuthApp {
    <#
        .SYNOPSIS
        Removes an existing Zoom OAuth application on the server
         
        .DESCRIPTION
        Removes an existing Zoom OAuth application on the server.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER ID
        The ID of the OAuth application to remove. Obtain via Get-NectarZoomOAuthApp
 
        .EXAMPLE
        Remove-NectarZoomOAuthApp -ID 1
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("rnzoa")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NectarCloud/aapi/client/oauth/zoom/$ID"
            $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error 'Cound not remove Zoom OAuth application.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarZoomAuthURL {
    <#
        .SYNOPSIS
        Returns the Nectar DXP Zoom authorization URL needed for connecting Nectar DXP to Zoom
         
        .DESCRIPTION
        Returns the Nectar DXP Zoom authorization URL needed for connecting Nectar DXP to Zoom
 
        .EXAMPLE
        Get-NectarZoomAuthURL
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnzau")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $URI = "https://$Global:NectarCloud/aapi/zoom/oauth/url?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            Return $JSON.data
        }
        Catch {
            Write-Error 'No tenant Zoom data found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-ZoomAccessToken_LEGACY {
    <#
        .SYNOPSIS
        Get a Zoom JWT access token for a given Zoom tenant. Needed to run other Zoom API queries.
         
        .DESCRIPTION
        Get a Zoom access token for a given Zoom tenant. Needed to run other Zoom API queries. Generates a JSON Web Ticket (JWT)
 
        .EXAMPLE
        $AuthToken = Get-ZoomAccessToken -APIKey yourapikey -APISecret yourapisecret
 
        .NOTES
        Version 1.0
    #>

    
    Param(
        [Parameter(Mandatory = $False)]
        [ValidateSet('HS256', 'HS384', 'HS512')]
        $Algorithm = 'HS256',
        $type = $null,
        [Parameter(Mandatory = $True)]
        [string]$APIKey = $null,
        [int]$ValidforSeconds = 86400,
        [Parameter(Mandatory = $True)]
        $APISecret = $null
    )

    $exp = [int][double]::parse((Get-Date -Date $((Get-Date).addseconds($ValidforSeconds).ToUniversalTime()) -UFormat %s)) # Grab Unix Epoch Timestamp and add desired expiration.

    [hashtable]$header = @{alg = $Algorithm; typ = $type}
    [hashtable]$payload = @{iss = $APIKey; exp = $exp}

    $headerjson = $header | ConvertTo-Json -Compress
    $payloadjson = $payload | ConvertTo-Json -Compress
    
    $headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
    $payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')

    $ToBeSigned = $headerjsonbase64 + "." + $payloadjsonbase64

    $SigningAlgorithm = switch ($Algorithm) {
        "HS256" {New-Object System.Security.Cryptography.HMACSHA256}
        "HS384" {New-Object System.Security.Cryptography.HMACSHA384}
        "HS512" {New-Object System.Security.Cryptography.HMACSHA512}
    }

    $SigningAlgorithm.Key = [System.Text.Encoding]::UTF8.GetBytes($APISecret)
    $Signature = [Convert]::ToBase64String($SigningAlgorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($ToBeSigned))).Split('=')[0].Replace('+', '-').Replace('/', '_')
    
    $Token = "$headerjsonbase64.$payloadjsonbase64.$Signature"
    Return $Token
}



Function Get-ZoomMeeting {
    <#
        .SYNOPSIS
        Return a list of Zoom meetings.
         
        .DESCRIPTION
        Return a list of Zoom meetings.
         
        .PARAMETER MeetingID
        Return information about a specific meeting. Provide meeting ID or UUID. Don't use with From/To date range
         
        .PARAMETER Type
        Return live, past or past meetings with only one participant. Defaults to past
         
        .PARAMETER FromDateTime
        Start of date/time range to query. UTC, inclusive. Time range is based on the call start time. Defaults to yesterday.
 
        .PARAMETER ToDateTime
        End of date/time range to query. UTC, inclusive. Defaults to today
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER APIKey
        The Zoom API key for the application granted access to Zoom.
         
        .PARAMETER APISecret
        The Zoom API secret for the application granted access to Zoom.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-ZoomAccessToken
         
        .PARAMETER PageSize
        The number of results to return per page. Defaults to 30.
         
        .EXAMPLE
        Get-ZoomMeetings -AuthToken $AuthToken
        Returns all past Zoom meetings from the previous 24 hours
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("uuid")]
        [string]$MeetingID,    
        [Parameter(Mandatory = $False)]
        [ValidateSet('past', 'pastone', 'live')]
        $Type = 'past',
        [Parameter(Mandatory=$False)]
        [string]$FromDateTime = ((Get-Date).AddDays(-1).ToString('yyyy-MM-dd')),
        [Parameter(Mandatory=$False)]
        [string]$ToDateTime = (Get-Date -Format 'yyyy-MM-dd'),        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APIKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APISecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,300)]
        [int]$PageSize = 30
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($APIKey) { 
                $AuthToken = Get-ZoomAccessToken -APIKey $APIKey -APISecret $APISecret
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            If ($MeetingID) {
                $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID"
                
                $Body = @{
                    type = $Type
                }
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON
            }
            Else {
                $URI = "https://api.zoom.us/v2/metrics/meetings"
                
                $Body = @{
                    from = $FromDateTime
                    to = $ToDateTime
                    type = $Type
                    page_size = $PageSize
                }
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.meetings
                
                # If there is more than one page, use next_page_token to iterate through the pages
                While ($JSON.next_page_token) {
                    $Body.next_page_token = $JSON.next_page_token
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.meetings
                }
            }
            Clear-Variable -Name AuthToken
        }
    } 
}



Function Get-ZoomMeetingParticipants {
    <#
        .SYNOPSIS
        Return a list of Zoom meeting participants for a given meeting and their connection details.
         
        .DESCRIPTION
        Return a list of Zoom meeting participants for a given meeting and their connection details.
         
        .PARAMETER MeetingID
        The ID of the meeting. Provide meeting ID or UUID.
         
        .PARAMETER Type
        Return live, past or past meetings with only one participant. Defaults to past
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER APIKey
        The Zoom API key for the application granted access to Zoom.
         
        .PARAMETER APISecret
        The Zoom API secret for the application granted access to Zoom.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-ZoomAccessToken
         
        .PARAMETER PageSize
        The number of results to return per page. Defaults to 30.
         
        .EXAMPLE
        Get-ZoomMeetingParticipants 928340928 -AuthToken $AuthToken
        Returns participant information from a specific meeting
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("uuid")]
        [string]$MeetingID,    
        [Parameter(Mandatory = $False)]
        [ValidateSet('past', 'pastone', 'live')]
        [string]$Type = 'past',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APIKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APISecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,300)]
        [int]$PageSize = 30
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($APIKey) { 
                $AuthToken = Get-ZoomAccessToken -APIKey $APIKey -APISecret $APISecret
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID/participants"
            
            $Body = @{
                type = $Type
                page_size = $PageSize
            }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.participants
            
            # If there is more than one page, use next_page_token to iterate through the pages
            While ($JSON.next_page_token) {
                $Body.next_page_token = $JSON.next_page_token
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.participants
            }
            Clear-Variable -Name AuthToken
        }
    } 
}



Function Get-ZoomMeetingParticipantQoS {
    <#
        .SYNOPSIS
        Return the participant QoS from a given meeting.
         
        .DESCRIPTION
        Return the participant QoS from a given meeting.
         
        .PARAMETER MeetingID
        Return information about a specific meeting.
         
        .PARAMETER ParticipantID
        Return information about a specific participant in a given meeting. Optional
         
        .PARAMETER Type
        Return live, past or past meetings with only one participant. Defaults to past
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .PARAMETER APIKey
        The Zoom API key for the application granted access to Zoom.
         
        .PARAMETER APISecret
        The Zoom API secret for the application granted access to Zoom.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-ZoomAccessToken
         
        .PARAMETER PageSize
        The number of results to return per page. Defaults to 30.
         
        .EXAMPLE
        Get-ZoomMeetingParticipantQoS 928340928 -AuthToken $AuthToken
        Returns all past Zoom meetings from the previous 24 hours
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("uuid")]
        [string]$MeetingID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ParticipantID,
        [Parameter(Mandatory = $False)]
        [ValidateSet('past', 'pastone', 'live')]
        [string]$Type = 'past',    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APIKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APISecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,300)]
        [int]$PageSize = 30
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($APIKey) { 
                $AuthToken = Get-ZoomAccessToken -APIKey $APIKey -APISecret $APISecret
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            If ($ParticipantID) {
                $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID/participants/$ParticipantID/qos"
                
                $Body = @{
                    type = $Type
                }
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON
            }
            Else {
                $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID/participants/qos"
                
                $Body = @{
                    type = $Type
                    page_size = $PageSize
                }
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.participants
                
                # If there is more than one page, use next_page_token to iterate through the pages
                While ($JSON.next_page_token) {
                    $Body.next_page_token = $JSON.next_page_token
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.participants
                }
            }
            Clear-Variable -Name AuthToken
        }
    } 
}



#################################################################################################################################################
# #
# Informational Functions #
# #
#################################################################################################################################################

Function Get-NectarCodecs {
    <#
        .SYNOPSIS
        Returns a list of Nectar DXP codecs used in calls
         
        .DESCRIPTION
        Returns a list of Nectar DXP codecs used in calls
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarCodecs
        Returns all codecs
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnc")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/codecs?tenant=$TenantName"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
            If (!$JSON) {
                Write-Error 'Codec not found.'
            }
            Else {
                Return $JSON
            }
        }
        Catch {
            Write-Error 'Unable to get codecs.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarExtCities {
    <#
        .SYNOPSIS
        Returns a list of cities found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar DXP does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the cities where Nectar DXP was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The name of the city to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
 
        .EXAMPLE
        Get-NectarExtCities
        Returns the first 1000 cities sorted alphabetically.
 
        .EXAMPLE
        Get-NectarExtCities -ResultSize 5000
        Returns the first 5000 cities sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtCities -SearchQuery Gu
        Returns all cities that contain the letters 'gu'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneci")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/cities"
            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarExtCountries {
    <#
        .SYNOPSIS
        Returns a list of 2-letter country codes found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar DXP does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the countries where Nectar DXP was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The 2-letter country code to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-NectarExtCountries
        Returns all country codes sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtCountries -SearchQuery US
        Returns all country codes that contain the letters 'US'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneco")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/countries"
            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarExtISPs {
    <#
        .SYNOPSIS
        Returns a list of ISPs found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar DXP does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the ISPs where Nectar DXP was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The name of the city to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
 
        .EXAMPLE
        Get-NectarExtISPs
        Returns the first 1000 ISPs sorted alphabetically.
 
        .EXAMPLE
        Get-NectarExtISPs -ResultSize 5000
        Returns the first 5000 ISPs sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtISPs -SearchQuery Be
        Returns all ISPs that contain the letters 'be'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneci")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/isps"
            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



#################################################################################################################################################
#################################################################################################################################################
## ##
## Endpoint Client Functions ##
## ##
#################################################################################################################################################
#################################################################################################################################################

#################################################################################################################################################
# #
# Controller Connection Functions #
# #
#################################################################################################################################################

Function Connect-EPCController {
    <#
        .SYNOPSIS
        Connects to EPC Controller and store the credentials for later use.
 
        .DESCRIPTION
        Connects to an Endpoint Client Controller and store the credentials for later use.
         
        .PARAMETER ControllerFQDN
        The FQDN of the Endpoint Client Controller
 
        .PARAMETER Credential
        The credentials used to access the EPC Controller.
         
        .PARAMETER StoredCredentialTarget
        Use stored credentials saved via New-StoredCredential. Requires prior installation of CredentialManager module via Install-Module CredentialManager, and running:
        Get-Credential | New-StoredCredential -Target MyEPCCreds -Persist LocalMachine
         
        .PARAMETER EnvFromFile
        Use a CSV file called EPCEnvList.csv located in the user's default Documents folder to show a list of environments to select from. Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if EPCEnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default stored credential target to use for the selected environment. Requires prior installation and configuration of CredentialManager PS add-in.
        EPCEnvList.csv must have a header with two columns defined as "Environment, StoredCredentialTarget".
        Each environment and StoredCredentialTarget (if used) should be on their own separate lines
         
        .EXAMPLE
        $Cred = Get-Credential
        Connect-EPControllerFQDN -Credential $cred -ControllerFQDN contoso.nectar.services
        Connects to the contoso.nectar.services EPC Controller using the credentials supplied to the Get-Credential command
         
        .EXAMPLE
        Connect-EPControllerFQDN -ControllerFQDN contoso.nectar.services -StoredCredentialTarget MyEPCCreds
        Connects to contoso.nectar.services EPC Controller using previously stored credentials called MyEPCCreds
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        # [ValidateScript ({
            # If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                # $True
            # }
            # Else {
                # Throw "ERROR: Endpoint Client Controller name must be in FQDN format."
            # }
        # })]
        [string] $ControllerFQDN,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential] $Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$StoredCredentialTarget,
        [switch]$UseXML
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\EPCEnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }

    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $ControllerFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "ControllerFQDN: $ControllerFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($ControllerFQDN)
                
                # Check for stored credential target in EPCEnvList.csv and use if available
                $StoredCredentialTarget = $EnvSet[$EnvPos].StoredCredentialTarget
                Write-Verbose "StoredCredentialTarget: $StoredCredentialTarget"
            }
        }
        <#
        The New-WebServiceProxy command is not supported on PS versions higher than 5.1.
        On PS 6.0+, we have to use Invoke-WebRequest, which works, but is more verbose and doesn't utilize the
        WSDL files which creates specific objects for the results.
        Do a check and set a global variable which will determine if the function will use New-WebServiceProxy or Invoke-WebRequest
        #>
 
        If ($PSVersionTable.PSVersion.Major -gt 5) {
            $Global:EPC_UseWSDL = $FALSE
        }
        Else {
            $Global:EPC_UseWSDL = $TRUE
        }
        Write-Verbose "Setting global UseWSDL variable to $Global:EPC_UseWSDL"
    }    
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global EPCController tenant variable not available and not entered on command line
        If ((-not $Global:EPControllerFQDN) -And (-not $ControllerFQDN)) {
            $ControllerFQDN = Read-Host "Enter the Endpoint Client Controller FQDN"
        }
        ElseIf (($Global:EPControllerFQDN) -And (-not $ControllerFQDN)) {
            $ControllerFQDN = $Global:EPControllerFQDN
        }
    
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+)$"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $ControllerFQDN
        $EPControllerFQDN = $FQDNMatch.Matches.Groups[2].Value
    
        # Ask for credentials if global EPCController creds aren't available
        If (((-not $Global:EPControllerCred) -And (-not $Credential)) -Or (($Global:EPControllerFQDN -ne $EPControllerFQDN) -And (-Not $Credential)) -And (-Not $StoredCredentialTarget)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:EPControllerCred -And (-not $Credential)) {
            $Credential = $Global:EPControllerCred
        }
        
        # Pull stored credentials if specified
        If ($StoredCredentialTarget) {
            Try {
                $Credential = Get-StoredCredential -Target $StoredCredentialTarget
            }
            Catch {
                Write-Error "Cannot find stored credential for target: $StoredCredentialTarget"
            }
        }
        
        # Get the WSDL
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL'
            If ($Global:EPCWSDL_ActiveCtrl -and $Global:EPControllerFQDN -eq $EPControllerFQDN) {
                Write-Verbose 'Using WSDL from global variable'
                $EPC_ActiveCtrl = $Global:EPCWSDL_ActiveCtrl
                $EPC_TestGrpMgmt = $Global:EPCWSDL_TestGrpMgmt
                $EPC_ResGrpMgmt = $Global:EPCWSDL_ResGrpMgmt
                $EPC_SvcMgmt = $Global:EPCWSDL_SvcMgmt
                $EPC_BulkExport = $Global:EPCWSDL_BulkExport
                $EPC_GetData = $Global:EPCWSDL_GetData
            }
            ElseIf ((!$Global:EPCWSDL_ActiveCtrl -and $Global:EPControllerFQDN -eq $EPControllerFQDN) -Or !$Global:EPCWSDL_ActiveCtrl) {
                Write-Verbose "Loading WSDL from $EPControllerFQDN"
                $EPC_ActiveCtrl = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService?wsdl" -Namespace EPC.ActiveCtrl -Class ActiveCtrl
                $EPC_TestGrpMgmt = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService?wsdl" -Namespace EPC.TestGrpMgmt -Class TestGrpMgmt
                $EPC_ResGrpMgmt = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyRsrcGroupMgmtService?wsdl" -Namespace EPC.ResGrpMgmt -Class ResGrpMgmt
                $EPC_SvcMgmt = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemySvcMgmtService?wsdl" -Namespace EPC.SvcMgmt -Class SvcMgmt
                $EPC_BulkExport = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyBulkExportService?wsdl" -Namespace EPC.BulkExport -Class BulkExport
                $EPC_GetData = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyGetDataService?wsdl" -Namespace EPC.GetData -Class GetData
            }
            Else {
                Write-Error "There is already an active connection to $($Global:EPControllerFQDN). Please open a new PowerShell window to connect to a new controller."
                Return
            }
        }
                
        If ((-not $Global:EPControllerCred) -Or (-not $Global:EPControllerFQDN) -Or ($Global:EPControllerFQDN -ne $EPControllerFQDN)) {
            # Validate login credentials by running a simple API query
            If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                # Store creds in EPC credential object
                $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
                $EPCCred.username = $Credential.UserName
                $EPCCred.password = $Credential.GetNetworkCredential().Password

                Write-Verbose 'Using WSDL to test access'
                $EPCAPIVersionParams = New-Object -TypeName EPC.ActiveCtrl.GetAPIVersionParametersType
                $EPCAPIVersionParams.credentials = $EPCCred
                $EPCAPIVersion = $EPC_ActiveCtrl.getAPIVersion($EPCAPIVersionParams)
                
                $LoginResult = $EPCAPIVersion.result
            }
            Else {
                $ProgressPreference = 'SilentlyContinue'
                Write-Verbose 'Using WebXML to test access'
                [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                    xmlns:urn='urn:telchemyActiveCtrl'>
                     <soapenv:Header/>
                     <soapenv:Body>
                     <urn:getAPIVersionParameters>
                    <credentials>
                    <username>$($Credential.UserName)</username>
                    <password>$($Credential.GetNetworkCredential().Password)</password>
                    </credentials>
                     </urn:getAPIVersionParameters>
                     </soapenv:Body>
                    </soapenv:Envelope>"


                Write-Verbose $SOAPReq.OuterXML
                $SOAPFQDN = "https://$EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
                Write-Verbose $SOAPFQDN
                
                [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
                $LoginResult = $XMLResponse.Envelope.Body.getAPIVersionResults.result                
            }
            
            If ($LoginResult -ne 'success') {
                Write-Error "Could not connect to $EPControllerFQDN using $($Credential.UserName)"
                Throw $LoginResult
            }
            Else {
                Write-Host -ForegroundColor Green "Successful API connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$EPControllerFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow ($Credential).UserName
                $Global:EPControllerFQDN = $EPControllerFQDN
                $Global:EPControllerCred = $Credential
                $Global:EPCWSDL_ActiveCtrl = $EPC_ActiveCtrl
                $Global:EPCWSDL_TestGrpMgmt = $EPC_TestGrpMgmt
                $Global:EPCWSDL_ResGrpMgmt = $EPC_ResGrpMgmt
                $Global:EPCWSDL_SvcMgmt = $EPC_SvcMgmt
                $Global:EPCWSDL_BulkExport = $EPC_BulkExport
                $Global:EPCWSDL_GetData = $EPC_GetData

                If ($PSVersionTable.PSVersion.Major -gt 5) {
                    # Check for the PowerHTML module and install if not present. Needed for parsing web pageSize
                    If (!(Get-InstalledModule -Name PowerHTML)) { 
                        $Null = Install-Module -Name PowerHTML -Scope CurrentUser -Force
                    }
                }
                Write-Verbose "Setting global UseWSDL variable to $Global:EPC_UseWSDL"
            }
        }
    }
}    



Function Get-EPCConnectedController {
    <#
        .SYNOPSIS
        Returns the FQDN of the connected controller
 
        .DESCRIPTION
        Returns the FQDN of the connected controller
         
        .NOTES
        Version 1.0
    #>

    
    If ($Global:EPControllerFQDN) {
        Return $Global:EPControllerFQDN
    }
    Else {
        Return 'Not connected to a controller' 
    }
}



Function Get-EPCWebSessionCookie {
    <#
        .SYNOPSIS
        Creates a web session cookie for use with commands that can't use the EPC API
 
        .DESCRIPTION
        Connects to an Endpoint Client Controller and store the credentials for later use.
        Must have already successfully connected via Connect-EPCController
         
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param ()

    Connect-EPCController
    $ProgressPreference = 'SilentlyContinue'

    #URLEncode the password
    $EncodedPassword = [System.Web.HttpUtility]::UrlEncode($Global:EPControllerCred.GetNetworkCredential().Password)
    
    # Check for existing global session cookie. Connect if not available and save the web session variable in a global variable
    If (!$Global:EPCSessionCookie) {
        Write-Verbose 'Global session cookie does not exist'
        $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -SessionVariable 'EPCSession'
        $NULL = Invoke-WebRequest -Uri "https://$EPControllerFQDN/j_security_check" -Method POST -Body "j_username=$($Global:EPControllerCred.UserName)&j_password=$EncodedPassword" -WebSession $EPCSession
        
        # Verify that session is active. We do this by looking for the existence of a single link on the dashboard page. If there is, this means the session expired and the base login page is being shown.
        # If there are numerous links, this indicates a successful login.
        $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -WebSession $EPCSession
        
        If ($Dashboard.Links.Count -gt 1) {
            Write-Host -ForegroundColor Green "Successful web session connection to " -NoNewLine
            Write-Host -ForegroundColor Yellow "https://$EPControllerFQDN" -NoNewLine
            Write-Host -ForegroundColor Green " using " -NoNewLine
            Write-Host -ForegroundColor Yellow ($Global:EPControllerCred).UserName
            $Global:EPCSessionCookie = $EPCSession
        }
        Else {
            Throw "Could not connect to $EPControllerFQDN web session using $($Credential.UserName)"
        }                
    }
    Else {
        # Verify that session is active. We do this by looking for the existence of a single link on the dashboard page. If there is, this means the session expired and the base login page is being shown.
        # If there are numerous links, this indicates the session is still active
        
        $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -WebSession $Global:EPCSessionCookie
        Write-Verbose "Session verify against $EPControllerFQDN"
        If ($Dashboard.Links.Count -eq 1) {
            # Re-authenticate and update global session cookie
            Write-Verbose 'Global session cookie expired'
            $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -SessionVariable 'EPCSession'
            
            $NULL = Invoke-WebRequest -Uri "https://$EPControllerFQDN/j_security_check" -Method POST -Body "j_username=$($Global:EPControllerCred.UserName)&j_password=$EncodedPassword" -WebSession $EPCSession
            $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -WebSession $EPCSession
            
            If ($Dashboard.Links.Count -gt 1) {
                Write-Verbose "Successful auth cookie regeneration against $EPControllerFQDN"
                $Global:EPCSessionCookie = $EPCSession
            }
            Else {
                Throw "Could not regenerate auth cookie against $EPControllerFQDN using $($Credential.UserName)"
            }
        }
        Else {
            Write-Verbose "Session verified against $EPControllerFQDN"
        }
    }
}



Function Disconnect-EPCController {
    <#
        .SYNOPSIS
        Disconnects from any active Endpoint Client Controller
         
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables.
        If PS version is less than 6, this doesn't currently work because there apparently isn't a way to remove the WSDL namespace from memory.
        Thiss means that connecting to a new EPC controller will still try to use the WSDL associated with the original controller.
        Any API commands will fail. Therefore, there is no useful reason to disconnect.
 
        .EXAMPLE
        Disconnect-EPCController
        Disconnects from all active connections to an EPC Controller
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    param ()

    $VariableNames = 'EPControllerCred','EPControllerFQDN','EPCSessionCookie','EPCWSDL_ActiveCtrl','EPCWSDL_ResGrpMgmt','EPCWSDL_SvcMgmt', 'EPC_UseWSDL'

    ForEach ($Variable in $VariableNames) {
        Remove-Variable $Variable -Scope Global -ErrorAction:SilentlyContinue
    }
}




#################################################################################################################################################
# #
# EPC Test Point Functions #
# #
#################################################################################################################################################

Function Get-EPCTestPoint {
    <#
        .SYNOPSIS
        Return information about a given EPC test point
         
        .DESCRIPTION
        Return information about a given EPC test point
         
        .PARAMETER Name
        The name of a specific test point to retrieve information about. Will do partial matches.
     
        .PARAMETER Status
        Only return information about test points with a specified status
         
        .PARAMETER OrgID
        Only return information about test points within a specified organization
         
        .PARAMETER Version
        Only return information about test points that match the given version
         
        .PARAMETER RGName
        Only return information about test points that are within a given resource group
        Can specify either RG name or ID
 
        .PARAMETER RGID
        Only return information about test points that are within a given resource group
        Can specify either RG name or ID
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-EPCTestPoint
        Returns information about the first 10000 test points
 
        .EXAMPLE
        Get-EPCTestPoint -Name 'TFerguson Laptop'
        Returns information about a specific test point using the test point name
         
        .EXAMPLE
        Get-EPCTestPoint -Status Connected -OrgID Contoso
        Returns information about connected test points in the Contoso organization
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('EndpointName')]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Connected','NotConnected','Reachable','Unknown','Unlicensed', IgnoreCase=$True)]
        [string]$Status,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$OrgID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Version,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RGName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int[]]$RGID,
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 10000,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL'
            $EPCListTestPointParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointsParametersType
            $EPCListTestPointParams.credentials = $EPCCred
            $EPCListTestPointParams.maxNumber = $ResultSize

            # If any of the below parameters are specified, apply a filter
            If ($Name -Or $Status -Or $OrgID -Or $Version -Or $RGName -Or $RGID) {
                $EPCListTestPointFilterParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointsParametersTypeFilter
                If ($Name) { $EPCListTestPointFilterParams.namesubstring = $Name }
                If ($OrgID) { $EPCListTestPointFilterParams.orgID = $OrgID }
                If ($Version) { $EPCListTestPointFilterParams.version = $Version }
                If ($Status) { $EPCListTestPointFilterParams.status = $Status; $EPCListTestPointFilterParams.statusSpecified = $TRUE }
                If ($RGName) { $RGID = (Get-EPCResourceGroup | Where-Object {$_.RGName -eq $RGName}).RGID }
                If ($RGID) { $EPCListTestPointFilterParams.RGList = $RGID }
                
                $EPCListTestPointParams.Filter = $EPCListTestPointFilterParams
            }
                        
            $TestPointResults = $Global:EPCWSDL_ActiveCtrl.listTestPoints($EPCListTestPointParams)
            Write-Verbose $TestPointResults.result

            If ($TestPointResults.result -eq 'success') {
                Return $TestPointResults.testptList
            }
            Else {
                Throw $TestPointResults.result
            }            
        }
        Else {
            Write-Verbose 'Using WebXML'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPointsParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                <maxNumber>$ResultSize</maxNumber>
                 </urn:listTestPointsParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"


            # If any of the below parameters are specified, apply a filter
            If ($Name -Or $Status -Or $OrgID -Or $Version -Or $OSArch -Or $RGName -Or $RGID) {
                $FilterXMLElement = $SOAPReq.Envelope.Body.listTestPointsParameters.AppendChild($SOAPReq.CreateElement('filter'))
                
                If ($Name) { 
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('name-substring'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($Name))
                }
                
                If ($Status) {
                    If ($Status -eq 'NotConnected') { $StatusFormatted = 'not connected' } Else { $StatusFormatted = $Status }
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('status'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($StatusFormatted.ToLower()))                    
                }
                
                If ($OrgID) {
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('orgID'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($OrgID.ToLower()))                    
                }

                If ($Version) {
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('version'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($Version))                
                }
                
                If ($RGName) { 
                    $RGID = (Get-EPCResourceGroup | Where-Object {$_.RGName -eq $RGName}).RGID
                }
                
                If ($RGID) { 
                    $RGXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('RGList'))
                    $NewXMLElement = $RGXMLElement.AppendChild($SOAPReq.CreateElement('rg'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($RGID))                
                }
            }

            Write-Verbose $SOAPReq.OuterXML
            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TestPointResults = $XMLResponse.Envelope.Body.listTestPointsResults
            
            If ($TestPointResults.result -eq 'success') {
                Return $TestPointResults.testptList.testpt
            }
            Else {
                Throw $TestPointResults.result
            }
        }        
    }
}



Function Set-EPCTestPoint {
    <#
        .SYNOPSIS
        Update the name and/or description for a given EPC test point
         
        .DESCRIPTION
        Update the name and/or description for a given EPC test point
         
        .PARAMETER UUID
        The UUID of the test point to update
         
        .PARAMETER Name
        The display name to set on the test point
         
        .PARAMETER Description
        The description to set on the test point
 
        .EXAMPLE
        Set-EPCTestPoint -UUID d4a1437f-1f18-11ec-cf33-0daedf04882a -Name 'New Name' -Description 'Updated description'
        Updates the selected test point's name and description
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("ID")]
        [string]$UUID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("UserDisplayName")]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description    
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $TPFQDN = "https://$EPControllerFQDN/test/testPoint.do?action=2"
        
        # If name isn't set, pull the name from the existing record, otherwise it will replace the name with its IP address
        If (!$DisplayName) { $DisplayName = (Get-EPCTestPoint -UUID $UUID).Name }
        
        $TPBody = @{
            tptype = 1
            uuid = $UUID
            name = $DisplayName
        }
        
        If ($Description) { $TPBody.Add('description',$Description -Replace "[^\sa-zA-Z0-9.-]") }

        Write-Verbose "UUID: $UUID, Name: $DisplayName"
        
        $NULL = Invoke-WebRequest -Method POST -Uri $TPFQDN -Body $TPBody -WebSession $Global:EPCSessionCookie
    }
}




Function Get-EPCTestPointInterface {
    <#
        .SYNOPSIS
        Return network interface inforemation for a given EPC test point
         
        .DESCRIPTION
        Return network interface inforemation for a given EPC test point
         
        .PARAMETER ID
        The ID of the test point to retrieve information about
         
        .EXAMPLE
        Get-EPCTestPointInterface -ID d4a1437f-1f18-11ec-cf33-0daedf04882a
        Returns network interface information about a specific test point
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('testptID', 'EndpointID')]
        [string]$UUID,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to pull interface list'
            $EPCListTestPointInterfaceParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointInterfaceParametersType
            $EPCListTestPointInterfaceParams.credentials = $EPCCred
            $EPCListTestPointInterfaceParams.testptID = $UUID
            $TestPointInterfaceResults = $Global:EPCWSDL_ActiveCtrl.listTestPointInterfaces($EPCListTestPointInterfaceParams)
            Write-Verbose $TestPointInterfaceResults.result
            
            If ($TestPointInterfaceResults.result -eq 'success') {
                Return $TestPointInterfaceResults.InterfaceList
            }
            Else {
                Throw $TestPointInterfaceResults.result
            }
        }
        Else {
            Write-Verbose 'Using XML to pull interface list'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPointInterfaceParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                <testptID>$UUID</testptID>
                 </urn:listTestPointInterfaceParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TestPointInterfaceResults = $XMLResponse.Envelope.Body.listTestPointInterfaceResults
            
            If ($TestPointInterfaceResults.result -eq 'success') {
                [psobject]$TestPointInterfaces = $TestPointInterfaceResults.InterfaceList.interface | ConvertFrom-XMLElement
                Return $TestPointInterfaces
            }
            Else {
                Throw $TestPointInterfaceResults.result
            }
        }
    }
}




Function Get-EPCTestPointEvent {
    <#
        .SYNOPSIS
        Returns a list of EPC test groups and their associated IDs
         
        .DESCRIPTION
        Returns a list of EPC test groups and their associated IDs
 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-EPCTestPointEvent
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("testptID")]
        [string]$TestPointID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TestPointName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','ALL','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [datetimeoffset]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [datetimeoffset]$TimePeriodTo,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('Any','Fatal','Error','Warn','Info','Debug', IgnoreCase=$True)]
        [string]$EventLevel = 'Any',
        [Parameter(Mandatory=$False)]
        [ValidateSet('Any','Reporter','Controller','Agent', IgnoreCase=$True)]
        [string]$AppType = 'Any',        
        [Parameter(Mandatory=$False)]
        [string]$SortColumn = 'evt_date',        
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $Body = @{
            action = 3
            filter = $EventLevel.ToLower()
        }
        
        If ($AppType -ne 'Any') {
            Switch ($AppType) {
                'Reporter' { $AppTypeID = 2 }
                'Controller' { $AppTypeID = 3 }
                'Agent' { $AppTypeID = 4 }
            }
            $Body.Add('apptype', $AppTypeID)
        }
        
        If ($TimePeriod -ne 'ALL') {
            Switch ($TimePeriod) {
                'LAST_HOUR' { $TimePeriodFrom = [DateTimeOffset]::Now.AddHours(-1); $TimePeriodTo = [DateTimeOffset]::Now }
                'LAST_DAY' { $TimePeriodFrom = [DateTimeOffset]::Now.AddDays(-1); $TimePeriodTo = [DateTimeOffset]::Now }
                'LAST_WEEK' { $TimePeriodFrom = [DateTimeOffset]::Now.AddDays(-7); $TimePeriodTo = [DateTimeOffset]::Now }
                'LAST_MONTH' { $TimePeriodFrom = [DateTimeOffset]::Now.AddMonths(-1); $TimePeriodTo = [DateTimeOffset]::Now }
            }
            
            If (!$TimePeriodTo) { $TimePeriodTo = [DateTimeOffset]::Now }
            
            $Body.Add('beginsecs', $TimePeriodFrom.ToUnixTimeSeconds())
            $Body.Add('endsecs', $TimePeriodTo.ToUnixTimeSeconds())
        }
        
        If ($TestPointID) { $Body.Add('hostuuid', $TestPointID) }
        
        If ($TestPointName) { 
            $TestPointID = (Get-EPCTestPoint -Name $TestPointName -ResultSize 1).testptID
            
            If (!$TestPointID) { Throw "Could not find test point with name $TestPointName" }

            $Body.Add('hostuuid', $TestPointID)
        }
        
        $TPEFQDN = "https://$Global:EPControllerFQDN/admin/eventLogs.do"
        $TPEResult = Invoke-RestMethod -Method GET -Uri $TPEFQDN -WebSession $Global:EPCSessionCookie -Body $Body

        $FormattedResults = $TPEResult.events | Select-Object application, host, appCode, @{Name='time';Expression={(([System.DateTimeOffset]::FromUnixTimeMilliSeconds($_.Time)).LocalDateTime).ToString("yyyy-MM-dd HH:mm:ss.fff")}}, priority, message
        Return $FormattedResults
    }
}




#################################################################################################################################################
# #
# EPC Test Group Functions #
# #
#################################################################################################################################################

Function Get-EPCTestGroup {
    <#
        .SYNOPSIS
        Returns a list of EPC test groups and their associated IDs
         
        .DESCRIPTION
        Returns a list of EPC test groups and their associated IDs
 
        .PARAMETER SearchString
        Filter the results to only show test groups that match the given search string
         
        .EXAMPLE
        Get-EPCTestGroup -SearchString Contoso
        Returns test groups that contain the name Contoso
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchString,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to pull test group list'
            $EPCListGroupsParams = New-Object -TypeName EPC.TestGrpMgmt.ListGroupsParametersType
            $EPCListGroupsParams.credentials = $EPCCred
            $EPCListGroupsParams.matchsubstring = $SearchString
            $EPCListGroupsResults = $Global:EPCWSDL_TestGrpMgmt.ListGroups($EPCListGroupsParams)
            Write-Verbose $EPCListGroupsResults.ListGroupsResponse
            
            If ($EPCListGroupsResults.ListGroupsResponse -eq 'success') {
                Return $EPCListGroupsResults.ListGroupsGroupList
            }
            Else {
                Throw $EPCListGroupsResults.ListGroupsResponse
            }
        }
        Else {
            Write-Verbose 'Using XML to pull test group list'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:ListGroupsMatchingString>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                <matchsubstring>$SearchString</matchsubstring>
                 </urn:ListGroupsMatchingString>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $EPCListGroupsResults = $XMLResponse.Envelope.Body.ListGroupsResults
            
            If ($EPCListGroupsResults.ListGroupsResponse -eq 'success') {
                If ($EPCListGroupsResults.ListGroupsGroupList) {
                    [psobject]$ListGroups = $EPCListGroupsResults.ListGroupsGroupList | ConvertFrom-XMLElement
                    Return $ListGroups
                }
                Else {
                    Write-Error "Could not find a group called $SearchString"
                }
            }
            Else {
                Throw $EPCListGroupsResults.ListGroupsResponse
            }
        }
    }
}



Function Set-EPCTestGroup {
    <#
        .SYNOPSIS
        Update the name of an existing test group
         
        .DESCRIPTION
        Update the name of an existing test group
         
        .PARAMETER Name
        The name of the test group to update
 
        .PARAMETER NewName
        The new name of the test group
         
        .PARAMETER TGID
        The ID of a test group. If both Name and TGID are specified, Name will take priority.
         
        .EXAMPLE
        Set-EPCTestGroup -Name 'Existing Name' -NewName 'New Name'
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$GroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$GroupID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$NewGroupName,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($GroupName -And !$GroupID) { 
            $GroupIDList = (Get-EPCTestGroup -SearchString $GroupName).GroupID
            If ($GroupIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified group name. Please refine your search to return a single group or use GroupID parameter.' 
            }
            Else {
                $GroupID = $GroupIDList
            }
        }
        
        If (!$GroupID) { Throw 'Test Group ID not specified or could not be found' }
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to set test group parameters'
            $EPCModifyGroupParams = New-Object -TypeName EPC.TestGrpMgmt.ModifyGroupParametersType
            $EPCModifyGroupParams.credentials = $EPCCred
            $EPCModifyGroupParams.ModifyGroupID = $GroupID
            $EPCModifyGroupParams.ModifyGroupUpdatedName = $NewGroupName
            $EPCModifyGroupResults = $Global:EPCWSDL_TestGrpMgmt.ModifyGroup($EPCModifyGroupParams)
            Write-Verbose $EPCModifyGroupResults.ModifyGroupResponse
        }
        Else {
            Write-Verbose 'Using XML to set test group parameters'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                <soapenv:Header/>
                <soapenv:Body>
                 <urn:ModifyGroupParameters>
                  <credentials>
                   <username>$($Global:EPControllerCred.UserName)</username>
                   <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                  </credentials>
                  <ModifyGroupID>$GroupID</ModifyGroupID>
                  <ModifyGroupUpdatedName>$NewGroupName</ModifyGroupUpdatedName>
                 </urn:ModifyGroupParameters>
                </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $EPCModifyGroupResults = $XMLResponse.Envelope.Body.ModifyGroupResults
        }
        
        If ($EPCModifyGroupResults.ModifyGroupResponse -eq 'success') {
            Return $EPCModifyGroupResults.ModifyGroupResponse
        }
        Else {
            Throw $EPCModifyGroupResults.ModifyGroupResponse
        }
    }
}



Function New-EPCTestGroup {
    <#
        .SYNOPSIS
        Create a new EPC test group
         
        .DESCRIPTION
        Create a new EPC test group
         
        .PARAMETER GroupName
        The name of the test group
         
        .PARAMETER GroupType
        The type to assign to the test group
         
        .EXAMPLE
        New-EPCTestGroup -GroupName MyGroup -GroupType Agent
        Creates an agent test group called MyGroup
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$GroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Agent','Network Entity','DHCP','DNS','HTTP','POP3','SIP Endpoint','SMTP', IgnoreCase=$True)]
        [string]$GroupType = 'Agent',
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to create test group'
            $EPCAddGroupParams = New-Object -TypeName EPC.TestGrpMgmt.AddGroupParametersType
            $EPCAddGroupParams.credentials = $EPCCred
            $EPCAddGroupParams.AddGroupGroupName = $GroupName
            $EPCAddGroupParams.GroupType = $GroupType
            $EPCAddGroupResults = $Global:EPCWSDL_TestGrpMgmt.AddGroup($EPCAddGroupParams)
            Write-Verbose $EPCAddGroupResults.AddGroupResponse
        }
        Else {
            Write-Verbose 'Using XML to create test group'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                <soapenv:Header/>
                <soapenv:Body>
                 <urn:AddGroupParameters>
                  <credentials>
                   <username>$($Global:EPControllerCred.UserName)</username>
                   <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                  </credentials>
                  <AddGroupGroupName>$GroupName</AddGroupGroupName>
                  <GroupType>$($GroupType.ToLower())</GroupType>
                 </urn:AddGroupParameters>
                </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $EPCAddGroupResults = $XMLResponse.Envelope.Body.AddGroupResults
        }
        
        If ($EPCAddGroupResults.AddGroupResponse -eq 'success') {
            Return $EPCAddGroupResults
        }
        Else {
            Throw $EPCAddGroupResults.AddGroupResponse
        }
    }
}



Function Remove-EPCTestGroup {
    <#
        .SYNOPSIS
        Remove an EPC test group
         
        .DESCRIPTION
        Remove an EPC test group
         
        .PARAMETER GroupName
        The name of the test group to remove. Use either this or GroupID.
         
        .PARAMETER GroupID
        The ID of the test group to remove. Use either this or GroupName.
         
        .EXAMPLE
        Remove-EPCTestGroup -GroupName MyGroup
        Removes the test group called MyGroup
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$GroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$GroupID,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($GroupName -And !$GroupID) { 
            $GroupIDList = (Get-EPCTestGroup -SearchString $GroupName).GroupID
            If ($GroupIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified group name. Please refine your search to return a single group or use GroupID parameter.' 
            }
            Else {
                $GroupID = $GroupIDList
            }
        }
        
        If (!$GroupID) { Throw 'Test Group ID not specified or could not be found' }
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to create test group'
            $EPCDeleteGroupParams = New-Object -TypeName EPC.TestGrpMgmt.DeleteGroupParametersType
            $EPCDeleteGroupParams.credentials = $EPCCred
            $EPCDeleteGroupParams.DeleteGroupID = $GroupID
            $EPCDeleteGroupResults = $Global:EPCWSDL_TestGrpMgmt.DeleteGroup($EPCDeleteGroupParams)
            Write-Verbose $EPCDeleteGroupResults.DeleteGroupResponse
        }
        Else {
            Write-Verbose 'Using XML to create test group'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                <soapenv:Header/>
                <soapenv:Body>
                 <urn:DeleteGroupWithID>
                  <credentials>
                   <username>$($Global:EPControllerCred.UserName)</username>
                   <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                  </credentials>
                  <DeleteGroupID>$GroupName</DeleteGroupID>
                 </urn:DeleteGroupWithID>
                </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $EPCDeleteGroupResults = $XMLResponse.Envelope.Body.AddGroupResults
        }
        
        If ($EPCDeleteGroupResults.DeleteGroupResponse -eq 'success') {
            Return $EPCDeleteGroupResults
        }
        Else {
            Throw $EPCDeleteGroupResults.DeleteGroupResponse
        }
    }
}



Function Get-EPCTestGroupMembers {
    <#
        .SYNOPSIS
        Returns a list of test points associated with a given test group
         
        .DESCRIPTION
        Returns a list of test points associated with a given test group
         
        .EXAMPLE
        Get-EPCTestGroupMembers -GroupID 4
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$GroupName,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$GroupID,
        [switch]$UseXML    
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($GroupName -And !$GroupID) { 
            $GroupIDList = (Get-EPCTestGroup -SearchString $GroupName).GroupID
            If ($GroupIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified GroupName. Please refine your search to return a single group or use GroupID parameter.' 
            }
            Else {
                $GroupID = $GroupIDList
            }
        }
        
        If (!$GroupID) { Throw 'Test Group ID not specified or could not be found' }

        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to pull test group member list'
            $EPCListEndpointsInGroupParams = New-Object -TypeName EPC.TestGrpMgmt.ListEndpointsInGroupParametersType
            $EPCListEndpointsInGroupParams.credentials = $EPCCred
            $EPCListEndpointsInGroupParams.GroupID = $GroupID    
            $EPCListEndpointsInGroupResults = $Global:EPCWSDL_TestGrpMgmt.ListEndpointsInGroup($EPCListEndpointsInGroupParams)
            Write-Verbose $EPCListEndpointsInGroupResults.ListEndpointsGroupResponse
            
            If ($EPCListEndpointsInGroupResults.ListEndpointsGroupResponse -eq 'success') {
                Return $EPCListEndpointsInGroupResults.EndpointIDList
            }
            Else {
                Throw $EPCListEndpointsInGroupResults.ListEndpointsGroupResponse
            }
        }
        Else {
            Write-Verbose 'Using XML to pull test group member list'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                <soapenv:Header/>
                <soapenv:Body>
                 <urn:ListEndpointsInGroupWithID>
                  <credentials>
                   <username>$($Global:EPControllerCred.UserName)</username>
                   <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                  </credentials>
                  <GroupID>$GroupID</GroupID>
                 </urn:ListEndpointsInGroupWithID>
                </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $ListGroupMembersResults = $XMLResponse.Envelope.Body.ListEndpointsInGroupResults
            
            If ($ListGroupMembersResults.ListEndpointsGroupResponse -eq 'success') {
                If ($ListGroupMembersResults.EndpointIDList) { # Only return results if the list exists
                    [psobject]$ListGroupMembers = $ListGroupMembersResults.EndPointIDList | ConvertFrom-XMLElement
                    Return $ListGroupMembers
                }
            }
            Else {
                Throw $ListGroupMembersResults.ListEndpointsGroupResponse
            }
        }
    }
}    



Function Add-EPCTestGroupMember {
    <#
        .SYNOPSIS
        Adds an EPC test point to an EPC test group
         
        .DESCRIPTION
        Adds an EPC test point to an EPC test group
         
        .PARAMETER TestGroupName
        The name of the test group to add the member to. Use either this or TestGroupID
         
        .PARAMETER TestGroupID
        The ID of the test group to add the member to. Use either this or TestGroupName
         
        .PARAMETER ResourceGroupName
        The name of the resource group associated with this test group
         
        .PARAMETER TestPointName
        The name of the test point to add to the test group
         
        .PARAMETER InterfaceID
        The interface ID of the test point
         
        .PARAMETER IPAddress
        The IP address associated with the given interface
         
        .EXAMPLE
        Add-EPCTestGroupMember -TestGroupName MyTestGroup -ResourceGroupName MyRG -TestPointName 'TFerguson laptop' -InterfaceID 'Primary Network Adapter' -IPAddress '192.168.1.20'
        Adds the given test point to the MyTestGroup test group
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TestGroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$TestGroupID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ResourceGroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$ResourceGroupID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('name')]
        [string]$TestPointName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('TestPtID')]
        [string]$TestPointID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$InterfaceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$IPAddress,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($TestGroupName -And !$TestGroupID) { 
            $TestGroupIDList = (Get-EPCTestGroup -SearchString $TestGroupName).GroupID
            If ($TestGroupIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified test group name. Please refine your search to return a single test group or use TestGroupID parameter.' 
            }
            Else {
                [int]$TestGroupID = $TestGroupIDList
            }
        }
        
        If (!$TestGroupID) { Throw 'Test Group ID not specified or could not be found' }
        

        If ($TestPointName -And !$TestPointID) { 
            $TestPointIDList = (Get-EPCTestPoint -Name $TestPointName).TestPtID
            If ($TestPointIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified TestPoint name. Please refine your search to return a single testpoint or use TestPointID parameter.' 
            }
            Else {
                $TestPointID = $TestPointIDList
            }
        }
        
        If (!$TestPointID) { Throw 'TestPoint ID not specified or could not be found' }

        
        If ($ResourceGroupName -And !$ResourcegroupID) { $ResourceGroupID = (Get-EPCResourceGroup | Where-Object {$_.RGName -eq $ResourceGroupName}).RGID }
        If (!$ResourceGroupID) { Throw 'Resource Group ID not specified or could not be found' }

        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to add test point to group'
            # Put the endpoint details into an object
            $AgentDetails = New-Object -TypeName EPC.TestGrpMgmt.AgentType
            $AgentDetails.AgentID = $TestPointID
            $AgentDetails.AgentIF = $InterfaceID
            $AgentDetails.AgentIP = $IPAddress
            $AgentDetails.AgentRG = $ResourceGroupID
            
            $EPCAddEndpointToGroupParams = New-Object -TypeName EPC.TestGrpMgmt.AddEndpointToGroupParametersType
            $EPCAddEndpointToGroupParams.credentials = $EPCCred
            $EPCAddEndpointToGroupParams.ToGroupID = $TestGroupID
            $EPCAddEndpointToGroupParams.Item = $AgentDetails
            $EPCAddEndpointToGroupResults = $Global:EPCWSDL_TestGrpMgmt.AddEndpointToGroup($EPCAddEndpointToGroupParams)
            Write-Verbose $EPCAddEndpointToGroupResults.AddEndpointToGroupResponse
        }
        Else {
            Write-Verbose 'Using XML to add test point to group'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                <soapenv:Header/>
                <soapenv:Body>
                <urn:AddEndpointToGroupParameters>
                <credentials>
                  <username>$($Global:EPControllerCred.UserName)</username>
                  <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                <ToGroupID>$TestGroupID</ToGroupID>
                <Agent>
                  <AgentID>$TestPointID</AgentID>
                  <AgentRG>$ResourceGroupID</AgentRG>
                  <AgentIF>$InterfaceID</AgentIF>
                  <AgentIP>$IPAddress</AgentIP>
                </Agent>
                 </urn:AddEndpointToGroupParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $EPCAddEndpointToGroupResults = $XMLResponse.Envelope.Body.AddEndpointToGroupResults
        }
        
        If ($EPCAddEndpointToGroupResults.AddEndpointToGroupResponse -eq 'success') {
            Return $EPCAddEndpointToGroupResults.AddEndpointToGroupResponse
        }
        Else {
            Throw $EPCAddEndpointToGroupResults.AddEndpointToGroupResponse
        }
    }
}    
    


Function Remove-EPCTestGroupMember {
    <#
        .SYNOPSIS
        Removes an EPC test point from an EPC test group
         
        .DESCRIPTION
        Removes an EPC test point from an EPC test group
         
        .PARAMETER GroupName
        The name of the test group to remove the member from
         
        .PARAMETER GroupID
        The ID of the test group to remove the member from
         
        .PARAMETER TestPointName
        The name of the test point to remove from the test group
         
        .PARAMETER TestPointID
        The ID of the test point to remove from the test group
         
        .EXAMPLE
        Remove-EPCTestGroupMember -GroupName MyGroup -TestPointName TPTest
        Removes TPTest from the MyGroup group
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$GroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$GroupID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Name','EndpointName')]
        [string]$TestPointName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('TestPtID','EndpointID')]
        [string]$TestPointID,
        [switch]$UseXML
    )

    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.TestGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($GroupName -And !$GroupID) { 
            $GroupIDList = (Get-EPCTestGroup -SearchString $GroupName).GroupID
            If ($GroupIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified test group name. Please refine your search to return a single test group or use TestGroupID parameter.' 
            }
            Else {
                $GroupID = $GroupIDList
            }
        }
        
        If (!$GroupID) { Throw 'Test Group ID not specified or could not be found' }
        

        If ($TestPointName -And !$TestPointID) { 
            $TestPointIDList = (Get-EPCTestPoint -Name $TestPointName).TestPtID
            If ($TestPointIDList.Count -gt 1) { 
                Throw 'Too many results returned for specified TestPoint name. Please refine your search to return a single testpoint or use TestPointID parameter.' 
            }
            Else {
                $TestPointID = $TestPointIDList
            }
        }
        
        If (!$TestPointID) { Throw 'TestPoint ID not specified or could not be found' }

        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to remove test point from group'
            $EPCDeleteEndpointFromGroupParams = New-Object -TypeName EPC.TestGrpMgmt.DeleteEndpointFromGroupParametersType
            $EPCDeleteEndpointFromGroupParams.credentials = $EPCCred
            $EPCDeleteEndpointFromGroupParams.FromGroupID = $GroupID
            $EPCDeleteEndpointFromGroupParams.DeleteEndpointID = $TestPointID
            $EPCDeleteEndpointFromGroupResults = $Global:EPCWSDL_TestGrpMgmt.DeleteEndpointFromGroup($EPCDeleteEndpointFromGroupParams)
            Write-Verbose $EPCDeleteEndpointFromGroupResults.DeleteEndpointFromGroupResponse

        }
        Else {
            Write-Verbose 'Using XML to remove test point from group'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyTestGroupManagement'>
                <soapenv:Header/>
                <soapenv:Body>
                <urn:DeleteEndpointFromGroupParameters>
                 <credentials>
                  <username>$($Global:EPControllerCred.UserName)</username>
                  <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                 </credentials>
                 <DeleteEndpointID>$TestPointID</DeleteEndpointID>
                 <FromGroupID>$GroupID</FromGroupID>
                 </urn:DeleteEndpointFromGroupParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyTestGroupManagementService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $EPCDeleteEndpointFromGroupResults = $XMLResponse.Envelope.Body.DeleteEndpointFromGroupResults
        }
        
        If ($EPCDeleteEndpointFromGroupResults.DeleteEndpointFromGroupResponse -eq 'success') {
            Return $EPCDeleteEndpointFromGroupResults.DeleteEndpointFromGroupResponse
        }
        Else {
            Throw $EPCDeleteEndpointFromGroupResults.DeleteEndpointFromGroupResponse
        }
    }
}    



Function Find-EPCTestGroupMember {
    <#
        .SYNOPSIS
        Returns the EPC test group that a test point is a member of
         
        .DESCRIPTION
        Returns the EPC test group that a test point is a member of
         
        .PARAMETER GroupNamePrefix
        The starting text of the group names used for the customer
         
        .PARAMETER TestPointName
        The name of the test point to remove from the test group
         
        .EXAMPLE
        Find-EPCTestGroupMember -GroupNamePrefix Contoso -TestPointName TPTest
        Returns the group name the TPTest testpoint is a member of
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$GroupNamePrefix,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('Name','EndpointName')]
        [string]$TestPointName
    )

    Begin {
        $TestGroupList = Get-EPCTestGroup -SearchString $GroupNamePrefix
        $TGResults = @()
    }
    
    Process {
        ForEach ($TG in $TestGroupList) {
            $TGMembers = Get-EPCTestGroupMembers -GroupID $TG.GroupID
            If ($TestPointName -in $TGMembers.EndpointName) {
                $TGResults += [pscustomobject][ordered]@{ 'TestPointName' = $TestPointName; 'GroupName' = $TG.GroupName }
                Break
            }
        }
    }
    End {
        Return $TGResults
    }
}    



#################################################################################################################################################
# #
# EPC Test Plan Functions #
# #
#################################################################################################################################################

Function Get-EPCTestPlan {
    <#
        .SYNOPSIS
        Return a list of EPC test plans
         
        .DESCRIPTION
        Return a list of EPC test plans
         
        .EXAMPLE
        Get-EPCTestPlan
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController

        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListTestPlansParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPlansParametersType
            $EPCListTestPlansParams.credentials = $EPCCred

            $TestPlanResults = $Global:EPCWSDL_ActiveCtrl.listTestPlans($EPCListTestPlansParams)
            
            Write-Verbose $TestPlanResults.result
            
            If ($TestPlanResults.result -eq 'success') {
                Return $TestPlanResults.TestPlanList
            }
            Else {
                Throw $TestPlanResults.result
            }
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPlansParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listTestPlansParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"


            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TestPlanResults = $XMLResponse.Envelope.Body.listTestPlansResults
            
            If ($TestPlanResults.result -eq 'success') {
                [psobject]$TestPlans = $TestPlanResults.TestPlanList.testplan | ConvertFrom-XMLElement
                Return $TestPlans
            }
            Else {
                Throw $TestPlanResults.result
            }
        }
    }
}



#################################################################################################################################################
# #
# EPC Service Functions #
# #
#################################################################################################################################################

Function Get-EPCService {
    <#
        .SYNOPSIS
        Return list of EPC services
          
        .DESCRIPTION
        Return list of EPC services
          
        .PARAMETER ServiceClass
        Limit results to only a specific service class
          
        .EXAMPLE
        Get-EPCService -ServiceClass VOIP
        Returns service information about all VOIP services
  
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('Composite','Interface','IPSec','IPTV','NetApp','NetSVC','NetTrans','Network','Other','System','TCP','VidConf','VOIP', IgnoreCase=$True)]
        [string]$ServiceClass
    )
    
    Begin {
        Connect-EPCController
        
        $EPCCred = New-Object -TypeName EPC.SvcMgmt.CredentialsType
        $EPCCred.username = $Global:EPControllerCred.UserName
        $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
    }
    Process {
        $EPCListServicesParams = New-Object -TypeName EPC.SvcMgmt.ListServicesParametersType
        $EPCListServicesParams.credentials = $EPCCred
        
        If ($ServiceClass) {
            $EPCListServicesParams.serviceClass = $ServiceClass
            $EPCListServicesParams.serviceClassSpecified = $True
        }
        
        $ServiceResults = $Global:EPCWSDL_SvcMgmt.listServices($EPCListServicesParams)
        Write-Verbose $ServiceResults.result
        
        If ($ServiceResults.result -eq 'success') {
            Return $ServiceResults.ServiceList
        }
        Else {
            Throw $ServiceResults.result
        }            
    }
}


#################################################################################################################################################
# #
# EPC Resource Group Functions #
# #
#################################################################################################################################################

Function Get-EPCResourceGroup {
    <#
        .SYNOPSIS
        Return list of EPC resource groups
         
        .DESCRIPTION
        Return list of EPC resource groups
         
        .PARAMETER ParentRGID
        Limit results to only a specific resource group
         
        .EXAMPLE
        Get-EPCResourceGroup
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [int]$ParentRGID,
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 1000,
        [switch]$UseXML
    )
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ResGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListResourceGroupParams = New-Object -TypeName EPC.ResGrpMgmt.ListResourceGroupParametersType
            $EPCListResourceGroupParams.credentials = $EPCCred
            $EPCListResourceGroupParams.maxNumber = $ResultSize
            
            If ($ParentRGID) {
                $RGRelation = New-Object -TypeName EPC.ResGrpMgmt.ResourceGroupRelationshipMapType
                $RGRelation.RGID = $ParentRGID
                $FilterType = New-Object -TypeName EPC.ResGrpMgmt.ResourceGroupFilterType
                $FilterType.ItemElementName = 'RGrelation'
                $FilterType.Item = $RGRelation
                $EPCListResourceGroupParams.filter = $FilterType
            }
            
    
            $ResourceGroupResults = $Global:EPCWSDL_ResGrpMgmt.listResourceGroups($EPCListResourceGroupParams)
            Write-Verbose $ResourceGroupResults.result
            
            If ($ResourceGroupResults.result -eq 'success') {
                Return $ResourceGroupResults.ResourceGroupList
            }
            Else {
                Throw $ResourceGroupResults.result
            }
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyRsrcGroupMgmt'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listResourceGroupParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listResourceGroupParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            # Add filter options if entered
            If ($ParentRGID) {
                $FilterXMLElement = $SOAPReq.Envelope.Body.listResourceGroupParameters.AppendChild($SOAPReq.CreateElement('filter'))
                $RGRelationXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('RGrelation'))
                $RGRelationshipXMLElement = $RGRelationXMLElement.AppendChild($SOAPReq.CreateElement('relationship'))
                $NULL = $RGRelationshipXMLElement.AppendChild($SOAPReq.CreateTextNode('all-children'))
                $RGIDXMLElement = $RGRelationXMLElement.AppendChild($SOAPReq.CreateElement('RGID'))
                $NULL = $RGIDXMLElement.AppendChild($SOAPReq.CreateTextNode($ParentRGID))
            }

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyRsrcGroupMgmtService"
            Write-Verbose $SOAPFQDN
            Write-Verbose $SOAPReq.OuterXML
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $ResourceGroupResults = $XMLResponse.Envelope.Body.listResourceGroupResults

            If ($ResourceGroupResults.result -eq 'success') {
                [psobject]$ResourceGroups = $ResourceGroupResults.ResourceGroupList.RG | ConvertFrom-XMLElement
                Return $ResourceGroups
            }
            Else {
                Throw $ResourceGroupResults.result
            }
        }
    }
}





#################################################################################################################################################
# #
# EPC Test Instance Functions #
# #
#################################################################################################################################################


Function Get-EPCTestPointTestInstance {
    <#
        .SYNOPSIS
        Returns information about test point tests
         
        .DESCRIPTION
        Returns information about test point tests. Can be used to show any running tests
         
        .PARAMETER UUID
        The UUID of the test point to retrieve test information about
         
        .PARAMETER ExecMode
        Filter results by either normal or ad hoc tests
 
        .PARAMETER Status
        Filter results by the given status of a test.
         
        .EXAMPLE
        Get-EPCTestPointTestInstance -UUID d4a1437f-1f18-11ec-cf33-0daedf04882a -Status Running
        Shows running tests associated with the given test point UUID
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$UUID,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Normal','AdHoc', IgnoreCase=$True)]
        [string]$ExecMode,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Aborted','Cancelling','Cancelled','Completed','Pending','Running','Unknown', IgnoreCase=$True)]
        [string]$Status,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListTestPointInstancesParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointTestInstancesParametersType
            $EPCListTestPointInstancesParams.credentials = $EPCCred
        
            If ($UUID) { $EPCListTestPointInstancesParams.TestPTID = $UUID }
            
            If ($ExecMode) {
                $EPCListTestPointInstancesParams.ExecModeFilter = $ExecMode
                $EPCListTestPointInstancesParams.ExecModeFilterSpecified = $True
            }
            
            If ($Status) {
                $EPCListTestPointInstancesParams.StatusFilter = $Status
                $EPCListTestPointInstancesParams.StatusFilterSpecified = $True            
            }

            $TestPointInstancesResults = $Global:EPCWSDL_ActiveCtrl.listTestPointTestInstances($EPCListTestPointInstancesParams)
            
            Write-Verbose $TestPointInstancesResults.result
            
            If ($TestPointInstancesResults.result -eq 'success') {
                Return $TestPointInstancesResults.TestInstanceList
            }
            Else {
                Throw $TestPointInstancesResults.result
            }        
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPointTestInstancesParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listTestPointTestInstancesParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            # Add the testpoint ID to the search if entered
            If ($UUID) {
                $NewXMLElement = $SOAPReq.Envelope.Body.listTestPointTestInstancesParameters.AppendChild($SOAPReq.CreateElement('testptID'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($UUID))    
            }
            
            If ($ExecMode) { # WSDL uses adhoc, XML uses ad-hoc
                If ($ExecMode -eq 'AdHoc') { 
                    $ExecModeLower = 'ad-hoc' 
                }
                Else {
                    $ExecModeLower = 'normal' 
                }
                $NewXMLElement = $SOAPReq.Envelope.Body.listTestPointTestInstancesParameters.AppendChild($SOAPReq.CreateElement('execModeFilter'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($ExecModeLower))    
            }
            
            If ($Status) {
                $NewXMLElement = $SOAPReq.Envelope.Body.listTestPointTestInstancesParameters.AppendChild($SOAPReq.CreateElement('statusFilter'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($Status.ToLower()))    
            }

            Write-Verbose $SOAPReq.OuterXML
            
            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"

            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TPTestResults = $XMLResponse.Envelope.Body.listTestPointTestInstancesResults
            
            If ($TPTestResults.result -eq 'success') {
                Return $TPTestResults.TestInstanceList.testInstance
            }
            Else {
                Throw $TPTestResults.result
            }
        }
    }
}



Function Start-EPCTestInstance {
    <#
        .SYNOPSIS
        Starts an EPC test
         
        .DESCRIPTION
        Starts an EPC test
         
        .PARAMETER SrcTestPointID
        The UUID of the originating test point
         
        .PARAMETER SrcInterface
        The name of the originating network interface
         
        .PARAMETER SrcIPAddress
        The orginating test point's IP address
         
        .PARAMETER SrcResourceGroup
        The originating test point's resource group
 
        .PARAMETER DstTestPointID
        The UUID of the target test point
 
        .PARAMETER DstInterface
        The name of the target network interface
         
        .PARAMETER DstIPAddress
        The orginating test point's IP address
         
        .PARAMETER DstResourceGroup
        The target test point's resource group
 
        .PARAMETER Service
        The service name that contains the test to run
         
        .PARAMETER TestPlan
        The test plan name of the test to run
         
        .EXAMPLE
        Start-EPCTestInstance -SrcTestPointID d4a1437f-1f18-11ec-cf33-0daedf04882a -SrcInterface Ethernet -SrcIPAddress 192.168.1.100 -DstTestPointID b4a4437f-4f48-440c-cf33-0ba0bf04882a -DstInterface Wifi -DstIPAddress 192.168.0.43 -Service Contoso -TestPlan P2P_short
        Stops the given EPC test
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SrcTestPointID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SrcTestPointName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DstTestPointID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DstTestPointName,
        [switch]$UseXML
    )
    DynamicParam {
        # Create the dictionary
        $RuntimeParamDict = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        
        $ParamList = @()
        $Params = [pscustomobject][ordered]@{'ParamName' = 'SrcInterface'; 'Prefix' = 'SIF'; 'ParamRequired' = $FALSE; 'Position' = 2}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'SrcIPAddress'; 'Prefix' = 'SIP'; 'ParamRequired' = $FALSE; 'Position' = 3}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'SrcResourceGroup'; 'Prefix' = 'SRG'; 'ParamRequired' = $FALSE; 'Position' = 4}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'DstInterface'; 'Prefix' = 'DIF'; 'ParamRequired' = $FALSE; 'Position' = 5}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'DstIPAddress'; 'Prefix' = 'DIP'; 'ParamRequired' = $FALSE; 'Position' = 6}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'DstResourceGroup'; 'Prefix' = 'DRG'; 'ParamRequired' = $FALSE; 'Position' = 7}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'TestPlan'; 'Prefix' = 'TP'; 'ParamRequired' = $FALSE; 'Position' = 8}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'Service'; 'Prefix' = 'SRV'; 'ParamRequired' = $FALSE; 'Position' = 9}
        $ParamList += $Params
        
        # Initialize parameter list
        ForEach ($Param In $ParamList) { 
            New-DynamicParam -ParamName $Param.ParamName -Prefix $Param.Prefix -ParamRequired $Param.ParamRequired -Position $Param.Position 
        }
        
        If ($SrcTestPointID) {
            # Generate and set the ValidateSet
            $SrcTestPointIFList = Get-EPCTestPointInterface -UUID $SrcTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')}
            $ValidateSetAttrib_SIF = New-Object System.Management.Automation.ValidateSetAttribute($SrcTestPointIFList.name)
            
            If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $SrcTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach-Object { $_.IFDetail | Select-Object -ExpandProperty Item }
            }
            Else {
                $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $SrcTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach-Object { ($_.IFDetail | Select-Object IPAddress).IPAddress } 
            }    
            
            $ValidateSetAttrib_SIP = New-Object System.Management.Automation.ValidateSetAttribute($SrcTestPointIPList)

            $SrcTestPointRGList = (Get-EPCTestPoint | Where-Object {$_.testptID -eq $SrcTestPointID} | Select-Object RGList).RGList
            If ($SrcTestPointRGList.GetType().FullName -eq 'System.Xml.XmlElement') { $SrcTestPointRGList = $SrcTestPointRGList.rg } # Account for differences in XML vs WSDL
            $ValidateSetAttrib_SRG = New-Object System.Management.Automation.ValidateSetAttribute($SrcTestPointRGList)
        }
        
        If ($DstTestPointID) {
            # Generate and set the ValidateSet
            $DstTestPointIFList = Get-EPCTestPointInterface -UUID $DstTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')}
            $ValidateSetAttrib_DIF = New-Object System.Management.Automation.ValidateSetAttribute($DstTestPointIFList.name)
                        
            If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                $DstTestPointIPList = Get-EPCTestPointInterface -UUID $DstTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach-Object { $_.IFDetail | Select-Object -ExpandProperty Item }
            }
            Else {
                $DstTestPointIPList = Get-EPCTestPointInterface -UUID $DstTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach-Object { ($_.IFDetail | Select-Object IPAddress).IPAddress } 
            }    
            
            $ValidateSetAttrib_DIP = New-Object System.Management.Automation.ValidateSetAttribute($DstTestPointIPList)

            $DstTestPointRGList = (Get-EPCTestPoint | Where-Object {$_.testptID -eq $DstTestPointID} | Select-Object RGList).RGList
            If ($DstTestPointRGList.GetType().FullName -eq 'System.Xml.XmlElement') { $DstTestPointRGList = $DstTestPointRGList.rg } # Account for differences in XML vs WSDL
            $ValidateSetAttrib_DRG = New-Object System.Management.Automation.ValidateSetAttribute($DstTestPointRGList)

            $TestPlanList = Get-EPCTestPlan
            $ValidateSetAttrib_PT = New-Object System.Management.Automation.ValidateSetAttribute($TestPlanList.name)

            $ServiceList = Get-EPCService
            $ValidateSetAttrib_SRV = New-Object System.Management.Automation.ValidateSetAttribute($ServiceList.name)
        }
        
        # Add the ValidateSet to the attributes collection
        $AttribColl_SIF.Add($ValidateSetAttrib_SIF)
        $AttribColl_SIP.Add($ValidateSetAttrib_SIP)
        $AttribColl_SRG.Add($ValidateSetAttrib_SRG)
        $AttribColl_DIF.Add($ValidateSetAttrib_DIF)
        $AttribColl_DIP.Add($ValidateSetAttrib_DIP)
        $AttribColl_DRG.Add($ValidateSetAttrib_DRG)
        $AttribColl_TP.Add($ValidateSetAttrib_PT)
        $AttribColl_SRV.Add($ValidateSetAttrib_SRV)
                
        # Create and return the dynamic parameter
        $RunTimeParam_SIF = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SIF, [string], $AttribColl_SIF)
        $RunTimeParam_SIP = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SIP, [string], $AttribColl_SIP)
        $RunTimeParam_SRG = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SRG, [string], $AttribColl_SRG)
        $RunTimeParam_DIF = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_DIF, [string], $AttribColl_DIF)
        $RunTimeParam_DIP = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_DIP, [string], $AttribColl_DIP)
        $RunTimeParam_DRG = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_DRG, [string], $AttribColl_DRG)
        $RunTimeParam_TP = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_TP, [string], $AttribColl_TP)
        $RunTimeParam_SRV = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SRV, [string], $AttribColl_SRV)
        $RuntimeParamDict.Add($ParamName_SIF, $RunTimeParam_SIF)
        $RuntimeParamDict.Add($ParamName_SIP, $RunTimeParam_SIP)
        $RuntimeParamDict.Add($ParamName_SRG, $RunTimeParam_SRG)
        $RuntimeParamDict.Add($ParamName_DIF, $RunTimeParam_DIF)
        $RuntimeParamDict.Add($ParamName_DIP, $RunTimeParam_DIP)
        $RuntimeParamDict.Add($ParamName_DRG, $RunTimeParam_DRG)
        $RuntimeParamDict.Add($ParamName_TP, $RunTimeParam_TP)
        $RuntimeParamDict.Add($ParamName_SRV, $RunTimeParam_SRV)
        
        Return $RuntimeParamDict
    }

    Begin {
        Connect-EPCController
        
        # If no parameters were entered, then bring up the UI
        If ($PSBoundParameters.Count -eq 0 -Or $SrcTestPointName -Or $DstTestPointName) {
            [bool]$Script:SubmitClicked = $False
            $Script:SrcIPAddress = $NULL
            $Script:DstIPAddress = $NULL
            
            # Check to see if the module was installed to determine the location of the gui.xaml file
            $N10Path = (Get-Module -Name Nectar10 -ErrorAction:SilentlyContinue).Path
            If ($N10Path) {
                $N10Path = (Get-Item $N10Path).DirectoryName
            }
            Else {
                $N10Path = '.'
            }
            
            $inputXML = Get-Content "$N10Path\epc_starttest_gui.xaml"

            $inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
            [void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
            [xml]$XAML = $inputXML

            #Read XAML
            $Reader=(New-Object System.Xml.XmlNodeReader $xaml)
            Try {
                $Form=[Windows.Markup.XamlReader]::Load( $Reader )
            }
            Catch {
                Throw "Unable to parse XML, with error: $($Error[0])`n Ensure that there are NO SelectionChanged or TextChanged properties in your textboxes (PowerShell cannot process them)"
            }
             
            #===========================================================================
            # Load XAML Objects In PowerShell
            #===========================================================================
              
            $xaml.SelectNodes("//*[@Name]") | ForEach-Object {
                Try { Set-Variable -Name "$($_.Name)" -Value $Form.FindName($_.Name) -ErrorAction Stop }
                Catch { Throw }
                }

            # Populate the Test Plan List
            $TestPlanList = Get-EPCTestPlan | Select-Object Name, testPlanID | Sort-Object Name
            ForEach ($TestPlan in $TestPlanList) {
                [void] $TP_DropBox.Items.Add($TestPlan.name)
            }

            # Grab the default value
            $Script:TestPlanID = $TestPlanList[0].testPlanID

            # Populate the Resource Group List
            Try {
                $RGList = Get-EPCResourceGroup | Select-Object RGName, RGID | Sort-Object RGName
                ForEach ($RG in $RGList) {
                    [void] $RG_DropBox.Items.Add($RG.RGName)
                }
            }
            Catch { # Hide the RG list dropdown if user doesn't have permission to view
                $RG_Label.Visibility = 'Hidden'
                $RG_DropBox.Visibility = 'Hidden'            
            }

            # Grab the default value
            $Script:TestPlanID = $TestPlanList[0].testPlanID

            $IPRegEx = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"

            # Populate the Testpoint list
            $Script:TPList = Get-EPCTestPoint | Where-Object {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name

            ForEach ($TestPoint in $Script:TPList) {
                [void] $SrcEndpoint_ListBox.Items.Add($TestPoint.name)
                [void] $DstEndpoint_ListBox.Items.Add($TestPoint.name)
            }

            ########################
            #Add Event Handlers
            ########################
            $TP_DropBox.add_SelectionChanged({
                # Get the selected Test Plan
                $SelectedTPIndex = $TP_DropBox.SelectedIndex
                $Script:TestPlanID = $TestPlanList[$SelectedTPIndex].testPlanID
            })

            $RG_DropBox.add_SelectionChanged({
                # Get the selected Resource Group and set the test point list
                $SelectedRGIndex = $RG_DropBox.SelectedIndex
                $Script:ResourceGroupID = $RGList[$SelectedRGIndex].RGID
                $Script:TPList = Get-EPCTestPoint -RGID $Script:ResourceGroupID | Where-Object {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name
                $SrcEndpoint_ListBox.Items.Clear()
                $DstEndpoint_ListBox.Items.Clear()
                $SrcInterface_ListBox.Items.Clear()
                $DstInterface_ListBox.Items.Clear()
                $SrcIPAddress_ListBox.Items.Clear()
                $DstIPAddress_ListBox.Items.Clear()
                
                ForEach ($TestPoint in $Script:TPList) {
                    [void] $SrcEndpoint_ListBox.Items.Add($TestPoint.name)
                    [void] $DstEndpoint_ListBox.Items.Add($TestPoint.name)
                }
            })

            $SrcEndpoint_TextBox.add_LostFocus({
                $Script:TPList = Get-EPCTestPoint -RGID $Script:ResourceGroupID -Name $($SrcEndpoint_TextBox.text) | Where-Object {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name
                $SrcEndpoint_ListBox.Items.Clear()
                $SrcInterface_ListBox.Items.Clear()
                $SrcIPAddress_ListBox.Items.Clear()
                
                ForEach ($TestPoint in $Script:TPList) {
                    [void] $SrcEndpoint_ListBox.Items.Add($TestPoint.name)
                }
            })


            $SrcEndpoint_ListBox.add_SelectionChanged({
                # Get the selected endpoint
                $SelectedEPIndex = $SrcEndpoint_ListBox.SelectedIndex
                $Script:SrcTestPointID = $Script:TPList[$SelectedEPIndex].testPtID
                $Script:SrcResourceGroupList = $Script:TPList[$SelectedEPIndex].RGList
                
                # Ensure RGList object is a PSObject instead of XML
                If ($Global:EPC_UseWSDL -eq $False -Or $UseXML) { $Script:SrcResourceGroupList = ForEach($Item in $Script:SrcResourceGroupList.RG) { $Item } }
                
                Try {
                    # Populate interface list
                    $SrcInterface_ListBox.Items.Clear()
                    $SrcIPAddress_ListBox.Items.Clear()
                    $SrcInterface_ListBox.IsEnabled = $True

                    $Script:SrcTestPointIFList = Get-EPCTestPointInterface -UUID $Script:SrcTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route') -And $_.ifID -notlike 'Teredo*'} 

                    ForEach ($Interface in $Script:SrcTestPointIFList) {
                        $SrcInterface_ListBox.Items.Add($Interface.name)
                    }

                    # Auto-select the interface if there is only one
                    If (($Script:SrcTestPointIFList | Measure-Object).Count -eq 1) {
                        $SrcInterface_ListBox.SelectedIndex = 0
                    }
                    
                    # Auto-select the 'Any' interface if available
                    If ($Script:SrcTestPointIFList.Name.Contains('Any')) {
                        $SrcInterface_ListBox.SelectedIndex = $Script:SrcTestPointIFList.Name.IndexOf('Any')
                    }
                    
                }
                Catch {
                    $SrcInterface_ListBox.Items.Clear()
                    $SrcIPAddress_ListBox.Items.Clear()
                    $SrcInterface_ListBox.Items.Add("ERROR CONNECTING TO ENDPOINT")
                    $SrcInterface_ListBox.IsEnabled = $False
                }
            })


            $SrcInterface_ListBox.add_SelectionChanged({
                # Get the selected interface
                $SelectedIFIndex = $SrcInterface_ListBox.SelectedIndex
                
                If ($SelectedIFIndex -ge 0) {
                    $Script:SrcInterfaceID = $Script:SrcTestPointIFList[$SelectedIFIndex].IFID

                    # Populate the IP address list
                    If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                        $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $Script:SrcTestPointID | Where-Object {$_.IFID -eq $Script:SrcInterfaceID} | ForEach-Object { $_.IFDetail } | Select-Object Item | Where-Object {$_.Item -match $IPRegEx -Or $_.Item -like 'Any*'}
                    }
                    Else {
                        $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $Script:SrcTestPointID | Where-Object {$_.IFID -eq $Script:SrcInterfaceID} | ForEach-Object { $_.IFDetail } | Select-Object @{Name='Item';Expression={$_.IPAddress}} | Where-Object {$_.Item -match $IPRegEx -Or $_.Item -like 'Any*'}
                    }
                    
                    $SrcIPAddress_ListBox.Items.Clear()

                    ForEach ($IPAddress in $SrcTestPointIPList) {
                        $SrcIPAddress_ListBox.Items.Add($IPAddress.Item)
                    }

                    # Auto-select the IP address if there is only one (usual case)
                    If (($SrcTestPointIPList | Measure-Object).Count -eq 1 -Or $Script:SrcInterfaceID -eq 'Any') {
                       $SrcIPAddress_ListBox.SelectedIndex = 0
                    }
                }
            })


            $SrcIPAddress_ListBox.add_SelectionChanged({
                # Get the selected IP address
                $Script:SrcIPAddress = $SrcIPAddress_ListBox.SelectedValue
                
                If ($Script:SrcIPAddress -and $Script:DstIPAddress) { 
                    $bSubmit.IsEnabled = $True 
                }
                Else {
                    $bSubmit.IsEnabled = $False
                }
            })


            
            $DstEndpoint_TextBox.add_LostFocus({
                $Script:TPList = Get-EPCTestPoint -RGID $Script:ResourceGroupID -Name $($DstEndpoint_TextBox.text) | Where-Object {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name
                $DstEndpoint_ListBox.Items.Clear()
                $DstInterface_ListBox.Items.Clear()
                $DstIPAddress_ListBox.Items.Clear()
                
                ForEach ($TestPoint in $Script:TPList) {
                    [void] $DstEndpoint_ListBox.Items.Add($TestPoint.name)
                }
            })


            $DstEndpoint_ListBox.add_SelectionChanged({
                # Get the selected endpoint
                $SelectedEPIndex = $DstEndpoint_ListBox.SelectedIndex
                $Script:DstTestPointID = $Script:TPList[$SelectedEPIndex].testPtID
                $Script:DstResourceGroupList = $Script:TPList[$SelectedEPIndex].RGList

                # Ensure RGList object is a PSObject instead of XML
                If ($Global:EPC_UseWSDL -eq $False -Or $UseXML) { $Script:DstResourceGroupList = ForEach($Item in $Script:DstResourceGroupList.RG) { $Item } }
                

                # Populate interface list
                Try {
                    $DstInterface_ListBox.Items.Clear()
                    $DstIPAddress_ListBox.Items.Clear()
                    $DstInterface_ListBox.IsEnabled = $True

                    $Script:DstTestPointIFList = Get-EPCTestPointInterface -UUID $Script:DstTestPointID | Where-Object {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route') -And $_.ifID -notlike 'Teredo*'} 

                    ForEach ($Interface in $Script:DstTestPointIFList) {
                        $DstInterface_ListBox.Items.Add($Interface.name)
                    }

                    # Auto-select the interface if there is only one
                    If (($Script:DstTestPointIFList | Measure-Object).Count -eq 1) {
                       $DstInterface_ListBox.SelectedIndex = 0
                    }
                    
                    # Auto-select the 'Any' interface if available
                    If ($Script:DstTestPointIFList.Name.Contains('Any')) {
                        $DstInterface_ListBox.SelectedIndex = $Script:DstTestPointIFList.Name.IndexOf('Any')
                    }                    
                }
                Catch {
                    $DstInterface_ListBox.Items.Clear()
                    $DstIPAddress_ListBox.Items.Clear()
                    $DstInterface_ListBox.Items.Add("ERROR CONNECTING TO ENDPOINT")
                    $DstInterface_ListBox.IsEnabled = $False
                }
            })


            $DstInterface_ListBox.add_SelectionChanged({
                # Get the selected interface
                $SelectedIFIndex = $DstInterface_ListBox.SelectedIndex
                
                If ($SelectedIFIndex -ge 0) {    
                    $Script:DstInterfaceID = $Script:DstTestPointIFList[$SelectedIFIndex].IFID

                    #Populate the IP address list
                    If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                        $DstTestPointIPList = Get-EPCTestPointInterface -UUID $Script:DstTestPointID | Where-Object {$_.IFID -eq $Script:DstInterfaceID} | ForEach-Object { $_.IFDetail } | Select-Object Item | Where-Object {$_.Item -match $IPRegEx -Or $_.Item -like 'Any*'}
                    }
                    Else {
                        $DstTestPointIPList = Get-EPCTestPointInterface -UUID $Script:DstTestPointID | Where-Object {$_.IFID -eq $Script:DstInterfaceID} | ForEach-Object { $_.IFDetail } | Select-Object @{Name='Item';Expression={$_.IPAddress}} | Where-Object {$_.Item -match $IPRegEx -Or $_.Item -like 'Any*'}
                    }
                    
                    $DstIPAddress_ListBox.Items.Clear()

                    ForEach ($IPAddress in $DstTestPointIPList) {
                        $DstIPAddress_ListBox.Items.Add($IPAddress.Item)
                    }

                    # Auto-select the IP address if there is only one (usual case)
                    If (($DstTestPointIPList | Measure-Object).Count -eq 1 -Or $Script:DstInterfaceID -eq 'Any') {
                       $DstIPAddress_ListBox.SelectedIndex = 0
                    }
                }
            })


            $DstIPAddress_ListBox.add_SelectionChanged({
                # Get the selected IP address
                $Script:DstIPAddress = $DstIPAddress_ListBox.SelectedValue

                If ($Script:DstIPAddress -and $Script:SrcIPAddress) { 
                    $bSubmit.IsEnabled = $True 
                    $bSubmit.Background = 'PaleGreen'
                }
                Else {
                    $bSubmit.IsEnabled = $False
                }
            })


            $bSubmit.Add_Click({
                [bool]$Script:SubmitClicked = $True
                $form.Close()
            })

            # Select endpoint if it was entered on the command line
            If ($SrcTestPointName) {
                $SrcEndpoint_TextBox.Text = $SrcTestPointName  # Populate the search box with the name
                # Limit the listbox to show only entries that match the above text
                $Script:TPList = Get-EPCTestPoint -RGID $Script:ResourceGroupID -Name $($SrcEndpoint_TextBox.text) | Where-Object {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name
                $SrcEndpoint_ListBox.Items.Clear()
                $SrcInterface_ListBox.Items.Clear()
                $SrcIPAddress_ListBox.Items.Clear()
                
                ForEach ($TestPoint in $Script:TPList) {
                    [void] $SrcEndpoint_ListBox.Items.Add($TestPoint.name)
                }

                Try {
                    $SrcEndpoint_ListBox.SelectedIndex = $SrcEndpoint_ListBox.Items.IndexOf($SrcTestPointName)
                    $SrcEndpoint_ListBox.SelectionChanged
                    $SrcEndpoint_ListBox.ScrollIntoView($SrcEndpoint_ListBox.Items.GetItemAt($SrcEndpoint_ListBox.SelectedIndex))
                }
                Catch {}
            }    

            If ($DstTestPointName) {
                $DstEndpoint_TextBox.Text = $DstTestPointName # Populate the search box with the name
                # Limit the listbox to show only entries that match the above text
                $Script:TPList = Get-EPCTestPoint -RGID $Script:ResourceGroupID -Name $($DstEndpoint_TextBox.text) | Where-Object {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name
                $DstEndpoint_ListBox.Items.Clear()
                $DstInterface_ListBox.Items.Clear()
                $DstIPAddress_ListBox.Items.Clear()
                
                ForEach ($TestPoint in $Script:TPList) {
                    [void] $DstEndpoint_ListBox.Items.Add($TestPoint.name)
                }

                Try {
                    $DstEndpoint_ListBox.SelectedIndex = $DstEndpoint_ListBox.Items.IndexOf($DstTestPointName)
                    $DstEndpoint_ListBox.SelectionChanged
                    $DstEndpoint_ListBox.ScrollIntoView($DstEndpoint_ListBox.Items.GetItemAt($DstEndpoint_ListBox.SelectedIndex))
                }
                Catch {}
            }    

            #Show the Form
            $form.ShowDialog() | Out-Null

            If ($Script:SubmitClicked) {
                $SrcTestPointID = $Script:SrcTestPointID
                $DstTestPointID = $Script:DstTestPointID
                
                # Set the Resource Group for source and target if it was specified
                If ($Script:ResourceGroupID) {
                    $SrcRG = $Script:ResourceGroupID
                    $DstRG = $Script:ResourceGroupID
                }
                Else { # Find a common resource group and use that for the test
                    $CommonRG = Compare-Object -IncludeEqual -ExcludeDifferent $Script:SrcResourceGroupList $Script:DstResourceGroupList
                    $SrcRG = $CommonRG[0].InputObject
                    $DstRG = $CommonRG[0].InputObject
                }
            }
            Else { Break }
        }
        
        # Write-Host "TestPlanID: $Script:TestPlanID"
        # Write-Host "SrcTPID: $Script:SrcTestPointID"
        # Write-Host "SrcIFID: $Script:SrcInterfaceID"
        # Write-Host "SrcIPID: $Script:SrcIPAddress"
        # Write-Host "DstTPID: $Script:DstTestPointID"
        # Write-Host "DstIFID: $Script:DstInterfaceID"
        # Write-Host "DstIPID: $Script:DstIPAddress"
        # Write-Host "SrcRG: $SrcRG"
        # Write-Host "DstRG: $DstRG"
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    
        If ($PSBoundParameters.Count -gt 0 -And -Not ($SrcTestPointName -Or $DstTestPointName)) {
            # Get the array position of the selected environment
            $SrcIFPos = $SrcTestPointIFList.Name.IndexOf($PsBoundParameters[$ParamName_SIF])
            $DstIFPos = $DstTestPointIFList.Name.IndexOf($PsBoundParameters[$ParamName_DIF])
            $TPPos = $TestPlanList.Name.IndexOf($PsBoundParameters[$ParamName_TP])
            $SRVPos = $ServiceList.Name.IndexOf($PsBoundParameters[$ParamName_SRV])
            
            $SrcInterfaceID = $SrcTestPointIFList[$SrcIFPos].IFID
            Write-Verbose "Src InterfaceID: $SrcInterfaceID"
            $SrcIPAddress = $PsBoundParameters[$ParamName_SIP]
            Write-Verbose "Src IPAddress: $SrcIPAddress"
            $SrcRG = $PsBoundParameters[$ParamName_SRG]
            Write-Verbose "Src RG: $SrcRG"            
            $DstInterfaceID = $DstTestPointIFList[$DstIFPos].IFID
            Write-Verbose "Dst InterfaceID: $DstInterfaceID"
            $DstIPAddress = $PsBoundParameters[$ParamName_DIP]
            Write-Verbose "Dst IPAddress: $DstIPAddress"
            $DstRG = $PsBoundParameters[$ParamName_DRG]
            Write-Verbose "Dst RG: $DstRG"
            $TestPlanID = $TestPlanList[$TPPos].TestPlanID
            Write-Verbose "TestPlanID: $TestPlanID"
            $ServiceID = $ServiceList[$SRVPos].ServiceID
            Write-Verbose "ServiceID: $ServiceID"
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {        
            $EPCSource = New-Object -TypeName EPC.ActiveCtrl.TestPointTargetType
            $EPCTarget = New-Object -TypeName EPC.ActiveCtrl.TestPointTargetType
            
            $EPCSource.TestPointID = $SrcTestPointID
            $EPCSource.ifID = $SrcInterfaceID
            $EPCSource.IPAddress = $SrcIPAddress
            $EPCSource.RG = $SrcRG

            $EPCTarget.TestPointID = $DstTestPointID
            $EPCTarget.ifID = $DstInterfaceID
            $EPCTarget.IPAddress = $DstIPAddress
            $EPCTarget.RG = $DstRG

            $EPCStartTestPlanTPtoTPParams = New-Object -TypeName EPC.ActiveCtrl.startTestPlanParamsTPtoTPType
            $EPCStartTestPlanTPtoTPParams.TestPlanID = $TestPlanID
            $EPCStartTestPlanTPtoTPParams.Originator = $EPCSource
            $EPCStartTestPlanTPtoTPParams.TPTarget = $EPCTarget
            
            If ($Service) { Write-Verbose "Selected Service: $ServiceID"; $EPCStartTestPlanTPtoTPParams.ServiceID = $ServiceID }    
            
            $EPCStartTestPlanParams = New-Object -TypeName EPC.ActiveCtrl.startTestPlanParametersType
            $EPCStartTestPlanParams.credentials = $EPCCred
            $EPCStartTestPlanParams.Items = $EPCStartTestPlanTPtoTPParams
            $EPCStartTestPlanParams.ItemsElementName = 'tp2tptest'
            
            $StartTestPlanResults = $Global:EPCWSDL_ActiveCtrl.startTestPlan($EPCStartTestPlanParams)
            Write-Verbose $StartTestPlanResults.result
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:startTestPlanParameters>
                 <credentials>
                 <username>$($Global:EPControllerCred.UserName)</username>
                 <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                 </credentials>
                 <tp2tpTest>
                 <testplanID>$TestPlanID</testplanID>
                 <originator>
                 <testpointID>$SrcTestPointID</testpointID>
                 <ifID>$SrcInterfaceID</ifID>
                 <ipAddress>$SrcIPAddress</ipAddress>
                 <RG>$SrcRG</RG>
                 </originator>
                 <tpTarget>
                 <testpointID>$DstTestPointID</testpointID>
                 <ifID>$DstInterfaceID</ifID>
                 <ipAddress>$DstIPAddress</ipAddress>
                 <RG>$DstRG</RG>
                 </tpTarget>
                 </tp2tpTest>
                 </urn:startTestPlanParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq.OuterXML

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $StartTestPlanResults = $XMLResponse.Envelope.Body.StartTestPlanResults
        }
        
        If ($StartTestPlanResults.result -eq 'success') {
            Return "Successfully started test`n`rTestPointID: $SrcTestPointID`n`rInstance ID: $($StartTestPlanResults.instanceID)"
        }
        Else {
            Return $StartTestPlanResults.result
        }
    }
}



Function Stop-EPCTestInstance {
    <#
        .SYNOPSIS
        Stops a running EPC test
         
        .DESCRIPTION
        Stops a running EPC test
         
        .PARAMETER InstanceID
        The InstanceID of the test to stop
         
        .EXAMPLE
        Stop-EPCTestInstance -InstanceID MDNkMGQxZGQ4YzVjY2VlYmRkZTBlYjA2MWRlLTg3MjI=<
        Stops the given EPC test
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$InstanceID,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCStopTestPlanParams = New-Object -TypeName EPC.ActiveCtrl.StopTestPlanParametersType
            $EPCStopTestPlanParams.credentials = $EPCCred
            $EPCStopTestPlanParams.instanceID = $InstanceID

            $StopTestPlanResult = $Global:EPCWSDL_ActiveCtrl.stopTestPlan($EPCStopTestPlanParams)
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:stopTestPlanParameters>
                <credentials>
                 <username>$($Global:EPControllerCred.UserName)</username>
                 <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 <instanceID>$InstanceID</instanceID>
                 </urn:stopTestPlanParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq.OuterXML

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $StopTestPlanResult = $XMLResponse.Envelope.Body.stopTestPlanResults
        }
        
        If ($StopTestPlanResult.result -eq 'success') { 
            Return 'Test stopped successfully'
        }
        Else {
            Throw $StopTestPlanResult.result
        }
    }
}



#################################################################################################################################################
# #
# EPC Performance Functions #
# #
#################################################################################################################################################

Function Get-EPCControllerUUID {
    [cmdletbinding()]
    Param (
        [switch]$UseXML
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $FQDN = "https://$Global:EPControllerFQDN/components.do?action=4&type=3"
        $Result = Invoke-RestMethod -Method GET -Uri $FQDN -WebSession $Global:EPCSessionCookie
        
        $ControllerUUID = $Result.data.uuid
                
        If ($ControllerUUID) {
            Return $ControllerUUID
        }
        Else {
            Write-Error "Could not obtain controller UUID"
        }
    }
}



Function Get-EPCBulkExportJobs {
    <#
        .SYNOPSIS
        Return list of EPC bulk export jobs
         
        .DESCRIPTION
        Return list of EPC bulk export jobs
         
        .EXAMPLE
        Get-EPCBulkExportJobs
        Returns a list of all EPC bulk export jobs
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.BulkExport.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            #$ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListExportJobsParams = New-Object -TypeName EPC.BulkExport.ListExportJobsType
            $EPCListExportJobsParams.credentials = $EPCCred
            
            $ExportJobResults = $Global:EPCWSDL_BulkExport.listExportJobs($EPCListExportJobsParams)
            Write-Verbose $ExportJobResults.result
            
            If ($ExportJobResults.result -eq 'success') {
                Return $ExportJobResults.jobList
            }
            Else {
                Throw $ExportJobResults.result
            }            
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyBulkExport'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listExportJobsParameters>>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listExportJobsParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyBulkExportService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $ExportJobResults = $XMLResponse.Envelope.Body.listExportJobsResults
            
            If ($ExportJobResults.result -eq 'success') {
                #[psobject]$JobList = $ExportJobResults.jobList.job | ConvertFrom-XMLElement
                Return $ExportJobResults.jobList.job
            }
            Else {
                Throw $ExportJobResults.result
            }    
        }
    }
}



Function Get-EPCBulkExportJobStatus {
    <#
        .SYNOPSIS
        Return the status of a given EPC bulk export job
         
        .DESCRIPTION
        Return the status of a given EPC bulk export job
         
        .PARAMETER JobID
        The JobID of the bulk export job to retrieve status
         
        .EXAMPLE
        Get-EPCBulkExportJobStatus -JobID 1
        Returns the export job status of bulk export job ID 1
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [int]$JobID,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.SvcMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCBulkExportStatusParams = New-Object -TypeName EPC.SvcMgmt.statusExportJobParameters>
            $EPCBulkExportStatusParams.credentials = $EPCCred
            
            $EPCBulkExportStatusParams.jobID = $JobID

            $ServiceResults = $Global:EPCWSDL_SvcMgmt.listServices($EPCListServicesParams)
            Write-Verbose $ServiceResults.result
            
            If ($ServiceResults.result -eq 'success') {
                Return $ServiceResults.ServiceList
            }
            Else {
                Throw $ServiceResults.result
            }            
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemySvcMgmt'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listServicesParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listServicesParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            # Add the testpoint ID to the search if entered
            If ($ServiceClass) {
                $NewXMLElement = $SOAPReq.Envelope.Body.listServicesParameters.AppendChild($SOAPReq.CreateElement('serviceClass'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($ServiceClass.ToLower()))    
            }

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemySvcMgmtService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $ServiceResults = $XMLResponse.Envelope.Body.listServicesResults
            
            If ($ServiceResults.result -eq 'success') {
                [psobject]$Services = $ServiceResults.ServiceList.service | ConvertFrom-XMLElement
                Return $Services
            }
            Else {
                Throw $ServiceResults.result
            }    
        }
    }
}



Function Get-EPCControllerPerfData {
    <#
        .SYNOPSIS
        Returns a list of performance metrics for a given EPC controller
         
        .DESCRIPTION
        Returns a list of performance metrics for a given EPC controller
 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-EPCControllerPerfData
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("testptID")]
        [string]$TestPointID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TestPointName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_DAY',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [datetimeoffset]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [datetimeoffset]$TimePeriodTo,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('Reporter','Controller','Agent', IgnoreCase=$True)]
        [string]$AppType,        
        [Parameter(Mandatory=$False)]
        [string]$SortColumn = 'evt_date',        
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        # Get the time period to return stats for
        Switch ($TimePeriod) {
            'LAST_HOUR' { $TimePeriodFrom = [DateTimeOffset]::Now.AddHours(-1); $TimePeriodTo = [DateTimeOffset]::Now }
            'LAST_DAY' { $TimePeriodFrom = [DateTimeOffset]::Now.AddDays(-1); $TimePeriodTo = [DateTimeOffset]::Now }
            'LAST_WEEK' { $TimePeriodFrom = [DateTimeOffset]::Now.AddDays(-7); $TimePeriodTo = [DateTimeOffset]::Now }
            'LAST_MONTH' { $TimePeriodFrom = [DateTimeOffset]::Now.AddMonths(-1); $TimePeriodTo = [DateTimeOffset]::Now }
        }
        
        If (!$TimePeriodTo) { $TimePeriodTo = [DateTimeOffset]::Now }
        
        $Body.Add('beginsecs', $TimePeriodFrom.ToUnixTimeSeconds())
        $Body.Add('endsecs', $TimePeriodTo.ToUnixTimeSeconds())
        
        # If Testpoint name/id not provided, assume we're getting controller stats
        If (!$TestPointID -and !$TestPointName) {
            # Get the UUID of the controller
            $FQDN = "https://$Global:EPControllerFQDN/components.do?action=4&type=3"
            $Result = Invoke-RestMethod -Method GET -Uri $FQDN -WebSession $Global:EPCSessionCookie
            $ControllerUUID = $Result.data.uuid

            # Get the database response time
        }
    }
}




Function Get-EPCLicense {
    <#
        .SYNOPSIS
        Returns a list of EPC licenses
         
        .DESCRIPTION
        Returns a list of EPC licenses
 
        .EXAMPLE
        Get-EPCLicense
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [switch]$UseXML
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $LicFQDN = "https://$Global:EPControllerFQDN/admin/licensecfg.do?action=12"
        $LicResult = Invoke-RestMethod -Method GET -Uri $LicFQDN -WebSession $Global:EPCSessionCookie

        $FormattedResults = $LicResult.pts | Select-Object `
                                            @{Name='Product';Expression={$_.product}},`
                                            @{Name='LicenseKey';Expression={$_.lk}},`
                                            @{Name='LastUpdate';Expression={(([System.DateTimeOffset]::FromUnixTimeSeconds($_.update)).LocalDateTime).ToString("yyyy-MM-dd HH:mm:ss")}},`
                                            @{Name='Expiry';Expression={If ($_.expiry -gt 0) { (([System.DateTimeOffset]::FromUnixTimeSeconds($_.expiry)).LocalDateTime).ToString("yyyy-MM-dd HH:mm:ss")}}},`
                                            status, host, id, `
                                            @{Name='AutoAssign';Expression={[regex]::Match($_.aV, 'autoAssign=(true|false)').captures.Groups[1].value }}
    }
    End {
        Return $FormattedResults
    }
}



Function Get-EPCLicenseDetails {
    <#
        .SYNOPSIS
        Returns details about a given EPC license
         
        .DESCRIPTION
        Returns details about a given EPC license. Accepts pipeline input from Get-EPCLicense
 
        .PARAMETER LicenseKey
        The license key of the license to retrieve
 
        .PARAMETER ID
        The id of the license to retrieve
         
        .EXAMPLE
        Get-EPCLicenseDetails -LicenseKey abcdef -ID 1
        Returns EPC license details about license key abcdef
 
        .EXAMPLE
        Get-EPCLicense | Where {$_.Product -like 'DVQ*'} | Get-EPCLicenseDetails
        Returns detailed license information about any license that starts with DVQ
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$LicenseKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ID,
        [switch]$UseXML
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
        [System.Collections.ArrayList]$LicMembers = @()
    }
    Process {
        Write-Verbose "LicenseKey: $LicenseKey"
        Write-Verbose "ID: $ID"
        $LicFQDN = "https://$Global:EPControllerFQDN/admin/viewlic.htm?licensekey=$LicenseKey&lkeyid=$ID"

        If ($PSItem.AutoAssign) { $LicFQDN = $LicFQDN + "&autoAssign=$($PSItem.AutoAssign)" } # For whatever reason, the value of the AutoAssign parameter is passed to the next page via the first page.

        Write-Verbose $LicFQDN
        $LicResult = Invoke-WebRequest -Method GET -Uri $LicFQDN -WebSession $Global:EPCSessionCookie        
        $Item = [PSCustomObject][Ordered]@{}

        # Parse out tables and focus on the table which contains all the data we want
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL method'
            $Tables = $LicResult.ParsedHtml.body.getElementsByTagName('Table')
            $LicMemberRows = $Tables[0].rows | Select-Object InnerHTML # Get the row data innerHTML, which contains the data we want
        } Else { # PS v.6+ doesn't have ParsedHtml in Invoke-WebRequest, so we have to use different method
            $Tables = $LicResult.Content | ConvertFrom-Html
            $Tables = $Tables.SelectNodes("//table")
            $LicMemberRows = $Tables[0].SelectNodes("//tr")
        }

        $LicMemberRows = $LicMemberRows[1..($LicMemberRows.count - 2)] # Ignore the first row and the last 2 rows
        Write-Verbose "RowCount: $($LicMemberRows.Count)"
        
        # Parse each row and pull out the data which is in <TD></TD> blocks into a custom object
        ForEach ($Row in $LicMemberRows) {
            $RowData = $Row.InnerHTML
            $RowMatch = [regex]::Match($RowData, '^\s*<(TD|td) [\w=":;% ]+>([\w\ ]+)</(TD|td)>\s+<(TD|td)[\w=":;% ]*>([\w\s\-\.,<>:;&/]+)</(TD|td)>').captures
            $ItemName = $RowMatch.groups[2].value
            $ItemValue = $RowMatch.groups[5].value

            If ($ItemValue -eq ' ') { # If the item value is empty, it might be a date value provided via inline javascript. Try to get the value from there
                $RowMatchDate = [regex]::Match($RowData, 'var _(expire|update) = (\d+)').captures
                Try {
                    If ($NULL -ne $RowMatchDate) { 
                        $RowMatchDate = $RowMatchDate.groups[2].value
                        If ($RowMatchDate -gt 0) { 
                            $ItemValue = (([System.DateTimeOffset]::FromUnixTimeSeconds($RowMatchDate)).LocalDateTime).ToString("yyyy-MM-dd HH:mm:ss") 
                        } 
                    } 
                }
                Catch {
                    $ItemValue = ''
                }
            }

            $Item | Add-Member -NotePropertyName $ItemName -NotePropertyValue $ItemValue
        }
        $LicMembers += $Item
    }
    End {
        Return $LicMembers
    }
}





#################################################################################################################################################
#################################################################################################################################################
## ##
## Nectar CX Functions ##
## ##
#################################################################################################################################################
#################################################################################################################################################

#################################################################################################################################################
# #
# CX Connection Functions #
# #
#################################################################################################################################################

Function Connect-NCXCloud {
    <#
        .SYNOPSIS
        Connects to Nectar CX cloud and store the credentials for later use.
 
        .DESCRIPTION
        Connects to Nectar CX cloud and store the credentials for later use.
         
        .PARAMETER CloudFQDN
        The FQDN of the Nectar CX cloud.
 
        .PARAMETER TenantName
        The name of a Nectar DXP cloud tenant to connect to and use for subsequent commands. Only useful for multi-tenant deployments
         
        .PARAMETER Credential
        The credentials used to access the Nectar DXP UI. Normally in username@domain.com format
         
        .PARAMETER StoredCredentialTarget
        Use stored credentials saved via New-StoredCredential. Requires prior installation of CredentialManager module via Install-Module CredentialManager, and running:
        Get-Credential | New-StoredCredential -Target MyCXCreds -Persist LocalMachine
         
        .PARAMETER EnvFromFile
        Use a CSV file called NCXEnvList.csv located in the user's default Documents folder to show a list of environments to select from. Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if N10EnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default stored credential target to use for the selected environment. Requires prior installation and configuration of CredentialManager PS add-in.
        N10EnvList.csv must have a header with three columns defined as "Environment, DefaultTenant, StoredCredentialTarget".
        Each environment and StoredCredentialTarget (if used) should be on their own separate lines
         
        .EXAMPLE
        $Cred = Get-Credential
        Connect-NCX -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar CX cloud using the credentials supplied to the Get-Credential command
         
        .EXAMPLE
        Connect-NCX-CloudFQDN contoso.nectar.services -StoredCredentialTarget MyCXCreds
        Connects to contoso.nectar.services Nectar CX cloud using previously stored credentials called MyCXCreds
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                $True
            } 
            Else {
                Throw "ERROR: Nectar CX cloud name must be in FQDN format."
            }
        })]
        [string]$CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential]$Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$StoredCredentialTarget
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\CXEnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }
    
    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $CloudFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "CloudFQDN: $CloudFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($CloudFQDN)
                
                # Check for default tenant in N10EnvList.csv and use if available, but don't override if user explicitly set the TenantName
                If (!$PsBoundParameters['TenantName']) {
                    $TenantName = $EnvSet[$EnvPos].DefaultTenant
                    Write-Verbose "DefaultTenant: $TenantName"
                }
                
                # Check for stored credential target in N10EnvList.csv and use if available
                $StoredCredentialTarget = $EnvSet[$EnvPos].StoredCredentialTarget
                Write-Verbose "StoredCredentialTarget: $StoredCredentialTarget"
            }
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line
        If ((-not $Global:NCXCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = Read-Host "Enter the Nectar DXP cloud FQDN"
        }
        ElseIf (($Global:NCXCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = $Global:NCXCloud
        }
        
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+)$"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
        $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        
        # Ask for credentials if global Nectar creds aren't available
        If (((-not $Global:NCXCred) -And (-not $Credential)) -Or (($Global:NCXCloud -ne $CloudFQDN) -And (-Not $Credential)) -And (-Not $StoredCredentialTarget)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:NCXCred -And (-not $Credential)) {
            $Credential = $Global:NCXCred
        }
        
        # Pull stored credentials if specified
        If ($StoredCredentialTarget) {
            Try {
                $Credential = Get-StoredCredential -Target $StoredCredentialTarget
            }
            Catch {
                Write-Error "Cannot find stored credential for target: $StoredCredentialTarget"
            }
        }
        
        If ((-not $Global:NCXCred) -Or (-not $Global:NCXCloud) -Or ($Global:NCXCloud -ne $CloudFQDN)) {
            # First check and notify if updated Nectar PS module available
            [string]$InstalledNectarPSVer = (Get-InstalledModule -Name Nectar10 -ErrorAction SilentlyContinue).Version
            
            If ($InstalledNectarPSVer -gt 0) {
                [string]$LatestNectarPSVer = (Find-Module Nectar10).Version
                If ($LatestNectarPSVer -gt $InstalledNectarPSVer) {
                    Write-Host "=============== Nectar PowerShell module version $LatestN10Ver available ===============" -ForegroundColor Yellow
                    Write-Host "You are running version $InstalledNectarPSVer. Type " -ForegroundColor Yellow -NoNewLine
                    Write-Host 'Update-Module Nectar10' -ForegroundColor Green -NoNewLine
                    Write-Host ' to update.' -ForegroundColor Yellow
                }
            }
            
            # Attempt connection to tenant
            $URI = "https://$CloudFQDN/cyclone-portlet/api/organisation/"
            Write-Verbose $URI
            $WebRequest = Invoke-WebRequest -Uri $URI -Method GET -Credential $Credential -UseBasicParsing -SessionVariable NectarSession
            
            If ($WebRequest.StatusCode -ne 200) {
                Write-Error "Could not connect to $CloudFQDN using $($Credential.UserName)"
            }
            Else {
                Write-Host -ForegroundColor Green "Successful connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$CloudFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow ($Credential).UserName
                $Global:NCXCloud = $CloudFQDN
                $Global:NCXCred = $Credential
                $Global:NCXSession = $NectarSession
                
                # If there is only one available tenant, assign that to the NCXTenantName global variable
                $TenantList = $WebRequest | ConvertFrom-Json
                If ($TenantList.Count -eq 1) { 
                    $Global:NCXTenantName = $TenantList.name 
                    $Global:NCXOrgID = $TenantList.ID
                }            
            }
        }
        
        # Check to see if tenant name was entered and set global variable, if valid.
        If ($TenantName) {
            $URI = "https://$Global:NCXCloud/cyclone-portlet/api/organisation/"
            Write-Verbose $URI
            $TenantList = Invoke-RestMethod -Method GET -Credential $Global:NCXCred -uri $URI
            Try {
                If ($TenantList.name -Contains $TenantName) {
                    $Global:NCXTenantName = ($TenantList | Where-Object {$_.name -eq $TenantName}).name
                    $Global:NCXOrgID = ($TenantList | Where-Object {$_.name -eq $TenantName}).id
                    Write-Host -ForegroundColor Green "Successsfully set the tenant name to " -NoNewLine
                    Write-Host -ForegroundColor Yellow $Global:NCXTenantName -NoNewLine
                    Write-Host -ForegroundColor Green " (OrgID=" -NoNewLine
                    Write-Host -ForegroundColor Yellow $Global:NCXOrgID -NoNewLine 
                    Write-Host -ForegroundColor Green "). This tenantname will be used in all subsequent commands."
                }
                Else {
                    $TenantList | ForEach-Object{ $TList += ($(If($TList){", "}) + $_.name) }
                    Write-Error "Could not find a tenant with the name $TenantName on https://$Global:NCXCloud. Select one of $TList"
                }
            }
            Catch {
                Write-Error "Invalid tenant name on https://$Global:NCXCloud"
            }
        }
        ElseIf ($PSBoundParameters.ContainsKey('TenantName')) { # Remove the NCXTenantName global variable only if TenantName is explicitly set to NULL
            Remove-Variable NCXTenantName -Scope Global -ErrorAction:SilentlyContinue
            Remove-Variable NCXOrgID -Scope Global -ErrorAction:SilentlyContinue
        }
    }
}



Function Disconnect-NCXCloud {
    <#
        .SYNOPSIS
        Disconnects from any active Nectar CX connection
         
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables
 
        .EXAMPLE
        Disconnect-NCXCloud
        Disconnects from all active connections to Nectar DXP tenants
 
        .NOTES
        Version 1.0
    #>

    [Alias("dnc")]
    [cmdletbinding()]
    param ()
    
    Remove-Variable NCXCred -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXCloud -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXSession -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXTenantName -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NCXOrgID -Scope Global -ErrorAction:SilentlyContinue

    Write-Verbose "Successfully disconnected from Nectar CX cloud"
}




Function Get-NCXCloudInfo {
    <#
        .SYNOPSIS
        Shows information about the active Nectar CX connection
         
        .DESCRIPTION
        Shows information about the active Nectar CX connection
 
        .EXAMPLE
        Get-NCXCloud
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    param ()
    
    $CloudInfo = "" | Select-Object -Property CloudFQDN, Credential
    $CloudInfo.CloudFQDN = $Global:NCXCloud
    $CloudInfo.Credential = ($Global:NCXCred).UserName
    $CloudInfo | Add-Member -TypeName 'Nectar.CloudInfo'
    
    Try {
        $TenantCount = Get-NCXTenantNames
        If ($TenantCount.Count -gt 1) {
            If ($Global:NCXTenantName) {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $Global:NCXTenantName
                $CloudInfo | Add-Member -NotePropertyName 'OrgID' -NotePropertyValue $Global:NCXOrgID
            }
            Else {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue '<Not Set>'
                $CloudInfo | Add-Member -NotePropertyName 'OrgID' -NotePropertyValue '<Not Set>'
            }
        }
    }
    Catch {
    }
    
    Return $CloudInfo
}




#################################################################################################################################################
# #
# Other CX Functions #
# #
#################################################################################################################################################


Function Get-NCXTenantNames {
    <#
        .SYNOPSIS
        Shows all the available Nectar CX tenants on the cloud host.
         
        .DESCRIPTION
        Shows all the available Nectar CX tenants on the cloud host. Only available for multi-tenant deployments.
 
        .EXAMPLE
        Get-NCXTenantNames
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    [alias('Get-NCXOrganization')]
    param ()
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NCXCloud/cyclone-portlet/api/organisation/"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI
            Return $JSON
        }
        Catch {
            Write-Error 'No data found or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NCXCampaign {
    <#
        .SYNOPSIS
        Shows all Nectar CX campaigns
         
        .DESCRIPTION
        Shows all Nectar CX campaigns
 
        .EXAMPLE
        Get-NCXCampaign
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NCXCloud/cyclone-portlet/api/campaigns/getCampaigns/$Global:NCXOrgID/$PlanType"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI
            Return $JSON
        }
        Catch {
            Write-Error 'No data or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NCXTestCase {
    <#
        .SYNOPSIS
        Shows all Nectar CX test cases within the tenant
         
        .DESCRIPTION
        Shows all Nectar CX test cases within the tenant
 
        .EXAMPLE
        Get-NCXTestCase
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            $URI = "https://$Global:NCXCloud/cyclone-portlet/api/testCases/getTestCases/$Global:NCXOrgID/$PlanType"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI
            Return $JSON
        }
        Catch {
            Write-Error 'No data or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





Function Get-NCXAlarm {
    <#
        .SYNOPSIS
        Shows NCX alarms
         
        .DESCRIPTION
        Shows NCX alarms
 
        .EXAMPLE
        Get-NCXAlarm
 
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary', Position = 0)]
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime', Position = 0)]
        [ValidateSet('Organization', 'TestCase', 'CalledNumber', 'TestSuite', IgnoreCase=$True)]
        [string]$AlarmType,
        [Parameter(Mandatory=$False, ParameterSetName = 'Summary')]
        [ValidateSet('LAST_1HR', 'LAST_4HR', 'LAST_12HR', 'LAST_24HR', 'CURRENT_WEEK', 'CURRENT_MONTH', 'LAST_30DAYS', IgnoreCase=$True)]
        [string]$TimePeriod,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodTo
    )
    DynamicParam {
        $ParamDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
        Switch ($AlarmType) {
            {$_ -in 'Organization', 'CalledNumber'} {
                # Define parameter attributes for PlanType attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ValidateSetAttributes = New-Object System.Management.Automation.ValidateSetAttribute('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND')
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $ParamAttributesCollect.Add($ValidateSetAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('PlanType', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('PlanType', $DynParam1)
            }
            'TestCase' {
                # Define parameter attributes for TestCaseID attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('TestCaseID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('TestCaseID', $DynParam1)
            }
            'CalledNumber' {
                # Define parameter attributes for CalledNumber attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 2
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('CalledNumber', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('CalledNumber', $DynParam1)
            }            
            'TestSuite' {
                # Define parameter attributes for TestSuiteID attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 2
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('TestSuiteID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('TestSuiteID', $DynParam1)
            }
        }
        Return $ParamDictionary
    }
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        $Body = @{}
        Try {
            Switch ($AlarmType) {
                'Organization' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-notification/result/organization/$Global:NCXOrgID/$($PSBoundParameters['PlanType'])"; Break }
                'TestCase' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-notification/result/test-case/$($PSBoundParameters['TestCaseID'])"; Break }
                'CalledNumber' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-notification/result/called-number/$($PSBoundParameters['CalledNumber'])/$($PSBoundParameters['PlanType'])"; Break }
                'TestSuite' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-notification/result/test-suite/$($PSBoundParameters['TestSuiteID'])" }
            }
            Write-Verbose $URI
            
            If ($TimePeriod) { $Body.Add('duration', $TimePeriod) }
            If ($TimePeriodFrom) { 
                $Body.Add('startDate', $TimePeriodFrom.ToString('dd-MM-yyyy'))
                $Body.Add('endDate', $TimePeriodTo.ToString('dd-MM-yyyy'))
            }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI -Body $Body        
            Return $JSON
        }
        Catch {
            Write-Error 'No data or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NCXHistoricalReport {
    <#
        .SYNOPSIS
        Shows NCX historical reports
         
        .DESCRIPTION
        Shows NCX historical reports
 
        .EXAMPLE
        Get-NCXHistoricalReports
 
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary', Position = 0)]
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime', Position = 0)]
        [ValidateSet('Organization', 'SingleCampaignRun', 'AllCampaignRun', 'Campaign', IgnoreCase=$True)]
        [string]$ReportType,
        [Parameter(Mandatory=$False, ParameterSetName = 'Summary')]
        [ValidateSet('LAST_1HR', 'LAST_4HR', 'LAST_12HR', 'LAST_24HR', 'CURRENT_WEEK', 'CURRENT_MONTH', 'LAST_30DAYS', IgnoreCase=$True)]
        [string]$TimePeriod,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$True, ParameterSetName = 'SummaryTime')]
        [DateTime]$TimePeriodTo
    )
    DynamicParam {
        $ParamDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
        Switch ($ReportType) {
            'Organization' {
                # Define parameter attributes for PlanType attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ValidateSetAttributes = New-Object System.Management.Automation.ValidateSetAttribute('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND')
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $ParamAttributesCollect.Add($ValidateSetAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('PlanType', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('PlanType', $DynParam1)
                Break
            }
            {$_ -like '*CampaignRun'} {
                # Define parameter attributes for TestCaseID attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 1
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('TestSuiteRunResultID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('TestSuiteRunResultID', $DynParam1)
                Break
            }
            'Campaign' {
                # Define parameter attributes for CalledNumber attribute
                $ParamAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
                $ParamAttributes.Mandatory = $True
                $ParamAttributes.Position = 2
                $ParamAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
                $ParamAttributesCollect.Add($ParamAttributes)
                $DynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('CampaignID', [string], $ParamAttributesCollect)
                $ParamDictionary.Add('CampaignID', $DynParam1)
            }            
        }
        Return $ParamDictionary
    }
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        $Body = @{}
        Try {
            Switch ($ReportType) {
                'Organization' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/test-suite-run-result/organisation/$Global:NCXOrgID/$($PSBoundParameters['PlanType'])"; Break }
                'SingleCampaignRun' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/test-suite-run-result/summary/$($PSBoundParameters['TestSuiteRunResultID'])"; Break }
                'AllCampaignRun' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/test-suite-run-result/test-case-result/$($PSBoundParameters['TestSuiteRunResultID'])"; Break }
                'Campaign' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/test-suite-run-result/test-case-run-result/$($PSBoundParameters['CampaignID'])" }
            }

            Write-Verbose $URI
            
            If ($TimePeriod) { $Body.Add('duration', $TimePeriod) }
            If ($TimePeriodFrom) { 
                $Body.Add('startDate', $TimePeriodFrom.ToString('dd-MM-yyyy'))
                $Body.Add('endDate', $TimePeriodTo.ToString('dd-MM-yyyy'))
            }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI -Body $Body        
            Return $JSON.data
        }
        Catch {
            Write-Error 'No data or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NCXLiveReport {
    <#
        .SYNOPSIS
        Shows CX live report information.
         
        .DESCRIPTION
        Shows CX live report information.
 
        .EXAMPLE
        Get-NCXLiveReport
 
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary')]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType,
        [Parameter(Mandatory=$True, ParameterSetName = 'Detail')]
        [ValidateSet('Voice', 'PESQ', 'MOS', IgnoreCase=$True)]
        [string]$ReportType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True, ParameterSetName = 'Detail')]
        [Alias("id")]
        [string]$DashboardID    
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            Switch ($ReportType) {
                'Voice' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/dashboard/realtime-voice-channel/$DashboardID"; Break }
                'PESQ' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/dashboard/realtime-pesq/$DashboardID"; Break }
                'MOS' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/dashboard/realtime-mos/$DashboardID"; Break }
                default { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/dashboard/$Global:NCXOrgID/$PlanType"}
            }
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI
            Return $JSON
        }
        Catch {
            Write-Error 'No data or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NCXAlarmConfig {
    <#
        .SYNOPSIS
        Shows CX email alarm configuration.
         
        .DESCRIPTION
        Shows CX email alarm configuration.
 
        .EXAMPLE
        Get-NCXAlarmConfig
 
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False, DefaultParameterSetName = 'Summary')]
    Param (
        [Parameter(Mandatory=$True, ParameterSetName = 'Summary')]
        [ValidateSet('RADAR', 'EXPRESS', 'VORTEX', 'CYCLONE', 'INBOUND', IgnoreCase=$True)]
        [string]$PlanType,
        [Parameter(Mandatory=$True, ParameterSetName = 'Detail')]
        [ValidateSet('TestCase', 'CalledNumber', 'TestSuite', IgnoreCase=$True)]
        [string]$AlarmType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True, ParameterSetName = 'Detail')]
        [Alias('TestCaseID')]
        [string]$ID    
    )
    
    Begin {
        Connect-NCXCloud
    }
    Process {
        Try {
            Switch ($ReportType) {
                'TestCase' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-alarm/test-case/$ID"; Break }
                'CalledNumber' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-alarm/called-number/$ID"; Break }
                'TestSuite' { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-alarm/test-suite/$ID"; Break }
                default { $URI = "https://$Global:NCXCloud/cyclone-portlet/api/email-alarm/organization/$Global:NCXOrgID/$PlanType"}
            }
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NCXSession -uri $URI
            Return $JSON
        }
        Catch {
            Write-Error 'No data or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





#################################################################################################################################################
# #
# WebRTC Functions #
# #
#################################################################################################################################################

Function Get-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Return information about an existing WebRTC call data integration with Nectar DXP
         
        .DESCRIPTION
        Return information about an existing WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarWebRTCConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing WebRTC configuration
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc?tenant=$TenantName"
        
        Write-Verbose $URI
        
        $Body = (Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader).data

        Return $Body
    }
}



Function Set-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Modify an existing WebRTC call data integration with Nectar DXP
         
        .DESCRIPTION
        Modify an existing WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER Platforms
        One or more platforms to pull WebRTC call data from
 
        .PARAMETER DisplayName
        The internal display name of the WebRTC configuration
         
        .PARAMETER RTCCollectionIntervalSecs
        How often (in seconds) to sample call quality data. Defaults to 60
         
        .PARAMETER LoginRefreshIntervalSecs
        How often (in seconds) to refresh login credentials for client WebRTC connections. Defaults to 86400 (1 day).
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
                 
        .EXAMPLE
        Set-NectarWebRTCConfig -CertID
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Teams','Zoom','AirBnB','CiscoWebEx', IgnoreCase=$False)]
        [string[]]$Platforms,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RTCCollectionIntervalSecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$LoginRefreshIntervalSecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing WebRTC config details
        $Body = (Get-NectarWebRTCConfig)[0]
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc/$($Body.id)"
        
        # Remove extraneous details
        $Body.psobject.Properties.Remove('Id')
        $Body.psobject.Properties.Remove('stateId')
        $Body.psobject.Properties.Remove('lastUpdatedTime')
        $Body.psobject.Properties.Remove('clientId')
        $Body.psobject.Properties.Remove('kafkaTopic')
        $Body.psobject.Properties.Remove('sourceId')
        $Body | Add-Member -MemberType NoteProperty -Name 'tenant' -Value $TenantName
        
        # Update with entered parameters
        ForEach ($Param in $PSBoundParameters.GetEnumerator()) {
            $Body.($Param.key) = $Param.value
        }
        
        $JSONBody = $Body | ConvertTo-Json
        
        Write-Verbose $URI
        Write-Verbose $JSONBody
        
        If ($PSCmdlet.ShouldProcess(("Updating Nectar DXP WebRTC config on tenant {0}" -f $TenantName), ("Update Nectar DXP WebRTC config on tenant {0}?" -f $TenantName), 'Nectar DXP WebRTC Config Update')) {
            Try {
                $NULL = Invoke-RestMethod -Method PUT -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



Function New-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Enables WebRTC call data integration with Nectar DXP
         
        .DESCRIPTION
        Enables WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER Platforms
        One or more platforms to pull WebRTC call data from
 
        .PARAMETER DisplayName
        The internal display name of the WebRTC configuration
         
        .PARAMETER RTCCollectionIntervalSecs
        How often (in seconds) to sample call quality data. Defaults to 60
         
        .PARAMETER LoginRefreshIntervalSecs
        How often (in seconds) to refresh login credentials for client WebRTC connections. Defaults to 86400 (1 day).
                 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarWebRTCConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('Teams','Zoom','AirBnB','CiscoWebEx', IgnoreCase=$False)]
        [string[]]$Platforms,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DisplayName = 'WebRTC',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RTCCollectionIntervalSecs = 60,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$LoginRefreshIntervalSecs = 86400,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc"
        
        # Check for existing Teams/Azure config
        # Will increment the sourceID if one already exists
        [int]$WebRTCSourceID = ((Get-NectarWebRTCConfig -ErrorAction:SilentlyContinue).sourceID | Measure-Object -Maximum).Maximum + 1
        
        # Build the JSON body for creating the config
        $Body = @{
            tenant = $TenantName
            platforms = $Platforms
            displayName = $DisplayName
            rtcCollectionIntervalSecs = $RTCCollectionIntervalSecs
            loginRefreshIntervalSecs = $LoginRefreshIntervalSecs
            sourceId = $WebRTCSourceID
        }
        
        $JSONBody = $Body | ConvertTo-Json
        
        Write-Verbose $URI
        Write-Verbose $JSONBody
        
        Try {
            $NULL = Invoke-RestMethod -Method POST -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarWebRTCConfig {
    <#
        .SYNOPSIS
        Removes an existing WebRTC call data integration from Nectar DXP
         
        .DESCRIPTION
        Removes an existing WebRTC call data integration from Nectar DXP. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarWebRTCConfig -TenantName contoso
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing WebRTC config details
        $ExistingConfig = (Get-NectarWebRTCConfig)[0]
        $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc/$($ExistingConfig.id)"
        
        $Body = @{
            tenant = $TenantName
        }
        
        $JSONBody = $Body | ConvertTo-JSON
        
        Write-Verbose $URI
        
        If ($PSCmdlet.ShouldProcess(("Deleting Nectar DXP WebRTC config on tenant {0}" -f $TenantName), ("Delete Nectar DXP WebRTC config on tenant {0}?" -f $TenantName), 'Nectar DXP WebRTC Config Deletion')) {
            Try {
                $NULL = Invoke-RestMethod -Method DELETE -uri $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# Supporting Functions #
# #
#################################################################################################################################################

Function Convert-NectarNumToTelURI {
    <#
        .SYNOPSIS
        Converts a Nectar formatted number "+12223334444 x200" into a valid TEL uri "+12223334444;ext=200"
 
        .DESCRIPTION
        Converts a Nectar formatted number "+12223334444 x200" into a valid TEL uri "+12223334444;ext=200"
         
        .PARAMETER PhoneNumber
        The phone number to convert to a TEL URI
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Convert-NectarNumToTelsURI "+12224243344 x3344"
        Converts the above number to a TEL URI
         
        .EXAMPLE
        Get-NectarUnallocatedNumber -LocationName Jericho | Convert-NectarNumToTelURI
        Returns the next available phone number in the Jericho location in Tel URI format
             
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName, Mandatory=$true)]
        [Alias("number")]
        [string]$PhoneNumber
    )
    
    $PhoneNumber = "tel:" + $PhoneNumber.Replace(" x", ";ext=")
    Return $PhoneNumber    
}



Function Get-NectarDefaultTenantName {
    <#
        .SYNOPSIS
        Set the default tenant name for use with other commands.
         
        .DESCRIPTION
        Set the default tenant name for use with other commands.
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("stne")]
    Param ()

    If ($Global:NectarTenantName) { # Use globally set tenant name, if one was set and not explicitly included in the command
        Return $Global:NectarTenantName 
    }
    ElseIf (!$TenantName -And !$Global:NectarTenantName) { # If a tenant name wasn't set (normal for most connections, set the TenantName variable if only one available
        $URI = "https://$Global:NectarCloud/aapi/tenant"
        Write-Verbose $URI
        $TenantList = Invoke-RestMethod -Method GET -uri $URI -Headers $Global:NectarAuthHeader
        If ($TenantList.Count -eq 1) {
            Return $TenantList
        }
        Else {
            $TenantList | ForEach-Object { $TList += ($(if($TList){", "}) + $_) }
            Write-Error "TenantName was not specified. Select one of $TList"
            $PSCmdlet.ThrowTerminatingError()
            Return 
        }
    }
}



Function Get-LatLong {
    <#
        .SYNOPSIS
        Returns the geographical coordinates for an address.
 
        .DESCRIPTION
        Returns the geographical coordinates for an address.
         
        .PARAMETER Address
        The address of the location to return information on. Include as much detail as possible.
             
        .EXAMPLE
        Get-LatLong -Address "33 Main Street, Jonestown, NY, USA"
        Retrieves the latitude/longitude for the selected location
         
        .NOTES
        Version 1.0
    #>

    
    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$Address
    )

    Begin {
        $GoogleGeoAPIKey = [System.Environment]::GetEnvironmentVariable('GoogleGeocode_API_Key','user')
        If(!$GoogleGeoAPIKey) {
            Write-Host -ForegroundColor Red "You need to register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key "
            $GoogleGeoAPIKey = Read-Host "Enter a valid Google API key to be saved as environment variable GoogleGeocode_API_Key"
            [System.Environment]::SetEnvironmentVariable('GoogleGeocode_API_Key', $GoogleGeoAPIKey,[System.EnvironmentVariableTarget]::User)
        }
    }
    Process {
        Try    {
            $URI = "https://maps.googleapis.com/maps/api/geocode/json?address=$Address&key=$GoogleGeoAPIKey"
            Write-Verbose $URI
            $JSON = Invoke-RestMethod -Method GET -Uri $URI -ErrorVariable EV
            $Status = $JSON.Status
            
            [double]$Lat = $JSON.results.geometry.location.lat
            [double]$Lng = $JSON.results.geometry.location.lng
            
            $LatLong = New-Object PSObject
            
            If($Status -eq "OK") {
                $LatLong | Add-Member -type NoteProperty -Name 'Latitude' -Value $Lat
                $LatLong | Add-Member -type NoteProperty -Name 'Longitude' -Value $Lng
            }
            ElseIf ($Status -eq 'ZERO_RESULTS') {
                $LatLong | Add-Member -type NoteProperty -Name 'Latitude' -Value 0
                $LatLong | Add-Member -type NoteProperty -Name 'Longitude' -Value 0                
            }
            Else {
                $ErrorMessage = $JSON.error_message
                Write-Host -ForegroundColor Yellow "WARNING: Address geolocation failed for the following reason: $Status - $ErrorMessage"
                $LatLong | Add-Member -type NoteProperty -Name 'Latitude' -Value 0
                $LatLong | Add-Member -type NoteProperty -Name 'Longitude' -Value 0
            }
        }
        Catch {
            "Something went wrong. Please try again."
            $ev.message
        }
        Return $LatLong
    }
}



Function New-DynamicParam {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [string]$ParamName,
        [Parameter(Mandatory=$True)]
        [string]$Prefix,
        [Parameter(Mandatory=$True)]
        [bool]$ParamRequired,
        [Parameter(Mandatory=$True)]
        [int]$Position        
    )
    
    Process {
        # Set the dynamic parameters' name
        New-Variable -Name "ParamName_$Prefix" -Value $ParamName -Scope Script -Force
        
        # Create the collection of attributes
        $AttribColl_XXX = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        
        # Create and set the parameters' attributes
        $ParamAttrib_XXX = New-Object System.Management.Automation.ParameterAttribute
        $ParamAttrib_XXX.Mandatory = $ParamRequired
        $ParamAttrib_XXX.Position = $Position
                
        # Add the attributes to the attributes collection
        $AttribColl_XXX.Add($ParamAttrib_XXX)
        
        New-Variable -Name "AttribColl_$Prefix" -Value $AttribColl_XXX -Scope Script -Force
    }
}



Function ConvertFrom-XMLElement {
    <#
    .Synopsis
        Converts named nodes of an element to properties of a PSObject, recursively.
 
    .Parameter Element
        The element to convert to a PSObject.
 
    .Parameter SelectXmlInfo
        Output from the Select-Xml cmdlet.
 
    .Inputs
        Microsoft.PowerShell.Commands.SelectXmlInfo output from Select-Xml.
 
    .Outputs
        System.Management.Automation.PSCustomObject object created from selected XML.
 
    .Link
        Select-Xml
 
    .Example
        Select-Xml /configuration/appSettings/add web.config |ConvertFrom-XmlElement
 
        key value
        --- -----
        webPages:Enabled false
    #>


    #Requires -Version 3
    [CmdletBinding()][OutputType([psobject])] Param(
    [Parameter(ParameterSetName='Element',Position=0,Mandatory=$true,ValueFromPipeline=$true)][Xml.XmlElement] $Element,
    [Parameter(ParameterSetName='SelectXmlInfo',Position=0,Mandatory=$true,ValueFromPipeline=$true)]
    [Microsoft.PowerShell.Commands.SelectXmlInfo]$SelectXmlInfo
    )
    Process
    {
        switch($PSCmdlet.ParameterSetName)
        {
            SelectXmlInfo { @($SelectXmlInfo | ForEach-Object { [Xml.XmlElement]$_.Node } | ConvertFrom-XmlElement) }
            Element
            {
                if(($Element.SelectNodes('*') | Group-Object Name | Measure-Object).Count -eq 1)
                {
                    @($Element.SelectNodes('*') |ConvertFrom-XmlElement)
                }
                else
                {
                    $properties = @{}
                    $Element.Attributes | ForEach-Object { [void]$properties.Add($_.Name,$_.Value) }
                    foreach ($node in $Element.ChildNodes | Where-Object { $_.Name -and $_.Name -ne '#whitespace' } )
                    {
                        $subelements = $node.SelectNodes('*') | Group-Object Name
                        $value =
                            if($node.InnerText -and !$subelements)
                            {
                                $node.InnerText
                            }
                            elseif(($subelements | Measure-Object).Count -eq 1)
                            {
                                @($node.SelectNodes('*') | ConvertFrom-XmlElement)
                            }
                            else
                            {
                                ConvertFrom-XmlElement $node
                            }
                        if(!$properties.Contains($node.Name))
                        { # new property
                            [void]$properties.Add($node.Name,$value)
                        }
                        else
                        { # property name collision!
                            if($properties[$node.Name] -isnot [Collections.Generic.List[object]])
                            { $properties[$node.Name] = ([Collections.Generic.List[object]]@($properties[$node.Name],$value)) }
                            else
                            { $properties[$node.Name].Add($value) }
                        }
                    }
                    New-Object PSObject -Property $properties
                }
            }
        }
    }    
}



Function New-EPCTestPointXML {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True)]
        $Data        
    )

    $xmlData = '<testPointList>'
    ForEach ($Obj in $Data) {    
        $Properties = $Obj |  Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
        $xmlData += '<testPoint>'
        foreach ($Property in $Properties) {
            $xmlData += "<$Property>$($Obj.$Property)</$Property>"
        }
        $xmlData += '</testPoint>'
    }
    $xmlData += '</testPointList>'
    Return $xmlData    
}



Function Show-GroupAndStats {
    <#
        .SYNOPSIS
        Groups a set of data by one parameter, and shows sum, average, min, max for another numeric parameter (such as duration)
 
        .DESCRIPTION
        Groups a set of data by one parameter, and shows sum, average, min, max for another numeric parameter (such as duration)
         
        .PARAMETER InputObject
        The data to group and sum. Can be pipelined
 
        .PARAMETER GroupBy
        The parameter to group on
         
        .PARAMETER SumBy
        The parameter to calculate numerical statistics. Must be a numeric field
 
        .PARAMETER ShowGroupMembers
        The field to show the members of the field used in the current grouping
 
        .PARAMETER ShowSumByAsTimeFormat
        If the SumBy parameter is in seconds (Duration is an example), format the output as dd.hh:mm:ss instead of seconds
         
        .EXAMPLE
        Get-NectarSession | Show-GroupAndStats -GroupBy CallerLocation -SumBy Duration
        Will group all calls by caller location and show sum, avg, min, max for the Duration column
             
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$True)]
        [Alias('Input')]
        [pscustomobject[]]$InputObject,
        [Parameter(Mandatory=$True)]
        [string[]]$GroupBy,
        [Parameter(Mandatory=$True)]
        [string]$SumBy,
        [Parameter(Mandatory=$False)]
        [string]$ShowGroupMembers,        
        [switch]$ShowSumByAsTimeFormat
    )    

    # Validate the parameters exist and are the proper format
    ForEach ($Item in $GroupBy) {
        If ($NULL -eq ($InputObject | Get-Member $Item)) {
            Write-Error "$Item is not a valid parameter for the source data"
            Break
        }
    }
    
    If ($NULL -eq ($InputObject | Get-Member $SumBy)) {
        Write-Error "$SumBy is not a valid parameter for the source data"
        Break
    }
    # ElseIf (($InputObject | Get-Member $SumBy).Definition -NotMatch 'int|float|double|decimal') {
    # Write-Error "$SumBy is not a numeric field"
    # Break
    # }
    
    # First, use the standard Group-Object to do the grouping.
    [System.Collections.ArrayList]$Output = @()
    $InputObject | Group-Object $GroupBy | Sort-Object Count -Descending | ForEach-Object {
        $RowData = [pscustomobject][ordered] @{}

        Write-Verbose "Name: $($_.Name)"

        # If grouping by multiple fields, the 'name' field consists of each of the grouped items separated by commas.
        # This function splits them apart into their own column
        ForEach ($Item in $GroupBy) {
            $ItemName = ($_.Name.Split(',')[$GroupBy.IndexOf($Item)])

            If (!$ItemName) { $ItemName = 'Unknown'} # Don't allow for blanks.

            $RowData | Add-Member -MemberType NoteProperty -Name $Item -Value $ItemName.Trim()
        }

        $RowData | Add-Member -MemberType NoteProperty -Name 'Count' -Value $_.Count 

        If ($ShowSumByAsTimeFormat) {
            $RowData | Add-Member -MemberType NoteProperty -Name "SUM_$SumBy" -Value ([timespan]::FromSeconds(($_.Group | Measure-Object $SumBy -Sum).Sum))
            $RowData | Add-Member -MemberType NoteProperty -Name "AVG_$SumBy" -Value ([timespan]::FromSeconds([math]::Round(($_.Group | Measure-Object $SumBy -Average).Average)))
            $RowData | Add-Member -MemberType NoteProperty -Name "MIN_$SumBy" -Value ([timespan]::FromSeconds(($_.Group | Measure-Object $SumBy -Minimum).Minimum))
            $RowData | Add-Member -MemberType NoteProperty -Name "MAX_$SumBy" -Value ([timespan]::FromSeconds(($_.Group | Measure-Object $SumBy -Maximum).Maximum))
        }
        Else {
            $RowData | Add-Member -MemberType NoteProperty -Name "SUM_$SumBy" -Value ($_.Group | Measure-Object $SumBy -Sum).Sum
            $RowData | Add-Member -MemberType NoteProperty -Name "AVG_$SumBy" -Value ([math]::Round(($_.Group | Measure-Object $SumBy -Average).Average,2))
            $RowData | Add-Member -MemberType NoteProperty -Name "MIN_$SumBy" -Value ($_.Group | Measure-Object $SumBy -Minimum).Minimum
            $RowData | Add-Member -MemberType NoteProperty -Name "MAX_$SumBy" -Value ($_.Group | Measure-Object $SumBy -Maximum).Maximum
        }

        If ($ShowGroupMembers) {
            $GroupMemberList = $NULL
            $_.Group.$ShowGroupMembers | ForEach-Object { $GroupMemberList += ($(if($GroupMemberList){','}) + $_) }
            $RowData | Add-Member -MemberType NoteProperty -Name "$($ShowGroupMembers)_List" -Value $GroupMemberList
        }
        $Output += $RowData
    }             
    Return $Output
}



Function Get-JSONErrorStream {
    <#
        .SYNOPSIS
        Returns the error text of a JSON stream
 
        .DESCRIPTION
        Returns the error text of a JSON stream
         
        .PARAMETER JSONResponse
        The error response
             
        .EXAMPLE
        Get-JSONErrorStream $_
        Returns the error message from a JSON stream that errored out.
         
        .NOTES
        Version 1.0
    #>

    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $JSONResponse
    )

    $ResponseStream = $JSONResponse.Exception.Response.GetResponseStream()
    $Reader = New-Object System.IO.StreamReader($ResponseStream)
    $ResponseBody = $Reader.ReadToEnd() | ConvertFrom-Json
    Write-Host -ForegroundColor Red -BackgroundColor Black $ResponseBody.errorMessage
    Write-Host
    Write-Host
}



Function New-Chart {
    <#
        .SYNOPSIS
        Creates a chart PNG file based on the input data
 
        .DESCRIPTION
        Creates a chart PNG file based on the input data. ONLY WORKS IN MS WINDOWS
         
        .PARAMETER InputData
        The data source to use for the chart. Can be either a variable or a command enclosed in brackets
 
        .PARAMETER TimeObjectName
        The name of the data column to use for the time (x-axis)
 
        .PARAMETER BarNames
        The names of the bars to display separated by commas. Must match up with the names of the desired column in the input data
 
        .PARAMETER BarColours
        The colours of the bars to display separated by commas. Colours will be matched up with the BarNames by position.
 
        .PARAMETER Interval
        The interval between numbers to show on the axis. Defaults to auto.
 
        .PARAMETER ChartName
        The name to use for the chart header and the filename. Defaults to the type of chart being generated.
 
        .PARAMETER ChartType
        The chart type to display. Defaults to StackedColumn.
 
        .EXAMPLE
        $Data = Get-NectarSessionCount -TimePeriod LAST WEEK
        New-Chart -InputData $Data -BarNames Good,Average,Poor -BarColors Green,Yellow,Red
        Creates a bar chart using a variable from a previous command
         
        .EXAMPLE
        New-Chart -InputData (Get-NectarSessionCount -TimePeriod LAST WEEK) -BarNames Good,Average,Poor -BarColors Green,Yellow,Red
        Same results as previous example, but shown as full command written within the New-Chart command
 
         
        .NOTES
        Version 1.0
    #>

    
    param (
        [Parameter(Mandatory=$True)]
        [PSCustomObject]$InputData,
        [Parameter(Mandatory=$True)]
        [string]$TimeObjectName,
        [Parameter(Mandatory=$True)]
        [string[]]$BarNames,
        [Parameter(Mandatory=$False)]
        [string[]]$BarColours,
        [Parameter(Mandatory=$False)]
        [int32]$Interval,
        [Parameter(Mandatory=$False)]
        [string]$ChartName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Area', 'Bar', 'BoxPlot', 'Bubble', 'Column', 'Doughnut', 'Line', 'Pie', 'Point', 'Polar', 'Radar', 'Range', 'RangeBar', 'RangeColumn', 'Spline', 'SplineArea', 'SplineRange', 'StackedArea', 'StackedArea100', 'StackedBar', 'StackedBar100', 'StackedColumn', 'StackedColumn100', 'StepLine',  IgnoreCase=$True)]
        [string]$ChartType = 'StackedColumn'
    )
    [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")

    # Creating chart object
    # The System.Windows.Forms.DataVisualization.Charting namespace contains methods and properties for the Chart Windows forms control.
    $ChartObject            = New-Object System.Windows.Forms.DataVisualization.Charting.Chart
    $ChartObject.Width        = 2000
    $ChartObject.Height        = 1000
    $ChartObject.BackColor    = [System.Drawing.Color]::white

    # Set Chart title
    If (!$ChartName) { $ChartName = $ChartType }

    [void]$ChartObject.Titles.Add("$ChartName for $($InputData[0].TenantName)")
    $ChartObject.Titles[0].Font            = "Arial,13pt"
    $ChartObject.Titles[0].Alignment    = "TopCenter"

    # Create a chartarea to draw on and add to chart
    $ChartAreaObject                = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea
    $ChartAreaObject.Name            = "ChartArea1"
    $ChartAreaObject.AxisY.Title    = "Count"
    $ChartAreaObject.AxisX.Title    = "Date"
    $ChartAreaObject.AxisY.Interval    = $Interval
    $ChartAreaObject.AxisX.Interval    = 1
    $ChartAreaObject.BackColor        = [System.Drawing.Color]::white
    $ChartObject.ChartAreas.Add($ChartAreaObject)

    # Creating legend for the chart
    $ChartLegend        = New-Object system.Windows.Forms.DataVisualization.Charting.Legend
    $ChartLegend.name    = "Legend1"
    $ChartObject.Legends.Add($ChartLegend)

    ForEach ($Bar in $BarNames) {
        [void]$ChartObject.Series.Add($Bar)
        $ChartObject.Series[$Bar].ChartType            = $ChartType
        $ChartObject.Series[$Bar].BorderWidth        = 3
        $ChartObject.Series[$Bar].IsVisibleInLegend    = $true
        $ChartObject.Series[$Bar].chartarea            = "ChartArea1"
        $ChartObject.Series[$Bar].Legend            = "Legend1"

        If ($BarColours[$BarNames.IndexOf($Bar)]) { $ChartObject.Series[$Bar].color = $BarColours[$BarNames.IndexOf($Bar)] }

        $InputData | ForEach-Object {$NULL = $ChartObject.Series[$Bar].Points.addxy([datetime]$_.$TimeObjectName, $_.$Bar) }
    }

    # Save chart with the Time frame for identifying the usage at the specific time
    $ChartObject.SaveImage("$($ChartName.Replace(' ','_'))_$($InputData[0].TenantName).png","png")
    Write-Host "Chart saved as $($ChartName.Replace(' ','_'))_$($InputData[0].TenantName).png"
}



# From https://github.com/allynl93/getSAMLResponse-Interactive
# Unfortunately, it relies on System.Windows.Forms, which uses IE and doesn't work with most modern IDPs
Function New-SAMLInteractive {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [string] $LoginIDP
    )

    Begin{
        $RegEx = '(?i)name="SAMLResponse"(?: type="hidden")? value=\"(.*?)\"(?:.*)?\/>'
        Add-Type -AssemblyName System.Windows.Forms 
        Add-Type -AssemblyName System.Web
    }
    Process{
        # create window for embedded browser
        $form = New-Object Windows.Forms.Form
        $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen;
        $form.Width = 640
        $form.Height = 700
        $form.showIcon = $false
        $form.TopMost = $true
    
        $web = New-Object Windows.Forms.WebBrowser
        $web.Size = $form.ClientSize
        $web.Anchor = "Left,Top,Right,Bottom"
        $web.ScriptErrorsSuppressed = $true

        $form.Controls.Add($web)

        $web.Navigate($LoginIDP)
        
        $web.add_Navigating({
            If ($web.DocumentText -match "SAMLResponse"){
                $_.cancel = $true

                if ($web.DocumentText -match $RegEx){
                    $form.Close()
                    $Script:SAMLResponse = $(($Matches[1] -replace '&#x2b;', '+') -replace '&#x3d;', '=')
                }
            }
        })
    
        # show browser window, waits for window to close
        If ([system.windows.forms.application]::run($form) -ne "OK") {
            If ($null -ne $Script:SAMLResponse){
                Write-Output $Script:SAMLResponse
                $form.Close()
                Remove-Variable -Name SAMLResponse -Scope Script -ErrorAction SilentlyContinue
            }
            Else {
                throw "SAMLResponse not matched"
            }
        }
    }
    End{
        $form.Dispose()
    }
}


Function ParseBool {
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [System.String]$inputVal
    )
    switch -regex ($inputVal.Trim()) {
        "^(1|true|yes|on|enabled)$" { Return $True }
        default { Return $False }
    }
}


Function Get-Cookies {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        $CookieContainer
    )

    try {
        [hashtable] $Table = $CookieContainer.GetType().InvokeMember("m_domainTable",
            [System.Reflection.BindingFlags]::NonPublic -bor
            [System.Reflection.BindingFlags]::GetField -bor
            [System.Reflection.BindingFlags]::Instance,
            $null,
            $CookieContainer,
            @()
        )
        Write-Output $Table.Values.Values
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}

Export-ModuleMember -Alias * -Function *


# SIG # Begin signature block
# MIIm8QYJKoZIhvcNAQcCoIIm4jCCJt4CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU6DfBloeTx3FbylHiuVa/OhGC
# 7LeggiCZMIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B
# AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz
# 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS
# 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7
# bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI
# SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH
# trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14
# Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2
# h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt
# 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR
# iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER
# ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K
# Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd
# BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC
# hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS
# b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV
# HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh
# hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO
# 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo
# 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h
# UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x
# aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMIIGrjCCBJag
# AwIBAgIQBzY3tyRUfNhHrP0oZipeWzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIw
# MzIzMDAwMDAwWhcNMzcwMzIyMjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQg
# UlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAxoY1BkmzwT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCw
# zIP5WvYRoUQVQl+kiPNo+n3znIkLf50fng8zH1ATCyZzlm34V6gCff1DtITaEfFz
# sbPuK4CEiiIY3+vaPcQXf6sZKz5C3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ
# 7Gnf2ZCHRgB720RBidx8ald68Dd5n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7
# QKxfst5Kfc71ORJn7w6lY2zkpsUdzTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/teP
# c5OsLDnipUjW8LAxE6lXKZYnLvWHpo9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCY
# OjgRs/b2nuY7W+yB3iIU2YIqx5K/oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9K
# oRxrOMUp88qqlnNCaJ+2RrOdOqPVA+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6
# dSgkQe1CvwWcZklSUPRR8zZJTYsg0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM
# 1+mYSlg+0wOI/rOP015LdhJRk8mMDDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbC
# dLI/Hgl27KtdRnXiYKNYCQEoAA6EVO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbEC
# AwEAAaOCAV0wggFZMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1N
# hS9zKXaaL3WMaiCPnshvMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAI
# BgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7Zv
# mKlEIgF+ZtbYIULhsBguEE0TzzBTzr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI
# 2AvlXFvXbYf6hCAlNDFnzbYSlm/EUExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/ty
# dBTX/6tPiix6q4XNQ1/tYLaqT5Fmniye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVP
# ulr3qRCyXen/KFSJ8NWKcXZl2szwcqMj+sAngkSumScbqyQeJsG33irr9p6xeZmB
# o1aGqwpFyd/EjaDnmPv7pp1yr8THwcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc
# 6UsCUqc3fpNTrDsdCEkPlM05et3/JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3c
# HXg65J6t5TRxktcma+Q4c6umAU+9Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0d
# KNPH+ejxmF/7K9h+8kaddSweJywm228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZP
# J/tgZxahZrrdVcA6KYawmKAr7ZVBtzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLe
# Mt8EifAAzV3C+dAjfwAL5HYCJtnwZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDy
# Divl1vupL0QVSucTDh3bNzgaoSv27dZ8/DCCBrAwggSYoAMCAQICEAitQLJg0pxM
# n17Nqb2TrtkwDQYJKoZIhvcNAQEMBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoT
# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UE
# AxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIxMDQyOTAwMDAwMFoXDTM2
# MDQyODIzNTk1OVowaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBANW0L0LQKK14t13VOVkbsYhC9TOM6z2Bl3DFu8SFJjCfpI5o2Fz16zQk
# B+FLT9N4Q/QX1x7a+dLVZxpSTw6hV/yImcGRzIEDPk1wJGSzjeIIfTR9TIBXEmtD
# mpnyxTsf8u/LR1oTpkyzASAl8xDTi7L7CPCK4J0JwGWn+piASTWHPVEZ6JAheEUu
# oZ8s4RjCGszF7pNJcEIyj/vG6hzzZWiRok1MghFIUmjeEL0UV13oGBNlxX+yT4Us
# SKRWhDXW+S6cqgAV0Tf+GgaUwnzI6hsy5srC9KejAw50pa85tqtgEuPo1rn3MeHc
# reQYoNjBI0dHs6EPbqOrbZgGgxu3amct0r1EGpIQgY+wOwnXx5syWsL/amBUi0nB
# k+3htFzgb+sm+YzVsvk4EObqzpH1vtP7b5NhNFy8k0UogzYqZihfsHPOiyYlBrKD
# 1Fz2FRlM7WLgXjPy6OjsCqewAyuRsjZ5vvetCB51pmXMu+NIUPN3kRr+21CiRshh
# WJj1fAIWPIMorTmG7NS3DVPQ+EfmdTCN7DCTdhSmW0tddGFNPxKRdt6/WMtyEClB
# 8NXFbSZ2aBFBE1ia3CYrAfSJTVnbeM+BSj5AR1/JgVBzhRAjIVlgimRUwcwhGug4
# GXxmHM14OEUwmU//Y09Mu6oNCFNBfFg9R7P6tuyMMgkCzGw8DFYRAgMBAAGjggFZ
# MIIBVTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRoN+Drtjv4XxGG+/5h
# ewiIZfROQjAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8B
# Af8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYIKwYBBQUHAQEEazBpMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKG
# NWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290
# RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMBwGA1UdIAQVMBMwBwYFZ4EMAQMw
# CAYGZ4EMAQQBMA0GCSqGSIb3DQEBDAUAA4ICAQA6I0Q9jQh27o+8OpnTVuACGqX4
# SDTzLLbmdGb3lHKxAMqvbDAnExKekESfS/2eo3wm1Te8Ol1IbZXVP0n0J7sWgUVQ
# /Zy9toXgdn43ccsi91qqkM/1k2rj6yDR1VB5iJqKisG2vaFIGH7c2IAaERkYzWGZ
# gVb2yeN258TkG19D+D6U/3Y5PZ7Umc9K3SjrXyahlVhI1Rr+1yc//ZDRdobdHLBg
# XPMNqO7giaG9OeE4Ttpuuzad++UhU1rDyulq8aI+20O4M8hPOBSSmfXdzlRt2V0C
# FB9AM3wD4pWywiF1c1LLRtjENByipUuNzW92NyyFPxrOJukYvpAHsEN/lYgggnDw
# zMrv/Sk1XB+JOFX3N4qLCaHLC+kxGv8uGVw5ceG+nKcKBtYmZ7eS5k5f3nqsSc8u
# pHSSrds8pJyGH+PBVhsrI/+PteqIe3Br5qC6/To/RabE6BaRUotBwEiES5ZNq0RA
# 443wFSjO7fEYVgcqLxDEDAhkPDOPriiMPMuPiAsNvzv0zh57ju+168u38HcT5uco
# P6wSrqUvImxB+YJcFWbMbA7KxYbD9iYzDAdLoNMHAmpqQDBISzSoUSC7rRuFCOJZ
# DW3KBVAr6kocnqX9oKcfBnTn8tZSkP2vhUgh+Vc7tJwD7YZF9LRhbr9o4iZghurI
# r6n+lB3nYxs6hlZ4TjCCBsIwggSqoAMCAQICEAVEr/OUnQg5pr/bP1/lYRYwDQYJ
# KoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2
# IFRpbWVTdGFtcGluZyBDQTAeFw0yMzA3MTQwMDAwMDBaFw0zNDEwMTMyMzU5NTla
# MEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4GA1UE
# AxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
# DwAwggIKAoICAQCjU0WHHYOOW6w+VLMj4M+f1+XS512hDgncL0ijl3o7Kpxn3GIV
# WMGpkxGnzaqyat0QKYoeYmNp01icNXG/OpfrlFCPHCDqx5o7L5Zm42nnaf5bw9Yr
# IBzBl5S0pVCB8s/LB6YwaMqDQtr8fwkklKSCGtpqutg7yl3eGRiF+0XqDWFsnf5x
# XsQGmjzwxS55DxtmUuPI1j5f2kPThPXQx/ZILV5FdZZ1/t0QoRuDwbjmUpW1R9d4
# KTlr4HhZl+NEK0rVlc7vCBfqgmRN/yPjyobutKQhZHDr1eWg2mOzLukF7qr2JPUd
# vJscsrdf3/Dudn0xmWVHVZ1KJC+sK5e+n+T9e3M+Mu5SNPvUu+vUoCw0m+PebmQZ
# BzcBkQ8ctVHNqkxmg4hoYru8QRt4GW3k2Q/gWEH72LEs4VGvtK0VBhTqYggT02ke
# fGRNnQ/fztFejKqrUBXJs8q818Q7aESjpTtC/XN97t0K/3k0EH6mXApYTAA+hWl1
# x4Nk1nXNjxJ2VqUk+tfEayG66B80mC866msBsPf7Kobse1I4qZgJoXGybHGvPrhv
# ltXhEBP+YUcKjP7wtsfVx95sJPC/QoLKoHE9nJKTBLRpcCcNT7e1NtHJXwikcKPs
# CvERLmTgyyIryvEoEyFJUX4GZtM7vvrrkTjYUQfKlLfiUKHzOtOKg8tAewIDAQAB
# o4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/
# BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB
# MB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQWBBSltu8T
# 5+/N0GSh1VapZTGj3tXjSTBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGltZVN0
# YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2VGlt
# ZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCBGtbeoKm1mBe8cI1P
# ijxonNgl/8ss5M3qXSKS7IwiAqm4z4Co2efjxe0mgopxLxjdTrbebNfhYJwr7e09
# SI64a7p8Xb3CYTdoSXej65CqEtcnhfOOHpLawkA4n13IoC4leCWdKgV6hCmYtld5
# j9smViuw86e9NwzYmHZPVrlSwradOKmB521BXIxp0bkrxMZ7z5z6eOKTGnaiaXXT
# UOREEr4gDZ6pRND45Ul3CFohxbTPmJUaVLq5vMFpGbrPFvKDNzRusEEm3d5al08z
# jdSNd311RaGlWCZqA0Xe2VC1UIyvVr1MxeFGxSjTredDAHDezJieGYkD6tSRN+9N
# UvPJYCHEVkft2hFLjDLDiOZY4rbbPvlfsELWj+MXkdGqwFXjhr+sJyxB0JozSqg2
# 1Llyln6XeThIX8rC3D0y33XWNmdaifj2p8flTzU8AL2+nCpseQHc2kTmOt44Owde
# OVj0fHMxVaCAEcsUDH6uvP6k63llqmjWIso765qCNVcoFstp8jKastLYOrixRoZr
# uhf9xHdsFWyuq69zOuhJRrfVf8y2OMDY7Bz1tqG4QyzfTkx9HmhwwHcK1ALgXGC7
# KP845VJa1qwXIiNO9OzTF/tQa/8Hdx9xl0RBybhG02wyfFgvZ0dl5Rtztpn5aywG
# Ru9BHvDwX+Db2a2QgESvgBBBijCCBtgwggTAoAMCAQICEAes7BBZfdyz3a9JuWha
# irgwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD
# ZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2ln
# bmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAeFw0yMzAyMTYwMDAwMDBaFw0y
# NjAzMDIyMzU5NTlaMGAxCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdPbnRhcmlvMQ8w
# DQYDVQQHEwZHdWVscGgxFjAUBgNVBAoTDUtlbm5ldGggTGFza28xFjAUBgNVBAMT
# DUtlbm5ldGggTGFza28wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDA
# ndWGRHf+/VHW6a7GGRg4rTRiWatX/HWu3/z5RTObazn6akqA8riACugOKM17j9FY
# plTRJ9tCX5LeYBL4KtuNvOqa44Fo4LJ042ULHXPGv88EBnp46oElNMAkrCYQ5M7f
# senJ/W0xQ0c2gLhQ6UH7Rc6ns4AC9OKADQsk9ZGzHiEnEZ1XkfIXBZbPUSeFiJzf
# StW6KC+XI0uqySxKoVtIAIi5mljDmW8rcMPko0okRa0Si5nLvl36FBct7CKqkcge
# i1wgmE56rqxCzbtQOcjCVoG0IOUFoXqLLCA96qDAVZZPX+5Z/BgsZERAvA8dA6es
# XqZpdEf6rTFVi/5WDgWfshcNa7riTouIfPqJvU5uvvNpB3SzzIH+XmlttuKV8/Ce
# 3W7j6IBL1lr8Myt2dZQRpgsWp+WwDUluW0hx3Fi9d4a+1zQyjEf0CTd1oilwcN3W
# cTrwLInHylcamPs8gHwNmo23iL7qh/TRhoUATHJNajDppIxH4RDl/P/efqhbRxMC
# AwEAAaOCAgMwggH/MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0G
# A1UdDgQWBBRt1/RRvnadH2iU+273ZlMWw+Wu2DAOBgNVHQ8BAf8EBAMCB4AwEwYD
# VR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2Ny
# bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0
# MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIw
# MjFDQTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0
# dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYI
# KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQ
# aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29k
# ZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwCQYDVR0TBAIwADANBgkq
# hkiG9w0BAQsFAAOCAgEABY8RK2Cj/yRb02RAMTvkehP0WkeWFeBq8n6OEHq82wdm
# WAKDglsqlduvfAhtYR6mcZrdEPBXq4Y//4dlzanuOLFgYdpRA3fwCIXdycz23vh6
# 1yXqlhmUeT82lobw33xCQUQiG30tREXwZlEansbQ2ohKXLL3gIM2zCfCnXjdk1z3
# oYwL/09e+S+WPRZnRuWNZhsAZ9oEfmnUgE4rvykr761RQrclOJpHCsRwyzXxpLHY
# oSaAqXOvCoT42dWAayWiYIJO+4VhqvfMjI4g6fb6/QqPMrNs8powRsDBXN1DP6RV
# W44rkBFZfaW3bwKuWJmL88lHRScl+2JrXNJdY9vN31hcIsmM57ZT23rpVub0LzuQ
# 2dXd1tKNLy5S5Osfl0HPRKRoHeevJz6SZuiXtd6e0ZFoGGYko/dRkcvcaPWMl1SI
# b7c+GCkhWghSmgQw870LWCWOBvUxADIodTeihsheGxweRKCwYspxZ5uZ7Dq/2ks9
# mkOP0a7r08jFdAn2VB6A9P7PpYTLlMFRXFK7eKO77FzSCmA/377J/Znuh4Xmnmii
# GFfIHGtkWTspyGdjYRCdpJozYibrzmZGSHp6feuYq0LzQ6Cr4K5fwIjGP9H4jv5x
# nTKkyuTfDA+NIWK+5EqZ+z5l9i1jKcd2mhZa/uIfckM8NzcmeT8fsfUReOrsEWox
# ggXCMIIFvgIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg
# SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg
# UlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEAes7BBZfdyz3a9JuWhairgwCQYFKw4D
# AhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZI
# hvcNAQkEMRYEFPI8D+9R1lG0oqd/Q/GwZHgtbq1pMA0GCSqGSIb3DQEBAQUABIIB
# gIw2kXiHLpqMpl2iNRAXeShZ/bTAoC0DQzyWijjfketDpM5eMyg5dtAQvAB9P93g
# JeReL8oXbQTCdc97JMMCRUdEmmw0oFim298bLbqrBsys41RT/TZLTavzWRyVxDIo
# CW4rknEo9z4Rlvg85MI+XpJQcRgljJLUKNgcdkWrWGETDDlW/nkg77G1ta7qjE0m
# nzp9JdZuKkuUy6eqR1NqbBPNKvCMGnjhWx4OSuJSsHq++LDtZR5jA3EulotDODM5
# ww/aFZ3+GVoN67a2Jyp1h9Q/e/ET9GvfYP21MQ3+bTLpWaJHXmy0kUjzNsOWRQvb
# MwVAW91EpWUQqU5VKeZH8PNXk/7lE+N4UMIddWkGxOdOOGrcvyWAWCc/oKKHKKvp
# LC4h9bCYODXDeOAx4oYbp1ao92/g6Ir4jlTodKdMmVTVa0p8WY4+n2yu2WRlbFd5
# TSY/FmhatM7JbUlbiFzlfZWbtt9lGEOKVXoYN3/OT5iATx6XXzNpf/aDpdEQDhLX
# 1KGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA4MjgyMDA2NDVaMC8GCSqGSIb3DQEJBDEi
# BCB+9CPzjnnsa+2cYXTWze/EoggUId078cwz8z7MjNAQYjANBgkqhkiG9w0BAQEF
# AASCAgB6u+W4QUUg9jgqj7ZD5k1B2/vEcsI0sONbxVCwZlPkgsNEnIUx0GxTP8ZB
# 3IEG9qjOKV01BQLjeQ2BLu5PLSuzHQLbyMn7kL8SdoXB5M5Ta35F7Xdf3cfLOssG
# dF1qTXzDI2KsBRaeAQzlhELSdTWyxhLCDUG/yTROc68KMbUBV9/+1C0baZMSq+1t
# FGF8ESRUKYZQyZqOIPyfJ1Te5fcXSf5dHFGVnbW/vfAjwVaR3Flrrl5SppyW9vrp
# 47x44R/wyVRsnoqEYU9wZ+9XWhfkSXPLWiX37z7JHj8ald9WPlQsxoWP0dVgL4TO
# n9GnJa+2D/67PvJXO0znLKMYNZmB6lkixC5QsXHfRlUzhfJ7/FF5/J/G0JH1uSxV
# aJhVIsKojKhzntAZrkRfJ47PUngU1peZ5WbPLZgIPz6ge2hzQQMq+KYk9wGlADpN
# V3rpbdZjrr6XE2qFodmLy1g23yGt8cIwEDHoV2Ub/rBMz1x1LeUNNchVvJHB5Gf2
# Z8vJTbi4KUOGNPSYqyjPkbhyJL8f7b9c1oOfAQsG+8zm8niFTRNeaLaNig/olsDJ
# 09jav8xcK0tC52eFPf+1G0z+8LYZTLhTo5TQFzvQSzYP8cjFhY3ICRc4xk+nEzQx
# nm5F7/+GIwGyz4Zo2r84/4tpZlIl3siVGde3/xXuivjlF2YiIA==
# SIG # End signature block