AtlassianPS.Configuration.psm1

#region Dependencies
Import-Module Metadata -MinimumVersion 1.5.7 -MaximumVersion '1.*' -Global -ErrorAction Stop

$incompatibleConfiguration = @(Get-Module Configuration -All | Where-Object { $_.Version -lt [Version]'1.6.0' -or $_.Version.Major -ne 1 })
if ($incompatibleConfiguration.Count) {
    throw 'An incompatible Configuration dependency is loaded. Start a fresh process with Configuration >=1.6.0 within major 1.'
}

if (('AtlassianPS.ServerData' -as [Type]) -and -not ('AtlassianPS.Connection' -as [Type])) {
    throw 'An older Configuration type assembly is loaded. Start a fresh PowerShell session before importing this version.'
}

# Load the Module's namespace from C#
if (-not("AtlassianPS.ServerData" -as [Type])) {
    $assemblyReferences = @(
        'Microsoft.CSharp'
        'Microsoft.PowerShell.Commands.Utility'
        'System.Management.Automation'
        'System.Runtime.Extensions'
        'System.Security.Cryptography.X509Certificates'
    )

    if ($PSVersionTable.PSEdition -eq 'Core') {
        $assemblyReferences += 'System.Security.Cryptography'
    }

    Add-Type -Path (Join-Path $PSScriptRoot AtlassianPS.Configuration.Types.cs) -ReferencedAssemblies $assemblyReferences
}
if ($PSVersionTable.PSVersion.Major -lt 5) {
    Add-Type -Path (Join-Path $PSScriptRoot AtlassianPS.Configuration.Attributes.cs) -ReferencedAssemblies Microsoft.CSharp, Microsoft.PowerShell.Commands.Utility, System.Management.Automation, System.Runtime.Extensions, System.Security.Cryptography.X509Certificates
}

#endregion Dependencies

if ($env:ATLASSIANPS_CONFIGURATION_ROOT) {
    $configurationRoot = $env:ATLASSIANPS_CONFIGURATION_ROOT
    if (-not [IO.Path]::IsPathRooted($configurationRoot)) { throw 'Configuration root must be an absolute disposable path.' }
    $null = New-Item -Path $configurationRoot -ItemType Directory -Force
    if ((Get-Module Configuration).PrivateData.AtlassianPSTestRoot -ne $configurationRoot) {
        Import-Module Configuration -MinimumVersion 1.6.0 -MaximumVersion '1.*' -Global -Force -ArgumentList @{}, $configurationRoot, $configurationRoot, $configurationRoot -ErrorAction Stop
        (Get-Module Configuration).PrivateData.AtlassianPSTestRoot = $configurationRoot
    }
}

#region ModuleConfig
$script:RegistryGeneration = [Guid]::NewGuid()
$script:NextConnectionId = 0
$script:NextGeneratedName = 0
$script:Connections = @{}
$script:DefaultConnections = @{}
$script:RequestContexts = [Collections.Generic.List[WeakReference]]::new()
$ExecutionContext.SessionState.Module.OnRemove = {
    foreach ($connection in @($script:Connections.Values)) { $connection.Dispose() }
    foreach ($reference in $script:RequestContexts) {
        $context = $reference.Target
        if ($context) { $context.Dispose() }
    }
    $script:RequestContexts.Clear()
    $script:Connections.Clear()
    $script:DefaultConnections.Clear()
}

# Add our own Converters for serialization
Add-MetadataConverter @{
    [AtlassianPS.MessageStyle] = { "AtlassianPSMessageStyle -Indent {0} -TimeStamp {1} -BreadCrumbs {2} -FunctionName {3}" -f (ConvertTo-Metadata $_.Indent), (ConvertTo-Metadata $_.TimeStamp), (ConvertTo-Metadata $_.BreadCrumbs), (ConvertTo-Metadata $_.FunctionName) }
    AtlassianPSMessageStyle    = {
        param($Indent, $TimeStamp, $BreadCrumbs, $FunctionName)
        [AtlassianPS.MessageStyle]$PSBoundParameters
    }
    [AtlassianPS.ServerData]   = { "AtlassianPSServerData -Id {0} -Name {1} -Uri {2} -Type {3} -Headers {4}" -f $_.Id, (ConvertTo-Metadata $_.Name), (ConvertTo-Metadata $_.Uri.AbsoluteUri), (ConvertTo-Metadata $_.Type.ToString()), (ConvertTo-Metadata $_.Headers) }
    AtlassianPSServerData      = {
        param($Id, $Name, $Uri, $Type, $Headers)
        if ([string]::IsNullOrEmpty($Headers)) { $Headers = $null }
        [AtlassianPS.ServerData]@{ Id = $Id; Name = $Name; Uri = $Uri; Type = $Type; Headers = $Headers }
    }
}

#region LoadFunctions
#region Add-ServerConfiguration
function Add-ServerConfiguration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )]
    [OutputType( [void] )]
    param(
        [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )]
        [Alias('Url', 'Address')]
        [ValidateScript( { $_.IsAbsoluteUri } )]
        [Uri]
        $Uri,

        [Parameter( ValueFromPipelineByPropertyName )]
        [ValidateNotNullOrEmpty()]
        [Alias('ServerName', 'Alias')]
        [String]
        $Name,

        [Parameter( Mandatory, ValueFromPipelineByPropertyName )]
        [AtlassianPS.ServerType]
        $Type,

        [Parameter( ValueFromPipelineByPropertyName )]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session,

        [Parameter( ValueFromPipelineByPropertyName )]
        [Hashtable]
        $Headers
    )

    begin {
        Write-Verbose "Function started"

        $serverList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new()
        foreach ($server in @(Get-ServerConfiguration)) {
            $serverList.Add($server)
        }

        $configurationChanged = $false
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        if ($PSBoundParameters.ContainsKey('Session')) {
            Write-Warning 'Profile Session is deprecated and is never used by Connect. Supply authentication to Connect instead.'
        }

        $entryName = if ($PSBoundParameters.ContainsKey('Name')) { $Name } else { $Uri.Authority }
        $entryHeaders = if ($PSBoundParameters.ContainsKey('Headers')) { $Headers } else { @{} }

        if ($serverList | Where-Object Name -eq $entryName) {
            $writeErrorSplat = @{
                ExceptionType = "System.ApplicationException"
                Message       = "An entry with name [$entryName] already exists"
                ErrorId       = "AtlassianPS.ServerData.EntryExists"
                Category      = "InvalidData"
                TargetObject  = $entryName
            }
            WriteError @writeErrorSplat
        }
        elseif ($PSCmdlet.ShouldProcess($entryName, "Add server configuration")) {
            if (-not ($index = ($serverList.Id | Measure-Object -Maximum).Maximum)) {
                $index = 0
            }
            $index++

            $config = [AtlassianPS.ServerData]@{
                Id      = $index
                Name    = $entryName
                Uri     = ([Uri]($Uri.AbsoluteUri -replace "\/$", ""))
                Type    = $Type
                # IsCloudServer = (Test-ServerIsCloud -Type $Type -Uri $Uri -Headers $Headers -ErrorAction Stop -verbose)
                Session = $Session
                Headers = $entryHeaders
            }

            Write-Verbose "Adding server #$($index): [$($config.Name)]"
            Write-DebugMessage "Adding server `$config: $($config.Name) @ index $index" -BreakPoint
            $serverList.Add($config)
            $configurationChanged = $true
        }
    }

    end {
        if ($configurationChanged) {
            Write-DebugMessage "Persisting ServerList"
            $script:Configuration["ServerList"] = $serverList
            Save-Configuration
        }

        Write-Verbose "Function ended"
    }
}
#endregion Add-ServerConfiguration

#region Disconnect-Connection
function Disconnect-Connection {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Object')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')][AtlassianPS.Connection]$Connection,
        [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Name,
        [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Product,
        [Parameter(Mandatory, ParameterSetName = 'Id')][Int]$Id
    )
    process {
        if ($PSCmdlet.ParameterSetName -ne 'Object') {
            $filter = @{}
            foreach ($key in @('Product', 'Name', 'Id')) {
                if ($PSBoundParameters.ContainsKey($key)) { $filter[$key] = $PSBoundParameters[$key] }
            }
            $Connection = Get-Connection @filter
        }
        if (-not $Connection) { throw 'Connection not found.' }
        Assert-ConnectionActive -Connection $Connection -RegisteredOnly
        if ($PSCmdlet.ShouldProcess($Connection.ToString(), 'Disconnect')) {
            $script:Connections.Remove($Connection.InstanceId)
            if ($script:DefaultConnections[$Connection.Product] -eq $Connection.InstanceId) {
                $script:DefaultConnections.Remove($Connection.Product)
            }
            $Connection.Dispose()
        }
    }
}
#endregion Disconnect-Connection

#region Export-ServerConfiguration
function Export-ServerConfiguration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][String]$Path, [String[]]$Name)

    $profiles = @(
        foreach ($server in @(Get-ServerConfiguration)) {
            if (-not $Name -or $server.Name -in $Name) { ConvertTo-PortableServer -Server $server }
        }
    )
    $document = @{ SchemaVersion = 1; ServerList = $profiles } | ConvertTo-Json -Depth 4
    if ($PSCmdlet.ShouldProcess($Path, 'Export non-secret server profiles')) {
        Set-Content -LiteralPath $Path -Value $document -Encoding UTF8
    }
}
#endregion Export-ServerConfiguration

#region Get-Configuration
function Get-Configuration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( DefaultParameterSetName = 'asObject' )]
    [OutputType( [PSCustomObject], ParameterSetName = 'asObject' )]
    [OutputType( [PSObject], ParameterSetName = 'asValue' )]
    [OutputType( [Hashtable], ParameterSetName = 'asHashTable' )]
    param(
        [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
                $command = "Get-Configuration"
                $module = (Get-Command -Name $commandName).Module
                $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") }
                & $commandName |
                    Where-Object { $_.Name -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) }
            }
        )]
        [String[]]
        $Name = '*',

        [Parameter( Mandatory, ParameterSetName = 'asValue' )]
        [Switch]
        $ValueOnly,

        [Parameter( Mandatory, ParameterSetName = 'asHashtable' )]
        [Switch]
        $AsHashtable
    )

    begin {
        Write-Verbose "Function started"
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)"

        $data = @{}

        foreach ($_name in $Name) {
            Write-Verbose "Filtering for [name = $_name]"
            foreach ($key in ($script:Configuration.Keys | Where-Object { $_ -like $_name })) {
                Write-DebugMessage "Found [key = $key]"
                $data[$key] = $script:Configuration[$key]
            }
        }

        if ($AsHashtable) { $data }
        elseif ($ValueOnly) { $data.Values }
        else {
            foreach ($key in $data.Keys) {
                New-Object -TypeName PSCustomObject -Property @{ Name = $key; Value = $data[$key] }
            }
        }
    }

    end {
        Write-Verbose "Function ended"
    }
}
#endregion Get-Configuration

#region Get-Connection
function Get-Connection {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([AtlassianPS.Connection])]
    param([String]$Product, [String]$Name, [Int]$Id, [Switch]$Default)

    $script:Connections.Values | Where-Object {
        (-not $Product -or $_.Product -eq $Product) -and
        (-not $Name -or $_.Name -eq $Name) -and
        (-not $PSBoundParameters.ContainsKey('Id') -or $_.Id -eq $Id) -and
        (-not $Default -or $script:DefaultConnections[$_.Product] -eq $_.InstanceId)
    } | Sort-Object Id
}
#endregion Get-Connection

#region Get-ServerConfiguration
function Get-ServerConfiguration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( DefaultParameterSetName = '_All' )]
    [OutputType( [AtlassianPS.ServerData] )]
    param(
        [Parameter( Position = 0, Mandatory, ParameterSetName = 'ServerDataByUri' )]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
                $command = "Get-ServerConfiguration"
                $module = (Get-Command -Name $commandName).Module
                $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") }
                & $commandName |
                    Where-Object { $_.Name -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) }
            }
        )]
        [Alias('Url', 'Address')]
        [Uri]
        $Uri,

        [Parameter( Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ServerDataByName' )]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
                $command = "Get-ServerConfiguration"
                $module = (Get-Command -Name $commandName).Module
                $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") }
                & $commandName |
                    Where-Object { $_.Uri -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) }
            }
        )]
        [Alias('ServerName', 'Alias')]
        [String[]]
        $Name
    )

    begin {
        Write-Verbose "Function started"

        $serverList = Get-Configuration -Name ServerList -ValueOnly
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)"

        switch ($PsCmdlet.ParameterSetName) {
            'ServerDataByName' {
                Write-Verbose "Filtering ServerList for [Name = $Name]"

                $serverList | Where-Object { $_.Name -in $Name }
            }
            'ServerDataByUri' {
                Write-Verbose "Filtering ServerList for [URI = $Uri]"

                $serverList | Where-Object { $_.Uri -like $Uri }
            }
            '_All' {
                $serverList
            }
        }
    }

    end {
        Write-Verbose "Function ended"
    }
}
#endregion Get-ServerConfiguration

#region Import-ServerConfiguration
function Import-ServerConfiguration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding(SupportsShouldProcess)]
    param([Parameter(Mandatory)][String]$Path, [Switch]$Replace)

    $document = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
    if ($document.SchemaVersion -ne 1 -or -not $document.PSObject.Properties['ServerList'] -or
        @($document.PSObject.Properties.Name | Where-Object { $_ -notin @('SchemaVersion', 'ServerList') }).Count) {
        throw 'Unsupported profile document. Expected SchemaVersion 1 and ServerList.'
    }
    $newList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new()
    foreach ($server in @(Get-ServerConfiguration)) { $newList.Add($server) }
    $names = @{}
    foreach ($entry in @($document.ServerList)) {
        if (-not $entry -or @($entry.PSObject.Properties.Name | Where-Object { $_ -notin @('Id', 'Name', 'Uri', 'Type') }).Count -or
            [string]::IsNullOrWhiteSpace($entry.Name) -or $names.ContainsKey($entry.Name)) {
            throw 'Invalid profile fields or duplicate profile names.'
        }
        $names[$entry.Name] = $true
        $server = [AtlassianPS.ServerData]@{ Id = 1; Name = $entry.Name; Uri = $entry.Uri; Type = $entry.Type }
        $null = ConvertTo-PortableServer -Server $server
        $existing = @($newList | Where-Object Name -eq $server.Name)
        if ($existing.Count -and -not $Replace) { throw 'A profile with that name exists. Use Replace to replace it.' }
        foreach ($item in $existing) { $null = $newList.Remove($item) }
        $server.Id = 1 + [int](($newList.Id | Measure-Object -Maximum).Maximum)
        $newList.Add($server)
    }
    if ($PSCmdlet.ShouldProcess($Path, 'Import validated non-secret server profiles')) {
        $previous = $script:Configuration['ServerList']
        try {
            $script:Configuration['ServerList'] = $newList
            Save-Configuration -ErrorAction Stop
        }
        catch {
            $script:Configuration['ServerList'] = $previous
            throw
        }
    }
}
#endregion Import-ServerConfiguration

#region New-ConnectionContext
function New-ConnectionContext {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Creates an unregistered in-memory candidate; it does not perform HTTP or persist state.')]
    [CmdletBinding()]
    [OutputType([AtlassianPS.Connection])]
    param(
        [ValidateNotNullOrEmpty()][String]$Name,
        [Parameter(Mandatory)][ValidateNotNullOrEmpty()][String]$Product,
        [Parameter(Mandatory)][Uri]$BaseUri,
        [Uri]$SiteUri,
        [String]$CloudId,
        [PSCredential]$Credential,
        [Alias('Email')][String]$EmailAddress,
        [Object]$ApiToken,
        [Alias('PAT', 'BearerToken')][Object]$PersonalAccessToken,
        [Switch]$Anonymous
    )

    if (-not $PSBoundParameters.ContainsKey('Name')) {
        do {
            $script:NextGeneratedName++
            $Name = '{0}-{1}' -f $Product, $script:NextGeneratedName
        } while (Get-Connection -Product $Product -Name $Name)
    }
    if (-not $BaseUri.IsAbsoluteUri -or $BaseUri.Scheme -notin @('http', 'https') -or
        $BaseUri.UserInfo -or $BaseUri.Query -or $BaseUri.Fragment) {
        throw 'BaseUri must be an absolute HTTP(S) product endpoint without user information, query or fragment.'
    }
    $modes = @('Credential', 'ApiToken', 'PersonalAccessToken', 'Anonymous' | Where-Object { $PSBoundParameters.ContainsKey($_) })
    if ($modes.Count -ne 1) { throw 'Supply exactly one authentication mode.' }
    if ($PSBoundParameters.ContainsKey('EmailAddress') -and $modes[0] -ne 'ApiToken') {
        throw 'EmailAddress requires ApiToken.'
    }
    if ($modes[0] -eq 'Anonymous' -and -not $Anonymous) { throw 'Anonymous must be enabled explicitly.' }
    if ($modes[0] -eq 'Credential' -and
        (-not $Credential -or $Credential -eq [PSCredential]::Empty -or
        [string]::IsNullOrWhiteSpace($Credential.UserName) -or $Credential.Password.Length -eq 0)) {
        throw 'Credential must contain a username and password.'
    }
    if ($modes[0] -eq 'ApiToken' -and [string]::IsNullOrWhiteSpace($EmailAddress)) {
        throw 'ApiToken requires EmailAddress.'
    }
    $token = $null
    try {
        if ($modes[0] -eq 'ApiToken') { $token = ConvertTo-ConnectionToken -Token $ApiToken }
        if ($modes[0] -eq 'PersonalAccessToken') { $token = ConvertTo-ConnectionToken -Token $PersonalAccessToken }
        $context = [AtlassianPS.Connection]::new(
            $Name, $Product, [Uri]$BaseUri.AbsoluteUri.TrimEnd('/'), $SiteUri,
            $CloudId, $modes[0], $EmailAddress, $Credential, $token, $script:RegistryGeneration
        )
        for ($index = $script:RequestContexts.Count - 1; $index -ge 0; $index--) {
            $existing = $script:RequestContexts[$index].Target
            if (-not $existing -or $existing.State -eq 'Disconnected') { $script:RequestContexts.RemoveAt($index) }
        }
        $script:RequestContexts.Add([WeakReference]::new($context))
        $context
    }
    finally {
        if ($token) { $token.Dispose() }
    }
}
#endregion New-ConnectionContext

#region Register-Connection
function Register-Connection {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([AtlassianPS.Connection])]
    param(
        [Parameter(Mandatory)][AtlassianPS.Connection]$Connection,
        [Switch]$Default
    )

    if ($Connection.RegistryGeneration -ne $script:RegistryGeneration -or $Connection.State -ne 'Candidate') {
        throw 'Only a candidate from this runtime can be registered.'
    }
    if (-not $Connection.DeploymentType) { throw 'Validate the product endpoint and identity before registration.' }
    if (Get-Connection -Product $Connection.Product -Name $Connection.Name) { throw 'A connection with that product and name already exists.' }
    $script:NextConnectionId++
    $Connection.Register($script:NextConnectionId)
    $script:Connections[$Connection.InstanceId] = $Connection
    if ($Default -or -not $script:DefaultConnections.ContainsKey($Connection.Product)) {
        $script:DefaultConnections[$Connection.Product] = $Connection.InstanceId
    }
    $Connection
}
#endregion Register-Connection

#region Remove-Configuration
function Remove-Configuration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )]
    [OutputType( [void] )]
    param(
        [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
                $command = "Get-Configuration"
                $module = (Get-Command -Name $commandName).Module
                $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") }
                & $commandName |
                    Where-Object { $_.Name -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) }
            }
        )]
        [String[]]
        $Name
    )

    begin {
        Write-Verbose "Function started"

        $reservedNames = @('ServerList')
        $configurationChanged = $false
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)"

        foreach ($_name in $Name) {
            if ($_name -in $reservedNames) {
                $writeErrorSplat = @{
                    ExceptionType = "System.ApplicationException"
                    Message       = "Configuration key [$_name] is reserved and cannot be removed with Remove-Configuration"
                    ErrorId       = "AtlassianPS.Configuration.ReservedKey"
                    Category      = "InvalidArgument"
                    TargetObject  = $_name
                }
                WriteError @writeErrorSplat
                continue
            }

            Write-Verbose "Filtering for [name = $_name]"

            if ($PSCmdlet.ShouldProcess($_name, "Remove configuration key")) {
                $configurationChanged = $script:Configuration.Remove($_name) -or $configurationChanged
            }
        }
    }

    end {
        if ($configurationChanged) {
            Save-Configuration
        }

        Write-Verbose "Function ended"
    }
}
#endregion Remove-Configuration

#region Remove-ServerConfiguration
function Remove-ServerConfiguration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )]
    [OutputType( [void] )]
    param(
        [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
                $commandName = (Get-Command -Module "AtlassianPS.Configuration" -Name "Get-*ServerConfiguration").Name
                & $commandName |
                    Where-Object { $_.Name -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) }
            }
        )]
        [Alias('ServerName', 'Alias')]
        [ValidateNotNullOrEmpty()]
        [String[]]
        $Name
    )

    begin {
        Write-Verbose "Function started"

        $serverList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new()
        foreach ($server in @(Get-ServerConfiguration)) {
            $serverList.Add($server)
        }

        $configurationChanged = $false
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)"

        foreach ($serverToRemove in $Name) {
            if ($serverToRemove -notin $serverList.Name) {
                $writeErrorSplat = @{
                    ExceptionType = "System.ApplicationException"
                    ErrorId       = "AtlassianPS.ServerData.ServerNotFound"
                    Category      = "ObjectNotFound"
                    Message       = "No server '$serverToRemove' could be found."
                    TargetObject  = $serverToRemove
                    Cmdlet        = $PSCmdlet
                }
                WriteError @writeErrorSplat
            }
        }

        foreach ($serverToRemove in $Name) {
            if ($serverToRemove -in $serverList.Name -and $PSCmdlet.ShouldProcess($serverToRemove, "Remove server configuration")) {
                $remainingServers = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new()
                foreach ($server in $serverList) {
                    if ($server.Name -ne $serverToRemove) {
                        $remainingServers.Add($server)
                    }
                }
                $serverList = $remainingServers
                $configurationChanged = $true
            }
        }
    }

    end {
        if ($configurationChanged) {
            Write-DebugMessage "Persisting ServerList"
            $script:Configuration.Remove("ServerList")
            $script:Configuration.Add("ServerList", $serverList)
            Save-Configuration
        }

        Write-Verbose "Function ended"
    }
}
#endregion Remove-ServerConfiguration

#region Resolve-RequestContext
function Resolve-RequestContext {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([AtlassianPS.Connection])]
    param(
        [Parameter(Mandatory)][String]$Product,
        [Parameter(DontShow)][AtlassianPS.Connection]$RequestContext,
        [AtlassianPS.Connection]$Connection,
        [String]$ConnectionName,
        [Uri]$BaseUri,
        [PSCredential]$Credential,
        [Alias('Email')][String]$EmailAddress,
        [Object]$ApiToken,
        [Alias('PAT', 'BearerToken')][Object]$PersonalAccessToken,
        [Switch]$Anonymous
    )

    if ($PSBoundParameters.ContainsKey('RequestContext')) {
        $conflicts = @('Connection', 'ConnectionName', 'BaseUri', 'Credential', 'EmailAddress', 'ApiToken', 'PersonalAccessToken', 'Anonymous')
        $hasConflict = @($conflicts | Where-Object { $PSBoundParameters.ContainsKey($_) }).Count -gt 0
        if (-not $RequestContext -or $RequestContext.Product -ne $Product -or
            $RequestContext.RegistryGeneration -ne $script:RegistryGeneration -or
            $RequestContext.State -eq 'Disconnected' -or $hasConflict) {
            throw 'Invalid internal request context.'
        }
        if ($RequestContext.State -eq 'Active' -or $RequestContext.IsOverride) {
            Assert-ConnectionActive -Connection $RequestContext -Product $Product
        }
        return $RequestContext
    }

    $selectors = @('Connection', 'ConnectionName', 'BaseUri' | Where-Object { $PSBoundParameters.ContainsKey($_) })
    if ($selectors.Count -gt 1) { throw 'Connection, ConnectionName and BaseUri are mutually exclusive.' }
    if ($PSBoundParameters.ContainsKey('Connection') -and -not $Connection) { throw 'Connection cannot be null.' }
    if ($PSBoundParameters.ContainsKey('ConnectionName') -and [string]::IsNullOrWhiteSpace($ConnectionName)) {
        throw 'ConnectionName cannot be empty.'
    }
    $auth = @{}
    foreach ($key in @('Credential', 'EmailAddress', 'ApiToken', 'PersonalAccessToken', 'Anonymous')) {
        if ($PSBoundParameters.ContainsKey($key)) { $auth[$key] = $PSBoundParameters[$key] }
    }
    if ($PSBoundParameters.ContainsKey('BaseUri') -and -not $BaseUri) { throw 'BaseUri cannot be null.' }
    if ($BaseUri) {
        $context = New-ConnectionContext -Name 'Standalone' -Product $Product -BaseUri $BaseUri @auth
        $context.MakeStandalone([Guid]::Empty)
        return $context
    }
    if ($ConnectionName) { $Connection = Get-Connection -Product $Product -Name $ConnectionName }
    elseif (-not $PSBoundParameters.ContainsKey('Connection')) { $Connection = Get-Connection -Product $Product -Default }
    if (-not $Connection) { throw "No $Product connection selected. Connect or select a connection first." }
    Assert-ConnectionActive -Connection $Connection -Product $Product
    if ($auth.Count -eq 0) { return $Connection }

    $context = New-ConnectionContext -Name $Connection.Name -Product $Product -BaseUri $Connection.BaseUri -SiteUri $Connection.SiteUri -CloudId $Connection.CloudId @auth
    $context.SetValidationResult($Connection.DeploymentType, $null)
    $context.MakeStandalone($Connection.InstanceId)
    $context
}
#endregion Resolve-RequestContext

#region Select-Connection
function Select-Connection {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding(DefaultParameterSetName = 'Object')]
    [OutputType([AtlassianPS.Connection])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')][AtlassianPS.Connection]$Connection,
        [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Name,
        [Parameter(Mandatory, ParameterSetName = 'Name')][String]$Product,
        [Parameter(Mandatory, ParameterSetName = 'Id')][Int]$Id
    )
    process {
        if ($PSCmdlet.ParameterSetName -ne 'Object') {
            $filter = @{}
            foreach ($key in @('Product', 'Name', 'Id')) {
                if ($PSBoundParameters.ContainsKey($key)) { $filter[$key] = $PSBoundParameters[$key] }
            }
            $Connection = Get-Connection @filter
        }
        if (-not $Connection) { throw 'Connection not found.' }
        Assert-ConnectionActive -Connection $Connection -RegisteredOnly
        $script:DefaultConnections[$Connection.Product] = $Connection.InstanceId
        $Connection
    }
}
#endregion Select-Connection

#region Set-Configuration
function Set-Configuration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )]
    [OutputType( [PSCustomObject] )]
    param(
        [Parameter( Mandatory, ValueFromPipelineByPropertyName )]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
                $command = "Get-Configuration"
                $module = (Get-Command -Name $commandName).Module
                $commandName = $module.ExportedCommands.Keys | Where-Object { $_ -like ($command -replace "-", "-$($module.Prefix)") }
                & $commandName |
                    Where-Object { $_.Name -like "$wordToComplete*" } |
                    ForEach-Object { [System.Management.Automation.CompletionResult]::new( $_.Name, $_.Name, [System.Management.Automation.CompletionResultType]::ParameterValue, $_.Name ) }
            }
        )]
        [String]
        $Name,

        [Parameter( ValueFromPipelineByPropertyName )]
        [AllowNull()]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [Object]
        $Value,

        [Switch]
        $Append,

        [Switch]
        $Passthru
    )

    begin {
        Write-Verbose "Function started"

        $reservedNames = @('ServerList')
        $configurationChanged = $false
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        Write-DebugMessage "PSBoundParameters: $($PSBoundParameters | Out-String)"

        if ($Name -in $reservedNames) {
            $writeErrorSplat = @{
                ExceptionType = "System.ApplicationException"
                Message       = "Configuration key [$Name] is reserved and cannot be changed with Set-Configuration"
                ErrorId       = "AtlassianPS.Configuration.ReservedKey"
                Category      = "InvalidArgument"
                TargetObject  = $Name
            }
            WriteError @writeErrorSplat
            return
        }

        if ($Append) {
            Write-Verbose "Appending to existing value"
            $oldValue = (Get-Configuration -Name $Name -ValueOnly)
            if ($null -eq $oldValue) {
                $newValue = @($Value)
            }
            else {
                try {
                    $newValue = @(@($oldValue) + @($Value)) -as ($oldValue.GetType())
                    if (-not $newValue) {
                        throw "failed to cast to $($oldValue.GetType().Name)"
                    }
                }
                catch {
                    Write-DebugMessage $_

                    $newValue = @(@($oldValue) + @($Value))
                }
            }
            $Value = $newValue
        }

        if ($Value) { $dataType = $Value.GetType().Name }
        else { $dataType = "null" }
        Write-Verbose "Storing value [$dataType] to [name = $Name]"

        if ($PSCmdlet.ShouldProcess($Name, "Set configuration value")) {
            $script:Configuration.Remove($Name)
            $script:Configuration.Add($Name, $Value)
            $configurationChanged = $true

            if ($Passthru) {
                Get-Configuration -Name $Name
            }
        }
    }

    end {
        if ($configurationChanged) {
            Save-Configuration
        }

        Write-Verbose "Function ended"
    }
}
#endregion Set-Configuration

#region Set-ServerConfiguration
function Set-ServerConfiguration {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding( ConfirmImpact = 'Low', SupportsShouldProcess = $true )]
    [OutputType( [void] )]
    param(
        [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )]
        [ValidateRange(1, [UInt32]::MaxValue)]
        [UInt32]
        $Id,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Alias('Url', 'Address')]
        [ValidateScript( { $_.IsAbsoluteUri } )]
        [Uri]
        $Uri,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [Alias('ServerName', 'Alias')]
        [String]
        $Name = $Uri.Authority,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [AtlassianPS.ServerType]
        $Type,

        [Parameter()]
        [Microsoft.PowerShell.Commands.WebRequestSession]
        $Session,

        [Parameter()]
        [Hashtable]
        $Headers
    )

    begin {
        Write-Verbose "Function started"

        $parametersToIgnore = @(
            'Id'
            'Verbose'
            'Debug'
            'ErrorAction'
            'WarningAction'
            'InformationAction'
            'ErrorVariable'
            'WarningVariable'
            'InformationVariable'
            'OutVariable'
            'OutBuffer'
            'PipelineVariable'
            'WhatIf'
            'Confirm'
        )
        $configurationChanged = $false
    }

    process {
        Write-DebugMessage "ParameterSetName: $($PsCmdlet.ParameterSetName)"
        if ($PSBoundParameters.ContainsKey('Session')) {
            Write-Warning 'Profile Session is deprecated and is never used by Connect. Supply authentication to Connect instead.'
        }

        $serverEntry = Get-ServerConfiguration | Where-Object { $_.Id -eq $Id }
        if ($serverEntry) {
            if ($PSBoundParameters.ContainsKey('Name') -and (Get-ServerConfiguration | Where-Object { $_.Id -ne $Id -and $_.Name -eq $Name })) {
                $writeErrorSplat = @{
                    ExceptionType = "System.ApplicationException"
                    Message       = "An entry with name [$Name] already exists"
                    ErrorId       = "AtlassianPS.ServerData.EntryExists"
                    Category      = "InvalidData"
                    TargetObject  = $Name
                }
                WriteError @writeErrorSplat
                return
            }

            if ($PSCmdlet.ShouldProcess("#$Id ($($serverEntry.Name))", "Update server configuration")) {
                foreach ($property in ($PSBoundParameters.Keys | Where-Object { $_ -notin $parametersToIgnore } )) {
                    Write-Verbose "Changing [$property] of entry #$Id"

                    $serverEntry.$property = Get-Variable $property -ValueOnly
                    $configurationChanged = $true
                }
            }
        }
        else {
            $writeErrorSplat = @{
                ExceptionType = "System.ApplicationException"
                Message       = "No entry could be found at index $Id"
                ErrorId       = "AtlassianPS.ServerData.NoEntryExists"
                Category      = "InvalidData"
                TargetObject  = $Id
            }
            WriteError @writeErrorSplat
        }
    }

    end {
        if ($configurationChanged) {
            Save-Configuration
        }

        Write-Verbose "Function ended"
    }
}
#endregion Set-ServerConfiguration

#region ConvertFrom-QueryString
function ConvertFrom-QueryString {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding(DefaultParameterSetName = 'ByString')]
    [OutputType([Hashtable])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByUri')]
        [Uri]
        $Uri,

        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'ByString')]
        [String]
        $Query
    )

    process {
        $getParameter = @{}

        if ($Uri) {
            $Query = $Uri.Query
        }

        if (($Query -match '^\?.+') -or ($Query -match '=')) {
            $Query.TrimStart('?').Split('&') | ForEach-Object {
                $key, $value = $_.Split('=', 2)
                if (-not [String]::IsNullOrEmpty($key)) {
                    $decodedKey = ConvertFrom-URLEncoded -InputString $key
                    $decodedValue = if ([String]::IsNullOrEmpty($value)) { $value } else { ConvertFrom-URLEncoded -InputString $value }
                    $getParameter[$decodedKey] = $decodedValue
                }
            }
        }

        $getParameter
    }
}
#endregion ConvertFrom-QueryString

#region ConvertFrom-URLEncoded
function ConvertFrom-URLEncoded {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [String[]]
        $InputString
    )

    begin {
        Import-HttpUtility
    }

    process {
        foreach ($string in $InputString) {
            [System.Web.HttpUtility]::UrlDecode($string)
        }
    }
}
#endregion ConvertFrom-URLEncoded

#region ConvertTo-Hashtable
function ConvertTo-Hashtable {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSObject]
        $InputObject
    )

    process {
        $hash = @{}
        foreach ($property in $InputObject.PSObject.Properties) {
            $hash[$property.Name] = $property.Value
        }

        $hash
    }
}
#endregion ConvertTo-Hashtable

#region ConvertTo-QueryString
function ConvertTo-QueryString {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [Hashtable]
        $InputObject
    )

    process {
        if ($InputObject.Count -eq 0) {
            return ''
        }

        $pairs = foreach ($key in $InputObject.Keys) {
            $encodedKey = ConvertTo-URLEncoded -InputString "$key"
            $value = $InputObject[$key]

            if ($null -eq $value -or "$value" -eq '') {
                "$encodedKey="
            }
            else {
                "{0}={1}" -f $encodedKey, (ConvertTo-URLEncoded -InputString "$value")
            }
        }

        '?' + ($pairs -join '&')
    }
}
#endregion ConvertTo-QueryString

#region ConvertTo-URLEncoded
function ConvertTo-URLEncoded {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [String[]]
        $InputString
    )

    begin {
        Import-HttpUtility
    }

    process {
        foreach ($string in $InputString) {
            [System.Web.HttpUtility]::UrlEncode($string)
        }
    }
}
#endregion ConvertTo-URLEncoded

#region Join-Hashtable
function Join-Hashtable {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowNull()]
        [System.Collections.IDictionary[]]
        $Hashtable,

        [Parameter()]
        [ValidateSet('Overwrite', 'Error')]
        [String]
        $OnDuplicate = 'Overwrite'
    )

    begin {
        [Hashtable]$table = @{}
        $isInitialized = $false
    }

    process {
        foreach ($item in $Hashtable) {
            if ($null -eq $item) {
                continue
            }

            if (-not $isInitialized) {
                # Start from the first dictionary so key comparison behavior
                # (for example case sensitivity) is preserved.
                $table = [System.Collections.Hashtable]::new($item)
                $isInitialized = $true
                continue
            }

            foreach ($entry in $item.GetEnumerator()) {
                if ($OnDuplicate -eq 'Error' -and $table.Contains($entry.Key)) {
                    $errorItem = [System.Management.Automation.ErrorRecord]::new(
                        ([System.ArgumentException]::new("Duplicate key '$($entry.Key)' was encountered while joining hashtables.")),
                        'JoinHashtable.DuplicateKey',
                        [System.Management.Automation.ErrorCategory]::InvalidArgument,
                        $entry.Key
                    )
                    $PSCmdlet.ThrowTerminatingError($errorItem)
                }

                $table[$entry.Key] = $entry.Value
            }
        }
    }

    end {
        $table
    }
}
#endregion Join-Hashtable

#region Resolve-DefaultParameterValue
function Resolve-DefaultParameterValue {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param(
        [Parameter(Mandatory)]
        [Hashtable]
        $Reference,

        [Parameter(Mandatory)]
        [String[]]
        $CommandName,

        [Parameter()]
        [Hashtable]
        $Target = @{},

        [Parameter()]
        [String[]]
        $ParameterName = '*'
    )

    begin {
        $defaultItems = New-Object -TypeName System.Collections.ArrayList

        foreach ($key in $Reference.Keys) {
            $null = $defaultItems.Add(
                [PSCustomObject]@{
                    Key       = $key
                    Value     = $Reference[$key]
                    Command   = $key.Split(':')[0]
                    Parameter = $key.Split(':')[1]
                }
            )
        }
    }

    process {
        foreach ($command in $CommandName) {
            foreach ($item in $defaultItems) {
                if ($command -notlike $item.Command) {
                    continue
                }

                foreach ($parameter in $ParameterName) {
                    if ($item.Parameter -like $parameter) {
                        if ($parameter -ne '*') {
                            $Target["${command}:$parameter"] = $item.Value
                        }
                        else {
                            $Target["${command}:$($item.Parameter)"] = $item.Value
                        }
                    }
                }
            }
        }
    }

    end {
        $Target
    }
}
#endregion Resolve-DefaultParameterValue

#region Resolve-FilePath
function Resolve-FilePath {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [Alias('PSPath', 'LiteralPath')]
        [String]
        $Path
    )

    process {
        $folder = Split-Path -Path $Path -Parent
        $file = Split-Path -Path $Path -Leaf

        if (-not [String]::IsNullOrEmpty($folder)) {
            $folderResolved = Resolve-Path -Path $folder
        }
        else {
            $folderResolved = Resolve-Path -Path $ExecutionContext.SessionState.Path.CurrentFileSystemLocation
        }

        Join-Path -Path $folderResolved.ProviderPath -ChildPath $file
    }
}
#endregion Resolve-FilePath

#region Write-DebugMessage
function Write-DebugMessage {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [String]
        $Message,

        [Parameter()]
        [Switch]
        $BreakPoint,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $(
            try {
                (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value
            }
            catch {
                $PSCmdlet
            }
        )
    )

    begin {
        Import-MqcnAlias -Alias "WriteDebug" -Command "Microsoft.PowerShell.Utility\Write-Debug"
        $oldDebugPreference = $DebugPreference
        if (-not $BreakPoint -and $DebugPreference -ne 'SilentlyContinue') {
            $DebugPreference = 'Continue'
        }
    }

    process {
        $caller = Get-PSCallStack | Select-Object -Last 1 -Skip 1 | Select-Object -First 1
        if ($caller -and $caller.Arguments -and $caller.Arguments.Contains("Debug")) {
            $DebugPreference = 'Continue'
        }

        Format-MessageStyle -Message $Message -Cmdlet $Cmdlet | ForEach-Object { WriteDebug $_ }
    }

    end {
        $DebugPreference = $oldDebugPreference
    }
}
#endregion Write-DebugMessage

#region Write-NonTerminatingError
function Write-NonTerminatingError {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding(DefaultParameterSetName = 'ExistingException')]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $(
            try {
                (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value
            }
            catch {
                $PSCmdlet
            }
        ),

        [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'NewException')]
        [ValidateNotNullOrEmpty()]
        [System.Exception]
        $Exception,

        [Parameter(ParameterSetName = 'NewException', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ExceptionType = 'System.Management.Automation.RuntimeException',

        [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 3)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Message,

        [Parameter()]
        [Object]
        $TargetObject,

        [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 10)]
        [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 10)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ErrorId,

        [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 11)]
        [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 11)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $Category,

        [Parameter(Mandatory, ParameterSetName = 'Rethrow', Position = 1)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    process {
        if (-not $Cmdlet) {
            throw 'Cmdlet runtime was not provided. Call this helper from an advanced function or pass -Cmdlet.'
        }

        if (-not $ErrorRecord) {
            $mode = if ($PSCmdlet.ParameterSetName -eq 'NewException') {
                'NewException'
            }
            else {
                'ExistingException'
            }

            $ErrorRecord = New-ErrorRecord `
                -Mode $mode `
                -Exception $Exception `
                -ExceptionType $ExceptionType `
                -Message $Message `
                -ErrorId $ErrorId `
                -Category $Category `
                -TargetObject $TargetObject
        }

        $Cmdlet.WriteError($ErrorRecord)
    }
}
#endregion Write-NonTerminatingError

#region Write-TerminatingError
function Write-TerminatingError {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding(DefaultParameterSetName = 'ExistingException')]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $(
            try {
                (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value
            }
            catch {
                $PSCmdlet
            }
        ),

        [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 1, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Parameter(ParameterSetName = 'NewException')]
        [ValidateNotNullOrEmpty()]
        [System.Exception]
        $Exception,

        [Parameter(ParameterSetName = 'NewException', Position = 2)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ExceptionType = 'System.Management.Automation.RuntimeException',

        [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 3)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Message,

        [Parameter()]
        [Object]
        $TargetObject,

        [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 10)]
        [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 10)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ErrorId,

        [Parameter(Mandatory, ParameterSetName = 'ExistingException', Position = 11)]
        [Parameter(Mandatory, ParameterSetName = 'NewException', Position = 11)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $Category,

        [Parameter(Mandatory, ParameterSetName = 'Rethrow', Position = 1)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord
    )

    process {
        if (-not $Cmdlet) {
            throw 'Cmdlet runtime was not provided. Call this helper from an advanced function or pass -Cmdlet.'
        }

        if (-not $ErrorRecord) {
            $mode = if ($PSCmdlet.ParameterSetName -eq 'NewException') {
                'NewException'
            }
            else {
                'ExistingException'
            }

            $ErrorRecord = New-ErrorRecord `
                -Mode $mode `
                -Exception $Exception `
                -ExceptionType $ExceptionType `
                -Message $Message `
                -ErrorId $ErrorId `
                -Category $Category `
                -TargetObject $TargetObject
        }

        $Cmdlet.ThrowTerminatingError($ErrorRecord)
    }
}
#endregion Write-TerminatingError

#region Write-VerboseMessage
function Write-VerboseMessage {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [String]
        $Message,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $(
            try {
                (Get-Variable -Scope 1 -Name PSCmdlet -ErrorAction Stop).Value
            }
            catch {
                $PSCmdlet
            }
        )
    )

    begin {
        Import-MqcnAlias -Alias "WriteVerbose" -Command "Microsoft.PowerShell.Utility\Write-Verbose"
    }

    process {
        $caller = Get-PSCallStack | Select-Object -Last 1 -Skip 1 | Select-Object -First 1
        if ($caller -and $caller.Arguments -and $caller.Arguments.Contains("Verbose")) {
            $VerbosePreference = 'Continue'
        }

        Format-MessageStyle -Message $Message -Cmdlet $Cmdlet | ForEach-Object { WriteVerbose $_ }
    }
}
#endregion Write-VerboseMessage

#region Assert-ConnectionActive
function Assert-ConnectionActive {
    [CmdletBinding()]
    param([Parameter(Mandatory)][AtlassianPS.Connection]$Connection, [String]$Product, [Switch]$RegisteredOnly)

    $sourceId = if ($Connection.IsOverride) { $Connection.SourceInstanceId } else { $Connection.InstanceId }
    $registered = $script:Connections[$sourceId]
    if ($Connection.RegistryGeneration -ne $script:RegistryGeneration -or
        ($RegisteredOnly -and $Connection.State -ne 'Active') -or
        $Connection.State -eq 'Disconnected' -or -not $registered -or
        $registered.State -ne 'Active' -or
        (-not $Connection.IsOverride -and -not [Object]::ReferenceEquals($registered, $Connection))) {
        throw 'Connection is not active in this runtime. Connect again in this PowerShell session.'
    }
    if ($Product -and $Connection.Product -ne $Product) { throw "Connection belongs to another product; expected $Product." }
}
#endregion Assert-ConnectionActive

#region ConvertTo-ConnectionToken
function ConvertTo-ConnectionToken {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The public contract accepts CI string tokens; normalization retains an owned SecureString copy.')]
    [CmdletBinding()]
    [OutputType([System.Security.SecureString])]
    param([Parameter(Mandatory)][AllowNull()][AllowEmptyString()][Object]$Token)

    if ($Token -is [System.Security.SecureString]) {
        if ($Token.Length -eq 0) { throw 'Token cannot be empty.' }
        return $Token.Copy()
    }
    if ($Token -is [string] -and -not [string]::IsNullOrWhiteSpace($Token)) {
        return ConvertTo-SecureString -String $Token -AsPlainText -Force
    }
    throw 'Token must be a nonempty string or SecureString.'
}
#endregion ConvertTo-ConnectionToken

#region ConvertTo-PortableServer
function ConvertTo-PortableServer {
    [CmdletBinding()]
    param([Parameter(Mandatory)][AtlassianPS.ServerData]$Server)

    if ($Server.Certificate -or ($Server.Headers -and $Server.Headers.Count -gt 0)) {
        throw 'Legacy profile contains headers or a certificate. Remove those settings and supply authentication to Connect before saving or exporting.'
    }
    if (-not $Server.Uri.IsAbsoluteUri -or $Server.Uri.UserInfo -or $Server.Uri.Query -or $Server.Uri.Fragment) {
        throw 'Profile URI must be absolute and contain no user information, query or fragment.'
    }
    [ordered]@{ Id = $Server.Id; Name = $Server.Name; Uri = $Server.Uri.AbsoluteUri; Type = $Server.Type.ToString() }
}
#endregion ConvertTo-PortableServer

#region Get-BreadCrumb
function Get-BreadCrumb {
    param(
        [String]$Delimiter = " > "
    )

    $depth = 1
    $path = New-Object -TypeName System.Collections.ArrayList

    while ($depth) {
        try {
            $null = $path.Add((Get-Variable MyInvocation -Scope $depth -ValueOnly).MyCommand.Name)
            $depth++
        }
        catch {
            $depth = 0
        }
    }

    $path.Remove("")
    $path -join $Delimiter
}
#endregion Get-BreadCrumb

#region Import-MqcnAlias
function Import-MqcnAlias {
    <#
    .SYNOPSIS
        Create an alias for a full command name
 
    .DESCRIPTION
        Create an alias for a full command name
        This can be used to create a mockable call to a full command name
 
    .EXAMPLE
        Import-MqcnAlias -Alias "GetItem" -Command Microsoft.PowerShell.Management\Get-Item
        ---------
        Description
        Create an alias "GetItem" for the normal Get-Item
    #>

    [CmdletBinding()]
    param(
        # Name of the alias to be used
        [Parameter( Mandatory )]
        [String]
        $Alias,

        # Name of the command for which to create the alias
        [Parameter( Mandatory )]
        [String]
        $Command
    )

    begin {
        Set-Alias -Name $Alias -Value $Command -Scope 1 -WhatIf:$false
    }
}
#endregion Import-MqcnAlias

#region Invoke-WebRequest
function Invoke-WebRequest {
    # For Version up to 5.1
    <#
    .ForwardHelpTargetName
        Microsoft.PowerShell.Utility\Invoke-WebRequest
    .ForwardHelpCategory
        Cmdlet
    #>

    [CmdletBinding(HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=217035')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        "PSAvoidUsingConvertToSecureStringWithPlainText",
        "",
        Justification = "Converting received plaintext token to SecureString"
    )]
    param(
        [switch]
        ${UseBasicParsing},

        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [uri]
        ${Uri},

        [Microsoft.PowerShell.Commands.WebRequestSession]
        ${WebSession},

        [Alias('SV')]
        [string]
        ${SessionVariable},

        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${Credential} = [System.Management.Automation.PSCredential]::Empty,

        [switch]
        ${UseDefaultCredentials},

        [ValidateNotNullOrEmpty()]
        [string]
        ${CertificateThumbprint},

        [ValidateNotNull()]
        [System.Security.Cryptography.X509Certificates.X509Certificate]
        ${Certificate},

        [string]
        ${UserAgent},

        [switch]
        ${DisableKeepAlive},

        [ValidateRange(0, 2147483647)]
        [int32]
        ${TimeoutSec},

        [System.Collections.IDictionary]
        ${Headers},

        [ValidateRange(0, 2147483647)]
        [int]
        ${MaximumRedirection},

        [Microsoft.PowerShell.Commands.WebRequestMethod]
        ${Method},

        [uri]
        ${Proxy},

        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${ProxyCredential} = [System.Management.Automation.PSCredential]::Empty,

        [switch]
        ${ProxyUseDefaultCredentials},

        [Parameter(ValueFromPipeline = $true)]
        [System.Object]
        ${Body},

        [string]
        ${ContentType},

        [ValidateSet('chunked', 'compress', 'deflate', 'gzip', 'identity')]
        [string]
        ${TransferEncoding},

        [string]
        ${InFile},

        [string]
        ${OutFile},

        [switch]
        ${PassThru})

    begin {
        if ($Credential) {
            $SecureCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes(
                    $('{0}:{1}' -f $Credential.UserName, $Credential.GetNetworkCredential().Password)
                ))
            $Headers["Authorization"] = "Basic $($SecureCreds)"
            $PSBoundParameters.Remove("Credential")
        }

        if ($InFile) {
            $boundary = [System.Guid]::NewGuid().ToString()
            $enc = [System.Text.Encoding]::GetEncoding("iso-8859-1")
            $fileName = Split-Path -Path $InFile -Leaf
            $readFile = Get-Content -Path $InFile -Encoding Byte
            $fileEnc = $enc.GetString($readFile)
            $PSBoundParameters["Body"] = @'
--{0}
Content-Disposition: form-data; name="file"; filename="{1}"
Content-Type: application/octet-stream
 
{2}
--{0}--
 
'@
 -f $boundary, $fileName, $fileEnc

            $PSBoundParameters["Headers"]['X-Atlassian-Token'] = 'nocheck'
            $PSBoundParameters["ContentType"] = "multipart/form-data; boundary=`"$boundary`""
            $null = $PSBoundParameters.Remove("InFile")
        }

        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Invoke-WebRequest', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = { & $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch {
            throw
        }
    }

    process {
        try {
            $steppablePipeline.Process($_)
            if ($SessionVariable) {
                Set-Variable -Name $SessionVariable -Value (Get-Variable $SessionVariable).Value -Scope 1
            }
        }
        catch {
            throw
        }
    }

    end {
        try {
            $steppablePipeline.End()
        }
        catch {
            throw
        }
    }
}

if ($PSVersionTable.PSVersion.Major -ge 6) {
    function Invoke-WebRequest {
        #require -Version 6
        <#
        .ForwardHelpTargetName
            Microsoft.PowerShell.Utility\Invoke-WebRequest
        .ForwardHelpCategory
            Cmdlet
        #>

        [CmdletBinding(DefaultParameterSetName = 'StandardMethod', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=217035')]
        param(
            [switch]
            ${UseBasicParsing},

            [Parameter(Mandatory = $true, Position = 0)]
            [ValidateNotNullOrEmpty()]
            [uri]
            ${Uri},

            [Microsoft.PowerShell.Commands.WebRequestSession]
            ${WebSession},

            [Alias('SV')]
            [string]
            ${SessionVariable},

            [switch]
            ${AllowUnencryptedAuthentication},

            [Microsoft.PowerShell.Commands.WebAuthenticationType]
            ${Authentication},

            [pscredential]
            [System.Management.Automation.CredentialAttribute()]
            ${Credential},

            [switch]
            ${UseDefaultCredentials},

            [ValidateNotNullOrEmpty()]
            [string]
            ${CertificateThumbprint},

            [ValidateNotNull()]
            [X509Certificate]
            ${Certificate},

            [switch]
            ${SkipCertificateCheck},

            [Microsoft.PowerShell.Commands.WebSslProtocol]
            ${SslProtocol},

            [securestring]
            ${Token},

            [string]
            ${UserAgent},

            [switch]
            ${DisableKeepAlive},

            [ValidateRange(0, 2147483647)]
            [int32]
            ${TimeoutSec},

            [System.Collections.IDictionary]
            ${Headers},

            [ValidateRange(0, 2147483647)]
            [int]
            ${MaximumRedirection},

            [Parameter(ParameterSetName = 'StandardMethod')]
            [Parameter(ParameterSetName = 'StandardMethodNoProxy')]
            [Microsoft.PowerShell.Commands.WebRequestMethod]
            ${Method},

            [Parameter(ParameterSetName = 'CustomMethod', Mandatory = $true)]
            [Parameter(ParameterSetName = 'CustomMethodNoProxy', Mandatory = $true)]
            [Alias('CM')]
            [ValidateNotNullOrEmpty()]
            [string]
            ${CustomMethod},

            [Parameter(ParameterSetName = 'CustomMethodNoProxy', Mandatory = $true)]
            [Parameter(ParameterSetName = 'StandardMethodNoProxy', Mandatory = $true)]
            [switch]
            ${NoProxy},

            [Parameter(ParameterSetName = 'StandardMethod')]
            [Parameter(ParameterSetName = 'CustomMethod')]
            [uri]
            ${Proxy},

            [Parameter(ParameterSetName = 'StandardMethod')]
            [Parameter(ParameterSetName = 'CustomMethod')]
            [pscredential]
            [System.Management.Automation.CredentialAttribute()]
            ${ProxyCredential},

            [Parameter(ParameterSetName = 'StandardMethod')]
            [Parameter(ParameterSetName = 'CustomMethod')]
            [switch]
            ${ProxyUseDefaultCredentials},

            [Parameter(ValueFromPipeline = $true)]
            [System.Object]
            ${Body},

            [string]
            ${ContentType},

            [ValidateSet('chunked', 'compress', 'deflate', 'gzip', 'identity')]
            [string]
            ${TransferEncoding},

            [string]
            ${InFile},

            [string]
            ${OutFile},

            [switch]
            ${PassThru},

            [switch]
            ${PreserveAuthorizationOnRedirect},

            [switch]
            ${SkipHeaderValidation})

        begin {
            if ($Credential -and (-not ($Authentication))) {
                $PSBoundParameters["Authentication"] = "Basic"
            }
            if ($InFile) {
                $multipartContent = [System.Net.Http.MultipartFormDataContent]::new()
                $FileStream = [System.IO.FileStream]::new($InFile, [System.IO.FileMode]::Open)
                $fileHeader = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("form-data")
                $fileHeader.Name = "file"
                $fileHeader.FileName = ([System.io.FileInfo]$InFile).name
                $fileContent = [System.Net.Http.StreamContent]::new($FileStream)
                $fileContent.Headers.ContentDisposition = $fileHeader
                $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream")
                $multipartContent.Add($fileContent)
                $PSBoundParameters["Headers"]['X-Atlassian-Token'] = 'nocheck'
                $PSBoundParameters["Body"] = $multipartContent
                $null = $PSBoundParameters.Remove("InFile")
            }
            try {
                $outBuffer = $null
                if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
                    $PSBoundParameters['OutBuffer'] = 1
                }
                $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Invoke-WebRequest', [System.Management.Automation.CommandTypes]::Cmdlet)
                $scriptCmd = { & $wrappedCmd @PSBoundParameters }
                $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
                $steppablePipeline.Begin($PSCmdlet)
            }
            catch {
                throw
            }
        }

        process {
            try {
                $steppablePipeline.Process($_)
                if ($SessionVariable) {
                    Set-Variable -Name $SessionVariable -Value (Get-Variable $SessionVariable).Value -Scope 1
                }
            }
            catch {
                throw
            }
        }

        end {
            try {
                $steppablePipeline.End()
            }
            catch {
                throw
            }
        }
    }
}
#endregion Invoke-WebRequest

#region Save-Configuration
function Save-Configuration {
    [CmdletBinding()]
    param()

    begin {
        Write-Verbose "Function started"

        Import-MqcnAlias -Alias "ExportConfiguration" -Command "Configuration\Export-Configuration"

        $configuration = Get-Configuration -AsHashtable
        $export = @{}
        foreach ($key in $configuration.Keys) {
            $export[$key] = $configuration[$key]
        }

        $serverList = [System.Collections.Generic.List[AtlassianPS.ServerData]]::new()
        foreach ($server in @($export["ServerList"])) {
            if (-not $server) { continue }

            $portable = ConvertTo-PortableServer -Server $server
            $serverData = [AtlassianPS.ServerData]$portable
            $serverList.Add($serverData)
        }
        $export["ServerList"] = $serverList

        ExportConfiguration -InputObject $export -ErrorAction Stop 3>$null

        Write-Verbose "Function ended"
    }
}
#endregion Save-Configuration

#region Format-MessageStyle
function Format-MessageStyle {
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [AllowEmptyString()]
        [String]
        $Message,

        [Parameter()]
        [AllowNull()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet,

        [Parameter()]
        [AllowNull()]
        [AtlassianPS.MessageStyle]
        $MessageSettings = $script:Configuration["Message"]
    )

    process {
        if (-not $MessageSettings) {
            $MessageSettings = [AtlassianPS.MessageStyle]::new()
        }

        $indent, $functionName, $timeStamp = ""

        if ($MessageSettings.BreadCrumbs) {
            "[$((Get-BreadCrumb) -replace '^Format-MessageStyle > ', '')]:"
            if ($MessageSettings.Indent) {
                $indent = " " * $MessageSettings.Indent
            }
            else {
                $indent = " " * 4
            }
        }
        elseif ($MessageSettings.FunctionName -and $Cmdlet) {
            $functionName = "[$($Cmdlet.MyInvocation.MyCommand.Name)] "
        }

        if ($MessageSettings.TimeStamp) {
            $timeStamp = "[$(Get-Date -Format "HH:mm:ss")] "
        }

        "{0}{1}{2}{3}" -f $timeStamp, $functionName, $indent, $Message
    }
}
#endregion Format-MessageStyle

#region Import-HttpUtility
function Import-HttpUtility {
    [CmdletBinding()]
    param()

    if ('System.Web.HttpUtility' -as [Type]) {
        return
    }

    if ($PSVersionTable.PSEdition -eq 'Desktop') {
        Add-Type -AssemblyName System.Web -ErrorAction Stop
    }
    else {
        Add-Type -AssemblyName System.Web.HttpUtility -ErrorAction Stop
    }
}
#endregion Import-HttpUtility

#region New-ErrorRecord
function New-ErrorRecord {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    [CmdletBinding()]
    [OutputType([System.Management.Automation.ErrorRecord])]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('ExistingException', 'NewException')]
        [String]
        $Mode,

        [Parameter()]
        [System.Exception]
        $Exception,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [String]
        $ExceptionType = 'System.Management.Automation.RuntimeException',

        [Parameter()]
        [String]
        $Message,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ErrorId,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $Category,

        [Parameter()]
        [Object]
        $TargetObject
    )

    if ($Mode -eq 'NewException') {
        if (-not $Message) {
            throw 'Message is required when creating a new exception.'
        }

        if ($Exception) {
            $Exception = New-Object -TypeName $ExceptionType -ArgumentList $Message, $Exception
        }
        else {
            $Exception = New-Object -TypeName $ExceptionType -ArgumentList $Message
        }
    }

    if (-not $Exception) {
        throw 'Exception is required when creating an error record from an existing exception.'
    }

    return [System.Management.Automation.ErrorRecord]::new($Exception, $ErrorId, $Category, $TargetObject)
}
#endregion New-ErrorRecord

#region Resolve-FullPath
function Resolve-FullPath {
    # .ExternalHelp ..\AtlassianPS.Configuration-help.xml
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript(
            {
                if (-not (Test-Path $_ -PathType Leaf)) {
                    $errorItem = [System.Management.Automation.ErrorRecord]::new(
                        ([System.ArgumentException]'File not found'),
                        'ParameterValue.FileNotFound',
                        [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                        $_
                    )
                    $errorItem.ErrorDetails = "No file could be found with the provided path '$_'."
                    $PSCmdlet.ThrowTerminatingError($errorItem)
                }
                else {
                    return $true
                }
            }
        )]
        [Alias('FullName', 'PSPath')]
        [String]
        $Path
    )

    process {
        $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path)
    }
}
#endregion Resolve-FullPath

#region ThrowError
function ThrowError {
    <#
    .SYNOPSIS
        Utility to throw a terminating errorrecord
    .NOTES
        Thanks to Jaykul:
        https://github.com/PoshCode/Configuration/blob/master/Source/Metadata.psm1
    #>

    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value),

        [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(ParameterSetName = "NewException")]
        [ValidateNotNullOrEmpty()]
        [System.Exception]
        $Exception,

        [Parameter(ParameterSetName = "NewException", Position = 2)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ExceptionType = "System.Management.Automation.RuntimeException",

        [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Message,

        [Parameter(Mandatory = $false)]
        [System.Object]
        $TargetObject,

        [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 10)]
        [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 10)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ErrorId,

        [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 11)]
        [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 11)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $Category,

        [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    process {
        if (!$ErrorRecord) {
            if ($PSCmdlet.ParameterSetName -eq "NewException") {
                if ($Exception) {
                    $Exception = New-Object $ExceptionType $Message, $Exception
                }
                else {
                    $Exception = New-Object $ExceptionType $Message
                }
            }
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject
        }
        $Cmdlet.ThrowTerminatingError($errorRecord)
    }
}
#endregion ThrowError

#region Write-Verbose
function Write-Verbose {
    <#
    .SYNOPSIS
        Write a verbose message
 
    .DESCRIPTION
        Write a verbose message
 
        This function allows the user to decide how the Verbose Message
        should be formatted. The configuration is inside `$scrpit:Configuration`
        and supports the following structure (json representation):
 
        {
            "message": {
                // show a line with the Bread Crumbs of the caller stack
                "breadcrumbs": true,
 
                // how many whitespaces should be used for indenting the
                // message
                "indent": true,
 
                // show the name of the calling function - this is ignored
                // if breadcrumbs is active
                "functionname": true,
 
                // show the timestamp (HH:mm:ss format) of the message
                "timestamp": true
            }
        }
 
    .EXAMPLE
        Write-DebugMessage "The value of `$var is: $var"
        ----------
        Description
        Shows the message if the user added `-Debug` to the command but does not create a breakpoint
    #>

    [CmdletBinding()]
    param(
        [Parameter( Mandatory, ValueFromPipeline )]
        [String]
        $Message,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = ((Get-Variable -Scope 1 PSCmdlet).Value)
    )

    begin {
        $indent, $functionName, $timeStamp = ""

        Import-MqcnAlias -Alias "WriteVerbose" -Command "Microsoft.PowerShell.Utility\Write-Verbose"

        $messageSettings = $script:Configuration["Message"]
    }

    process {

        if ((Get-PSCallstack | Select-Object -Last 1 -Skip 1).Arguments.Contains("Verbose")) {
            $VerbosePreference = 'Continue'
        }

        if ($messageSettings.Breadcrumbs) {
            WriteVerbose "[$(Get-BreadCrumb)]:"

            if ($messageSettings.Indent) {
                $indent = " " * $messageSettings.Indent
            }
            else {
                $indent = " " * 4
            }
        }
        else {
            if ($messageSettings.FunctionName) {
                $functionName = "[$($Cmdlet.MyInvocation.MyCommand.Name)] "
            }
        }

        if ($messageSettings.Timestamp) {
            $timeStamp = "[$(Get-Date -f "HH:mm:ss")] "
        }

        WriteVerbose ("{0}{1}{2}{3}" -f $timeStamp, $functionName, $indent, $Message)
    }
}
#endregion Write-Verbose

#region WriteError
function WriteError {
    <#
    .SYNOPSIS
        Utility to write an errorrecord to the errstd
    .NOTES
        Thanks to Jaykul:
        https://github.com/PoshCode/Configuration/blob/master/Source/Metadata.psm1
    #>

    param
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $((Get-Variable -Scope 1 PSCmdlet).Value),

        [Parameter(Mandatory = $true, ParameterSetName = "ExistingException", Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Parameter(ParameterSetName = "NewException")]
        [ValidateNotNullOrEmpty()]
        [System.Exception]
        $Exception,

        [Parameter(ParameterSetName = "NewException", Position = 2)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ExceptionType = "System.Management.Automation.RuntimeException",

        [Parameter(Mandatory = $true, ParameterSetName = "NewException", Position = 3)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Message,

        [Parameter(Mandatory = $false)]
        [System.Object]
        $TargetObject,

        [Parameter(Mandatory = $true, Position = 10)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ErrorId,

        [Parameter(Mandatory = $true, Position = 11)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $Category,

        [Parameter(Mandatory = $true, ParameterSetName = "Rethrow", Position = 1)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    process {
        if (!$ErrorRecord) {
            if ($PSCmdlet.ParameterSetName -eq "NewException") {
                if ($Exception) {
                    $Exception = New-Object $ExceptionType $Message, $Exception
                }
                else {
                    $Exception = New-Object $ExceptionType $Message
                }
            }
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $Exception, $ErrorId, $Category, $TargetObject
        }
        $Cmdlet.WriteError($errorRecord)
    }
}
#endregion WriteError

#endregion LoadFunctions

# Load configuration using
# https://github.com/PoshCode/Configuration
[Hashtable]$script:Configuration = Import-Configuration -CompanyName "AtlassianPS" -Name "AtlassianPS.Configuration"
if (-not $script:Configuration) { $script:Configuration = @{} }
if (-not $script:Configuration.ContainsKey("ServerList")) {
    $script:Configuration.Add("ServerList", [System.Collections.Generic.List[AtlassianPS.ServerData]]::new())
}
#endregion ModuleConfig