PowerAdobe.psm1

function ConvertTo-JsonLiteral { 
    <#
    .SYNOPSIS
    Converts an object to a JSON-formatted string.
 
    .DESCRIPTION
    The ConvertTo-Json cmdlet converts any object to a string in JavaScript Object Notation (JSON) format. The properties are converted to field names, the field values are converted to property values, and the methods are removed.
 
    .PARAMETER Object
    Specifies the objects to convert to JSON format. Enter a variable that contains the objects, or type a command or expression that gets the objects. You can also pipe an object to ConvertTo-JsonLiteral
 
    .PARAMETER Depth
    Specifies how many levels of contained objects are included in the JSON representation. The default value is 0.
 
    .PARAMETER AsArray
    Outputs the object in array brackets, even if the input is a single object.
 
    .PARAMETER DateTimeFormat
    Changes DateTime string format. Default "yyyy-MM-dd HH:mm:ss"
 
    .PARAMETER NumberAsString
    Provides an alternative serialization option that converts all numbers to their string representation.
 
    .PARAMETER BoolAsString
    Provides an alternative serialization option that converts all bool to their string representation.
 
    .PARAMETER PropertyName
    Uses PropertyNames provided by user (only works with Force)
 
    .PARAMETER NewLineFormat
    Provides a way to configure how new lines are converted for property names
 
    .PARAMETER NewLineFormatProperty
    Provides a way to configure how new lines are converted for values
 
    .PARAMETER PropertyName
    Allows passing property names to be used for custom objects (hashtables and alike are unaffected)
 
    .PARAMETER ArrayJoin
    Forces any array to be a string regardless of depth level
 
    .PARAMETER ArrayJoinString
    Uses defined string or char for array join. By default it uses comma with a space when used.
 
    .PARAMETER Force
    Forces using property names from first object or given thru PropertyName parameter
 
    .EXAMPLE
    Get-Process | Select-Object -First 2 | ConvertTo-JsonLiteral
 
    .EXAMPLE
    Get-Process | Select-Object -First 2 | ConvertTo-JsonLiteral -Depth 3
 
    .EXAMPLE
    Get-Process | Select-Object -First 2 | ConvertTo-JsonLiteral -NewLineFormat $NewLineFormat = @{
        NewLineCarriage = '\r\n'
        NewLine = "\n"
        Carriage = "\r"
    } -NumberAsString -BoolAsString
 
    .EXAMPLE
    Get-Process | Select-Object -First 2 | ConvertTo-JsonLiteral -NumberAsString -BoolAsString -DateTimeFormat "yyyy-MM-dd HH:mm:ss"
 
    .EXAMPLE
    # Keep in mind this advanced replace will break ConvertFrom-Json, but it's sometimes useful for projects like PSWriteHTML
    Get-Process | Select-Object -First 2 | ConvertTo-JsonLiteral -NewLineFormat $NewLineFormat = @{
        NewLineCarriage = '\r\n'
        NewLine = "\n"
        Carriage = "\r"
    } -NumberAsString -BoolAsString -AdvancedReplace @{ '.' = '\.'; '$' = '\$' }
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [alias('InputObject')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0, Mandatory)][Array] $Object,
        [int] $Depth,
        [switch] $AsArray,
        [string] $DateTimeFormat = "yyyy-MM-dd HH:mm:ss",
        [switch] $NumberAsString,
        [switch] $BoolAsString,
        [System.Collections.IDictionary] $NewLineFormat = @{
            NewLineCarriage = '\r\n'
            NewLine         = "\n"
            Carriage        = "\r"
        },
        [System.Collections.IDictionary] $NewLineFormatProperty = @{
            NewLineCarriage = '\r\n'
            NewLine         = "\n"
            Carriage        = "\r"
        },
        [System.Collections.IDictionary] $AdvancedReplace,
        [string] $ArrayJoinString,
        [switch] $ArrayJoin,
        [string[]]$PropertyName,
        [switch] $Force
    )
    Begin {
        $TextBuilder = [System.Text.StringBuilder]::new()
        $CountObjects = 0
        filter IsNumeric() {
            return $_ -is [byte] -or $_ -is [int16] -or $_ -is [int32] -or $_ -is [int64]  `
                -or $_ -is [sbyte] -or $_ -is [uint16] -or $_ -is [uint32] -or $_ -is [uint64] `
                -or $_ -is [float] -or $_ -is [double] -or $_ -is [decimal]
        }
        filter IsOfType() {
            return $_ -is [bool] -or $_ -is [char] -or $_ -is [datetime] -or $_ -is [string] `
                -or $_ -is [timespan] -or $_ -is [URI] `
                -or $_ -is [byte] -or $_ -is [int16] -or $_ -is [int32] -or $_ -is [int64] `
                -or $_ -is [sbyte] -or $_ -is [uint16] -or $_ -is [uint32] -or $_ -is [uint64] `
                -or $_ -is [float] -or $_ -is [double] -or $_ -is [decimal]
        }
        [int] $MaxDepth = $Depth
        [int] $InitialDepth = 0
    }
    Process {
        for ($a = 0; $a -lt $Object.Count; $a++) {
            $CountObjects++
            if ($CountObjects -gt 1) {
                $null = $TextBuilder.Append(',')
            }
            if ($Object[$a] -is [System.Collections.IDictionary]) {

                $null = $TextBuilder.AppendLine("{")
                for ($i = 0; $i -lt ($Object[$a].Keys).Count; $i++) {
                    $Property = ([string[]]$Object[$a].Keys)[$i] 
                    $DisplayProperty = $Property.Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
                    $null = $TextBuilder.Append("`"$DisplayProperty`":")
                    $Value = ConvertTo-StringByType -Value $Object[$a][$Property] -DateTimeFormat $DateTimeFormat -NumberAsString:$NumberAsString -BoolAsString:$BoolAsString -Depth $InitialDepth -MaxDepth $MaxDepth -TextBuilder $TextBuilder -NewLineFormat $NewLineFormat -NewLineFormatProperty $NewLineFormatProperty -Force:$Force -ArrayJoin:$ArrayJoin -ArrayJoinString $ArrayJoinString -AdvancedReplace $AdvancedReplace
                    $null = $TextBuilder.Append("$Value")
                    if ($i -ne ($Object[$a].Keys).Count - 1) {
                        $null = $TextBuilder.AppendLine(',')
                    }
                }
                $null = $TextBuilder.Append("}")
            } elseif ($Object[$a] | IsOfType) {
                $Value = ConvertTo-StringByType -Value $Object[$a] -DateTimeFormat $DateTimeFormat -NumberAsString:$NumberAsString -BoolAsString:$BoolAsString -Depth $InitialDepth -MaxDepth $MaxDepth -TextBuilder $TextBuilder -NewLineFormat $NewLineFormat -NewLineFormatProperty $NewLineFormatProperty -Force:$Force -ArrayJoin:$ArrayJoin -ArrayJoinString $ArrayJoinString -AdvancedReplace $AdvancedReplace
                $null = $TextBuilder.Append($Value)
            } else {
                $null = $TextBuilder.AppendLine("{")
                if ($Force -and -not $PropertyName) {
                    $PropertyName = $Object[0].PSObject.Properties.Name
                } elseif ($Force -and $PropertyName) {
                } else {
                    $PropertyName = $Object[$a].PSObject.Properties.Name
                }
                $PropertyCount = 0
                foreach ($Property in $PropertyName) {
                    $PropertyCount++
                    $DisplayProperty = $Property.Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
                    $null = $TextBuilder.Append("`"$DisplayProperty`":")
                    $Value = ConvertTo-StringByType -Value $Object[$a].$Property -DateTimeFormat $DateTimeFormat -NumberAsString:$NumberAsString -BoolAsString:$BoolAsString -Depth $InitialDepth -MaxDepth $MaxDepth -TextBuilder $TextBuilder -NewLineFormat $NewLineFormat -NewLineFormatProperty $NewLineFormatProperty -Force:$Force -ArrayJoin:$ArrayJoin -ArrayJoinString $ArrayJoinString -AdvancedReplace $AdvancedReplace

                    $null = $TextBuilder.Append("$Value")
                    if ($PropertyCount -ne $PropertyName.Count) {
                        $null = $TextBuilder.AppendLine(',')
                    }
                }
                $null = $TextBuilder.Append("}")
            }
            $InitialDepth = 0
        }
    }
    End {
        if ($CountObjects -gt 1 -or $AsArray) {
            "[$($TextBuilder.ToString())]"
        } else {
            $TextBuilder.ToString()
        }
    }
}
function Join-UriQuery { 
    <#
    .SYNOPSIS
    Provides ability to join two Url paths together including advanced querying
 
    .DESCRIPTION
    Provides ability to join two Url paths together including advanced querying which is useful for RestAPI/GraphApi calls
 
    .PARAMETER BaseUri
    Primary Url to merge
 
    .PARAMETER RelativeOrAbsoluteUri
    Additional path to merge with primary url (optional)
 
    .PARAMETER QueryParameter
    Parameters and their values in form of hashtable
 
    .PARAMETER EscapeUriString
    If set, will escape the url string
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts' -QueryParameter @{
        page = 1
        per_page = 20
        search = 'SearchString'
    }
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz/wp-json/wp/v2/posts' -QueryParameter @{
        page = 1
        per_page = 20
        search = 'SearchString'
    }
 
    .EXAMPLE
    Join-UriQuery -BaseUri 'https://evotec.xyz' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .NOTES
    General notes
    #>

    [alias('Join-UrlQuery')]
    [CmdletBinding()]
    param (
        [parameter(Mandatory)][uri] $BaseUri,
        [parameter(Mandatory = $false)][uri] $RelativeOrAbsoluteUri,
        [Parameter()][System.Collections.IDictionary] $QueryParameter,
        [alias('EscapeUrlString')][switch] $EscapeUriString
    )
    Begin {
        Add-Type -AssemblyName System.Web
    }
    Process {

        if ($BaseUri -and $RelativeOrAbsoluteUri) {
            $Url = Join-Uri -BaseUri $BaseUri -RelativeOrAbsoluteUri $RelativeOrAbsoluteUri
        } else {
            $Url = $BaseUri
        }

        if ($QueryParameter) {
            $Collection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty)
            foreach ($key in $QueryParameter.Keys) {
                $Collection.Add($key, $QueryParameter.$key)
            }
        }

        $uriRequest = [System.UriBuilder] $Url
        if ($Collection) {
            $uriRequest.Query = $Collection.ToString()
        }
        if (-not $EscapeUriString) {
            $uriRequest.Uri.AbsoluteUri
        } else {
            [System.Uri]::EscapeUriString($uriRequest.Uri.AbsoluteUri)
        }
    }
}
function Remove-EmptyValue { 
    <#
    .SYNOPSIS
    Removes empty values from a hashtable recursively.
 
    .DESCRIPTION
    This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values.
 
    .PARAMETER Hashtable
    The hashtable from which empty values will be removed.
 
    .PARAMETER ExcludeParameter
    An array of keys to exclude from the removal process.
 
    .PARAMETER Recursive
    Indicates whether to recursively remove empty values from nested hashtables.
 
    .PARAMETER Rerun
    Specifies the number of times to rerun the removal process recursively.
 
    .PARAMETER DoNotRemoveNull
    If specified, null values will not be removed.
 
    .PARAMETER DoNotRemoveEmpty
    If specified, empty string values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyArray
    If specified, empty array values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyDictionary
    If specified, empty dictionary values will not be removed.
 
    .EXAMPLE
    $hashtable = @{
        'Key1' = '';
        'Key2' = $null;
        'Key3' = @();
        'Key4' = @{}
    }
    Remove-EmptyValue -Hashtable $hashtable -Recursive
 
    Description
    -----------
    This example removes empty values from the $hashtable recursively.
 
    #>

    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function ConvertTo-StringByType { 
    <#
    .SYNOPSIS
    Private function to use within ConvertTo-JsonLiteral
 
    .DESCRIPTION
    Private function to use within ConvertTo-JsonLiteral
 
    .PARAMETER Value
    Value to convert to JsonValue
 
     .PARAMETER Depth
    Specifies how many levels of contained objects are included in the JSON representation. The default value is 0.
 
    .PARAMETER AsArray
    Outputs the object in array brackets, even if the input is a single object.
 
    .PARAMETER DateTimeFormat
    Changes DateTime string format. Default "yyyy-MM-dd HH:mm:ss"
 
    .PARAMETER NumberAsString
    Provides an alternative serialization option that converts all numbers to their string representation.
 
    .PARAMETER BoolAsString
    Provides an alternative serialization option that converts all bool to their string representation.
 
    .PARAMETER PropertyName
    Uses PropertyNames provided by user (only works with Force)
 
    .PARAMETER ArrayJoin
    Forces any array to be a string regardless of depth level
 
    .PARAMETER ArrayJoinString
    Uses defined string or char for array join. By default it uses comma with a space when used.
 
    .PARAMETER Force
    Forces using property names from first object or given thru PropertyName parameter
 
    .EXAMPLE
    $Value = ConvertTo-StringByType -Value $($Object[$a][$i]) -DateTimeFormat $DateTimeFormat
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Object] $Value,
        [int] $Depth,
        [int] $MaxDepth,
        [string] $DateTimeFormat,
        [switch] $NumberAsString,
        [switch] $BoolAsString,
        [System.Collections.IDictionary] $NewLineFormat = @{
            NewLineCarriage = '\r\n'
            NewLine         = "\n"
            Carriage        = "\r"
        },
        [System.Collections.IDictionary] $NewLineFormatProperty = @{
            NewLineCarriage = '\r\n'
            NewLine         = "\n"
            Carriage        = "\r"
        },
        [System.Collections.IDictionary] $AdvancedReplace,
        [System.Text.StringBuilder] $TextBuilder,
        [string[]] $PropertyName,
        [switch] $ArrayJoin,
        [string] $ArrayJoinString,
        [switch] $Force
    )
    Process {
        if ($null -eq $Value) {
            "`"`""
        } elseif ($Value -is [string]) {
            $Value = $Value.Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormat.NewLineCarriage).Replace("`n", $NewLineFormat.NewLine).Replace("`r", $NewLineFormat.Carriage)

            foreach ($Key in $AdvancedReplace.Keys) {
                $Value = $Value.Replace($Key, $AdvancedReplace[$Key])
            }
            "`"$Value`""
        } elseif ($Value -is [DateTime]) {
            "`"$($($Value).ToString($DateTimeFormat))`""
        } elseif ($Value -is [bool]) {
            if ($BoolAsString) {
                "`"$($Value)`""
            } else {
                $Value.ToString().ToLower()
            }
        } elseif ($Value -is [System.Collections.IDictionary]) {
            if ($MaxDepth -eq 0 -or $Depth -eq $MaxDepth) {
                "`"$($Value)`""
            } else {
                $Depth++
                $null = $TextBuilder.AppendLine("{")
                for ($i = 0; $i -lt ($Value.Keys).Count; $i++) {
                    $Property = ([string[]]$Value.Keys)[$i]
                    $DisplayProperty = $Property.Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
                    $null = $TextBuilder.Append("`"$DisplayProperty`":")
                    $OutputValue = ConvertTo-StringByType -Value $Value[$Property] -DateTimeFormat $DateTimeFormat -NumberAsString:$NumberAsString -BoolAsString:$BoolAsString -Depth $Depth -MaxDepth $MaxDepth -TextBuilder $TextBuilder -Force:$Force -ArrayJoinString $ArrayJoinString -ArrayJoin:$ArrayJoin.IsPresent
                    $null = $TextBuilder.Append("$OutputValue")
                    if ($i -ne ($Value.Keys).Count - 1) {
                        $null = $TextBuilder.AppendLine(',')
                    }
                }
                $null = $TextBuilder.Append("}")
            }
        } elseif ($Value -is [System.Collections.IList] -or $Value -is [System.Collections.ReadOnlyCollectionBase]) {
            if ($ArrayJoin) {
                $Value = $Value -join $ArrayJoinString
                $Value = "$Value".Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
                "`"$Value`""
            } else {
                if ($MaxDepth -eq 0 -or $Depth -eq $MaxDepth) {
                    $Value = "$Value".Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
                    "`"$Value`""
                } else {
                    $CountInternalObjects = 0
                    $null = $TextBuilder.Append("[")
                    foreach ($V in $Value) {
                        $CountInternalObjects++
                        if ($CountInternalObjects -gt 1) {
                            $null = $TextBuilder.Append(',')
                        }
                        if ($Force -and -not $PropertyName) {
                            $PropertyName = $V.PSObject.Properties.Name
                        } elseif ($Force -and $PropertyName) {
                        } else {
                            $PropertyName = $V.PSObject.Properties.Name
                        }
                        $OutputValue = ConvertTo-StringByType -Value $V -DateTimeFormat $DateTimeFormat -NumberAsString:$NumberAsString -BoolAsString:$BoolAsString -Depth $Depth -MaxDepth $MaxDepth -TextBuilder $TextBuilder -Force:$Force -PropertyName $PropertyName -ArrayJoinString $ArrayJoinString -ArrayJoin:$ArrayJoin.IsPresent
                        $null = $TextBuilder.Append($OutputValue)
                    }
                    $null = $TextBuilder.Append("]")
                }
            }
        } elseif ($Value -is [System.Enum]) {
            "`"$($($Value).ToString())`""
        } elseif (($Value | IsNumeric) -eq $true) {
            $Value = $($Value).ToString().Replace(',', '.')
            if ($NumberAsString) {
                "`"$Value`""
            } else {
                $Value
            }
        } elseif ($Value -is [PSObject]) {
            if ($MaxDepth -eq 0 -or $Depth -eq $MaxDepth) {
                "`"$($Value)`""
            } else {
                $Depth++
                $CountInternalObjects = 0
                $null = $TextBuilder.AppendLine("{")
                if ($Force -and -not $PropertyName) {
                    $PropertyName = $Value.PSObject.Properties.Name
                } elseif ($Force -and $PropertyName) {
                } else {
                    $PropertyName = $Value.PSObject.Properties.Name
                }
                foreach ($Property in $PropertyName) {
                    $CountInternalObjects++
                    if ($CountInternalObjects -gt 1) {
                        $null = $TextBuilder.AppendLine(',')
                    }
                    $DisplayProperty = $Property.Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
                    $null = $TextBuilder.Append("`"$DisplayProperty`":")
                    $OutputValue = ConvertTo-StringByType -Value $Value.$Property -DateTimeFormat $DateTimeFormat -NumberAsString:$NumberAsString -BoolAsString:$BoolAsString -Depth $Depth -MaxDepth $MaxDepth -TextBuilder $TextBuilder -Force:$Force -ArrayJoinString $ArrayJoinString -ArrayJoin:$ArrayJoin.IsPresent
                    $null = $TextBuilder.Append("$OutputValue")
                }
                $null = $TextBuilder.Append("}")
            }
        } else {
            $Value = $Value.ToString().Replace('\', "\\").Replace('"', '\"').Replace([System.Environment]::NewLine, $NewLineFormatProperty.NewLineCarriage).Replace("`n", $NewLineFormatProperty.NewLine).Replace("`r", $NewLineFormatProperty.Carriage)
            "`"$Value`""
        }
    }
}
function Join-Uri { 
    <#
    .SYNOPSIS
    Provides ability to join two Url paths together
 
    .DESCRIPTION
    Provides ability to join two Url paths together
 
    .PARAMETER BaseUri
    Primary Url to merge
 
    .PARAMETER RelativeOrAbsoluteUri
    Additional path to merge with primary url
 
    .EXAMPLE
    Join-Uri 'https://evotec.xyz/' '/wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri 'https://evotec.xyz/' 'wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri -BaseUri 'https://evotec.xyz/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .EXAMPLE
    Join-Uri -BaseUri 'https://evotec.xyz/test/' -RelativeOrAbsoluteUri '/wp-json/wp/v2/posts'
 
    .NOTES
    General notes
    #>

    [alias('Join-Url')]
    [cmdletBinding()]
    param(
        [parameter(Mandatory)][uri] $BaseUri,
        [parameter(Mandatory)][uri] $RelativeOrAbsoluteUri
    )

    return ($BaseUri.OriginalString.TrimEnd('/') + "/" + $RelativeOrAbsoluteUri.OriginalString.TrimStart('/'))
}
function Invoke-AdobeQuery {
    <#
    .SYNOPSIS
    Executes a query against the Adobe User Management API.
 
    .DESCRIPTION
    The Invoke-AdobeQuery function sends HTTP requests to the Adobe User Management API. It handles GET and other HTTP methods, manages pagination, and processes responses.
 
    .PARAMETER BaseUri
    The base URI for the Adobe User Management API. Defaults to 'https://usermanagement.adobe.io/v2/usermanagement'.
 
    .PARAMETER Url
    The endpoint URL for the specific API call.
 
    .PARAMETER Method
    The HTTP method to use for the request (e.g., GET, POST).
 
    .PARAMETER Data
    The data to include in the body of the request, applicable for methods like POST.
 
    .PARAMETER QueryParameter
    Additional query parameters to include in the request URI.
 
    .EXAMPLE
    Invoke-AdobeQuery -Url "users" -Method "GET"
 
    .EXAMPLE
    Invoke-AdobeQuery -Url "groups" -Method "POST" -Data $groupData
    #>

    [CmdletBinding()]
    param(
        [string] $BaseUri = 'https://usermanagement.adobe.io/v2/usermanagement',
        [Parameter(Mandatory)][string] $Url,
        [Parameter(Mandatory)][string] $Method,
        [Parameter()][System.Collections.IDictionary[]] $Data,
        [Parameter()][System.Collections.IDictionary] $QueryParameter
    )
    $Organization = $($($Script:AdobeTokenInformation).Organization)
    if ($Method -eq 'GET') {
        $Page = 0
        $UsedUrl = $Url
        Do {
            $UsedUrl = $Url.Replace('{orgId}', $Organization)
            $UsedUrl = $UsedUrl.Replace('{page}', $Page)
            $UriToUse = Join-UriQuery -BaseUri $BaseUri -RelativeOrAbsoluteUri $UsedUrl -QueryParameter $QueryParameter

            Write-Verbose -Message "Invoke-AdobeQuery - Url: $UriToUse / Method: $Method / Page: $Page"
            try {
                $Response = $null
                $Response = Invoke-WebRequest -Method Get -Uri $UriToUse -Headers $Script:AdobeTokenInformation.Headers -ErrorAction Stop -Verbose:$false
            } catch {
                if ($_.Exception.Response.StatusCode -eq 'TooManyRequests') {
                    $TimeToRetry = $_.Exception.Response.Headers.RetryAfter
                    Write-Warning -Message "Invoke-AdobeQuery - Too many requests. Retry after $TimeToRetry seconds"
                } else {
                    $ErrorDetails = $_.ErrorDetails
                    if ($_.ErrorDetails.Message) {
                        try {
                            $Message = $_.Exception.Message
                            $ErrorCode = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop
                            if ($ErrorCode) {
                                Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error code $($ErrorCode.error_Code) / $($ErrorCode.message)"
                            } else {
                                Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error $Message"
                            }
                        } catch {
                            Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error $($_.Exception.Message), ErrorDetails: $($ErrorDetails.Message)"
                        }
                    } else {
                        Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error $($_.Exception.Message), ErrorDetails: $($ErrorDetails.Message)"
                    }
                }
            }
            if ($Response) {
                $Output = $Response.Content | ConvertFrom-Json -ErrorAction SilentlyContinue
                if ($null -ne $Output.users) {
                    $Output.users
                } elseif ($null -ne $Output.groups) {
                    $Output.groups
                } elseif ($null -ne $Output.user) {
                    $Output.user
                } else {
                    $Output
                }
                if ($Output.Headers."-X-Page-Count") {
                    $MaximumPageCount = $Output.Headers."-X-Page-Count"
                }
            } else {
                Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Terminating"
                break
            }
            $Page++
        } while (-not $Output.LastPage -and $Page -le $MaximumPageCount)
    } else {
        $UriToUse = Join-UriQuery -BaseUri $BaseUri -RelativeOrAbsoluteUri "$Url/$Organization" -QueryParameter $QueryParameter

        Write-Verbose -Message "Invoke-AdobeQuery - Url: $UriToUse / Method: $Method"
        try {
            if ($PSVersionTable.PSVersion.Major -lt 6) {
                $DataJSON = @($Data) | ConvertTo-JsonLiteral -Depth 5 -AsArray
            } else {
                $DataJSON = @($Data) | ConvertTo-Json -Depth 5 -AsArray
            }
            $Response = Invoke-WebRequest -Method $Method -Uri $UriToUse -Headers $Script:AdobeTokenInformation.Headers -ErrorAction Stop -Verbose:$false -Body $DataJSON
        } catch {
            if ($_.Exception.Response.StatusCode -eq 'TooManyRequests') {
                $TimeToRetry = $_.Exception.Response.Headers.RetryAfter
                Write-Warning -Message "Invoke-AdobeQuery - Too many requests. Retry after $TimeToRetry seconds"
            } else {
                $ErrorDetails = $_.ErrorDetails
                if ($_.ErrorDetails.Message) {
                    try {
                        $Message = $_.Exception.Message
                        $ErrorCode = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop
                        if ($ErrorCode) {
                            Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error code $($ErrorCode.error_Code) / $($ErrorCode.message)"
                        } else {
                            Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error $Message"
                        }
                    } catch {
                        Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error $($_.Exception.Message), ErrorDetails: $($ErrorDetails.Message)"
                    }
                } else {
                    Write-Warning -Message "Invoke-AdobeQuery - Unable to connect to organization '$Organization'. Error $($_.Exception.Message), ErrorDetails: $($ErrorDetails.Message)"
                }
            }
        }
        if ($Response -and $Response.Content) {
            $Output = $Response.Content | ConvertFrom-Json -ErrorAction SilentlyContinue
            $Output
        } elseif ($Response) {
            $Response
        }
    }
}
function Add-AdobeGroupMember {
    <#
    .SYNOPSIS
    Adds a member to an Adobe group.
 
    .DESCRIPTION
    The Add-AdobeGroupMember cmdlet adds a specified user to one or more Adobe groups. It supports bulk processing for adding multiple groups at once.
 
    .PARAMETER GroupName
    The name(s) of the Adobe group(s) to which the user will be added.
 
    .PARAMETER Email
    The email address of the user to be added to the group(s).
 
    .PARAMETER BulkProcessing
    Enables bulk processing mode for adding multiple groups simultaneously.
 
    .EXAMPLE
    Add-AdobeGroupMember -GroupName "Admins" -Email "john.doe@example.com"
 
    .EXAMPLE
    Add-AdobeGroupMember -GroupName "Admins","Developers" -Email "john.doe@example.com" -BulkProcessing
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string[]] $GroupName,
        [Parameter(Mandatory)][string] $Email,
        [switch] $BulkProcessing
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Add-AdobeGroupMember - You need to connect to Adobe first using Connect-Adobe'
        return
    }
    if ($GroupName.Count -gt 10) {
        Write-Warning -Message 'Add-AdobeGroupMember - Only ten groups can be added at a time'
        return
    }
    $Data = [ordered] @{
        user      = $Email
        requestID = "action_$(Get-Random)"
        do        = @(
            @{
                'add' = @{
                    'group' = @(
                        $GroupName
                    )
                }
            }
        )
    }

    Remove-EmptyValue -Hashtable $Data -Recursive -Rerun 2

    if ($BulkProcessing) {
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($GroupName, 'Add Adobe Group Member')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}
function Connect-Adobe {
    <#
    .SYNOPSIS
    Connects to the Adobe API.
 
    .DESCRIPTION
    The Connect-Adobe cmdlet establishes a connection to the Adobe API using the provided credentials and scopes. It handles token retrieval and caching for subsequent API calls.
 
    .PARAMETER ClientID
    The Client ID for Adobe API authentication.
 
    .PARAMETER ClientSecret
    The Client Secret for Adobe API authentication.
 
    .PARAMETER ClientSecretEncrypted
    The encrypted Client Secret for Adobe API authentication.
 
    .PARAMETER Scopes
    The scopes for API access.
 
    .PARAMETER Organization
    The Adobe organization identifier.
 
    .PARAMETER ExistingToken
    Use an existing token if available.
 
    .PARAMETER DoNotSuppress
    Suppress suppression of the token information.
 
    .PARAMETER Force
    Force a new token retrieval even if a valid token exists.
 
    .EXAMPLE
    Connect-Adobe -ClientID "your_client_id" -ClientSecret "your_client_secret" -Scopes "openid, AdobeID" -Organization "your_org_id"
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string] $ClientID,

        [Parameter(Mandatory, ParameterSetName = 'ClearText')]
        [parameter(Mandatory)][string] $ClientSecret,

        [Parameter(Mandatory, ParameterSetName = 'Encrypted')]
        [alias('ApplicationSecretEncrypted', 'ApplicationKeyEncrypted')]
        [string] $ClientSecretEncrypted,

        [parameter(Mandatory)][string] $Scopes,
        [parameter(Mandatory)][string] $Organization,
        [switch] $ExistingToken,
        [switch] $DoNotSuppress,
        [switch] $Force
    )
    # Check for curent token
    $CurrentTime = (Get-Date).AddSeconds(2)

    if ($ClientSecretEncrypted) {
        try {
            $ApplicationKeyTemp = $ClientSecretEncrypted | ConvertTo-SecureString -ErrorAction Stop
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                throw
            } else {
                $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                Write-Warning -Message "Connect-Adobe - Error: $ErrorMessage"
                return
            }
        }
        $ApplicationKey = [System.Net.NetworkCredential]::new([string]::Empty, $ApplicationKeyTemp).Password
    } else {
        $ApplicationKey = $ClientSecret
    }

    if ($Script:AdobeTokenInformation.Expires -lt $CurrentTime -or $Force) {
        if ($ExistingToken) {
            Write-Verbose -Message "Connect-Adobe - Using existing token within command"
            $Url = $Script:AdobeTokenInformation.Url
            $Headers = $Script:AdobeTokenInformation.Headers
            $Body = $Script:AdobeTokenInformation.Body
        } else {
            $Headers = [ordered] @{
                'Content-Type' = 'application/x-www-form-urlencoded'
            }
            $Body = [ordered] @{
                'client_id'     = $ClientID
                'client_secret' = $ApplicationKey
                'grant_type'    = 'client_credentials'
                'scope'         = $Scopes
            }
            $Url = 'https://ims-na1.adobelogin.com/ims/token/v3'

            $Script:AdobeTokenInformation = [ordered] @{
                Organization = $Organization
                Url          = $Url
                Headers      = $Headers
                Token        = $null
                Expires      = $null
                Body         = $Body
            }
        }

        try {
            $Response = Invoke-RestMethod -Method Post -Uri $Url -Headers $Headers -Body $Body -ErrorAction Stop -Verbose:$false
        } catch {
            Write-Warning -Message "Connect-Adobe - Unable to connect to organization '$Organization' with user '$UserName'. Error $($_.Exception.Message)"
            return
        }

        $Script:AdobeTokenInformation.Token = $Response.access_token
        $Script:AdobeTokenInformation.Expires = [DateTime]::Now + ([TimeSpan]::FromSeconds($Response.expires_in))
        $Script:AdobeTokenInformation.Headers = @{
            #'Content-Type' = 'application/x-www-form-urlencoded'
            'Authorization' = "Bearer $($Script:AdobeTokenInformation.Token)"
            "X-Api-Key"     = "$ClientID"
            'Content-type'  = 'application/json'
        }
        if ($DoNotSuppress) {
            $Script:AdobeTokenInformation
        }
    } else {
        $WhenExpires = $Script:AdobeTokenInformation.expires - $CurrentTime
        Write-Verbose -Message "Connect-Adobe - Using existing cached token (Expires in: $WhenExpires)"
        if ($DoNotSuppress -and -not $ExistingToken) {
            $Script:AdobeTokenInformation
        }
    }
}
function Get-AdobeGroup {
    <#
    .SYNOPSIS
    Retrieves Adobe groups.
 
    .DESCRIPTION
    The Get-AdobeGroup cmdlet lists all user groups within the Adobe system. It requires an active Adobe connection.
 
    .EXAMPLE
    Get-AdobeGroup
    #>

    [CmdletBinding()]
    param(

    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Get-AdobeGroup - You need to connect to Adobe first using Connect-Adobe'
        return
    }

    Invoke-AdobeQuery -Url "groups/{orgId}/{page}" -Method 'GET'
}
function Get-AdobeGroupMember {
    <#
    .SYNOPSIS
    Retrieves members of a specified Adobe group.
 
    .DESCRIPTION
    The Get-AdobeGroupMember cmdlet fetches all members belonging to a specific group within the Adobe system. Requires an active Adobe connection.
 
    .PARAMETER GroupName
    The name of the group to retrieve members for.
 
    .EXAMPLE
    Get-AdobeGroupMember -GroupName "Admins"
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string] $GroupName
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Get-AdobeGroup - You need to connect to Adobe first using Connect-Adobe'
        return
    }

    Invoke-AdobeQuery -Url "users/{orgId}/{page}/$GroupName" -Method 'GET'
}
function Get-AdobeUser {
    <#
    .SYNOPSIS
    Retrieves Adobe user information.
 
    .DESCRIPTION
    The Get-AdobeUser cmdlet fetches details of a specific Adobe user or lists all users if no email is provided. Requires an active Adobe connection.
 
    .PARAMETER Email
    The email address of the user to retrieve information for.
 
    .EXAMPLE
    Get-AdobeUser -Email "jane.doe@example.com"
 
    .EXAMPLE
    Get-AdobeUser
    #>

    [CmdletBinding()]
    param(
        [string] $Email
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Get-AdobeUser - You need to connect to Adobe first using Connect-Adobe'
        return
    }

    if ($Email) {
        #Get user information : GET /v2/usermanagement/organizations/{orgId}/users/{userString}
        Invoke-AdobeQuery -Url "organizations/{orgId}/users/$Email" -Method 'GET'
    } else {
        #List all users : GET /v2/usermanagement/users/{orgId}/{page}
        Invoke-AdobeQuery -Url "users/{orgId}/{page}" -Method 'GET'
    }
}
function Invoke-AdobeBulk {
    <#
    .SYNOPSIS
    Executes bulk operations on Adobe users.
 
    .DESCRIPTION
    The Invoke-AdobeBulk cmdlet performs multiple Adobe user operations in a single request. It processes actions defined in a script block in batches of 20 to comply with Adobe's API limitations, ensures actions are executed in order, and aggregates the results to provide a single consolidated output at the end.
 
    .PARAMETER Actions
    A script block containing the bulk actions to execute. Each action should be a PowerShell cmdlet that modifies Adobe user or group information.
 
    .PARAMETER Suppress
    Suppresses the output of errors. When this switch is used, any errors encountered during the execution of bulk actions will not be displayed.
 
    .EXAMPLE
    # Execute bulk operations to add and update Adobe users
    Invoke-AdobeBulk {
        Add-AdobeUser -EmailAddress "john.doe@example.com" -Country "US" -FirstName "John" -LastName "Doe" -Type "createFederatedID" -BulkProcessing
        Set-AdobeUser -EmailAddress "john.doe@example.com" -LastName "Doe-Smith" -BulkProcessing
    }
 
    .EXAMPLE
    # Execute bulk operations and suppress error output
    Invoke-AdobeBulk {
        Add-AdobeUser -EmailAddress "jane.doe@example.com" -Country "UK" -FirstName "Jane" -LastName "Doe" -Type "createEnterpriseID" -BulkProcessing
        Set-AdobeUser -EmailAddress "jane.doe@example.com" -LastName "Doe-Smith" -BulkProcessing
    } -Suppress
 
    .NOTES
    - Ensure that you have connected to Adobe using `Connect-Adobe` before executing bulk operations.
    - The cmdlet processes actions in the order they are provided and aggregates the results to provide a comprehensive status report at the end of execution.
    - Adobe API limits bulk requests to 20 actions per batch. This cmdlet automatically handles batching to adhere to this limitation.
    - The aggregated output includes counts of completed, not completed, and completed in test mode actions, as well as any errors encountered during execution.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [scriptblock] $Actions,
        [switch] $Suppress
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Invoke-AdobeBulk - You need to connect to Adobe first using Connect-Adobe'
        return
    }

    $AllActions = & $Actions
    [Array] $ActionsToExecute = foreach ($Action in $AllActions) {
        $Action
    }

    # Initialize aggregation variables
    $aggregatedResult = [ordered] @{
        completed           = 0
        notCompleted        = 0
        completedInTestMode = 0
        result              = 'success'
        errors              = @()
    }

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess("Updates", 'Do bulk updates')) {
            $false 
        } else {
            $true 
        }
    }

    # Process actions in batches of 20
    for ($i = 0; $i -lt $ActionsToExecute.Count; $i += 20) {
        $Batch = $ActionsToExecute[$i..([math]::Min($i + 19, $ActionsToExecute.Count - 1))]

        $Batch | ConvertTo-Json -Depth 5 | Write-Verbose

        $batchOutput = Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Batch -QueryParameter $QueryParameter
        foreach ($ErrorMessage in $batchOutput.Errors) {
            Write-Warning -Message "Invoke-AdobeBulk - Processing error [user: $($ErrorMessage.User), index: $($ErrorMessage.Index)] - $($ErrorMessage.Message)"
        }

        # Aggregate results
        $aggregatedResult.completed += $batchOutput.completed
        $aggregatedResult.notCompleted += $batchOutput.notCompleted
        $aggregatedResult.completedInTestMode += $batchOutput.completedInTestMode
        if ($batchOutput.errors) {
            $aggregatedResult.errors += $batchOutput.errors
            #$aggregatedResult.failed += $batchOutput.errors.Count
        }

        # Update overall result status
        if ($batchOutput.result -eq 'failure') {
            $aggregatedResult.result = 'failure'
        } elseif ($batchOutput.result -ne 'success' -and $aggregatedResult.result -ne 'failure') {
            $aggregatedResult.result = 'partial'
        }
    }

    # Determine overall result based on aggregated data
    if ($aggregatedResult.failed -eq $ActionsToExecute.Count) {
        $aggregatedResult.result = 'failure'
    } elseif ($aggregatedResult.notCompleted -gt 0) {
        $aggregatedResult.result = 'partial'
    } else {
        $aggregatedResult.result = 'success'
    }

    if (-not $Suppress) {
        # Output the aggregated results
        $aggregatedResult
    }
}
function New-AdobeGroup {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string] $Name,
        [Parameter()][string] $Description,
        [ValidateSet('ignoreIfAlreadyExists', 'updateIfAlreadyExists')]
        [string] $Option = 'ignoreIfAlreadyExists',
        [switch] $BulkProcessing
    )

    $OptionList = @{
        ignoreIfAlreadyExists = 'ignoreIfAlreadyExists'
        updateIfAlreadyExists = 'updateIfAlreadyExists'
    }
    $OptionConverted = $OptionList[$Option]

    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Get-AdobeUser - You need to connect to Adobe first using Connect-Adobe'
        return
    }

    $Data = [ordered] @{
        usergroup = $Name
        requestID = "action_$(Get-Random)"
        do        = @(
            [ordered] @{
                'createUserGroup' = [ordered] @{
                    name        = $Name
                    description = $Description
                    option      = $OptionConverted
                }
            }
        )
    }

    Remove-EmptyValue -Hashtable $Data -Recursive -Rerun 2

    if ($BulkProcessing) {
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($Name, 'Create Adobe Group')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}
function New-AdobeUser {
    <#
    .SYNOPSIS
    Creates a new Adobe user.
 
    .DESCRIPTION
    The New-AdobeUser cmdlet adds a new user to the Adobe system with specified details. It supports bulk processing and different user types.
 
    .PARAMETER EmailAddress
    The email address of the new user.
 
    .PARAMETER Country
    The country of the new user.
 
    .PARAMETER FirstName
    The first name of the new user.
 
    .PARAMETER LastName
    The last name of the new user.
 
    .PARAMETER Option
    Determines how to handle existing users. Options are 'ignoreIfAlreadyExists' or 'updateIfAlreadyExists'.
 
    .PARAMETER Type
    Specifies the type of Adobe ID to create. Valid values are 'createEnterpriseID', 'addAdobeID', or 'createFederatedID'.
 
    .PARAMETER BulkProcessing
    Switch to enable bulk processing mode.
 
    .EXAMPLE
    New-AdobeUser -EmailAddress "jane.doe@example.com" -Country "US" -FirstName "Jane" -LastName "Doe" -Type "createFederatedID"
    #>

    [Alias('Add-AdobeUser')]
    [cmdletbinding(SupportsShouldProcess)]
    param(
        [string] $EmailAddress,
        [string] $Country,
        [string] $FirstName,
        [string] $LastName,

        [ValidateSet('ignoreIfAlreadyExists', 'updateIfAlreadyExists')]
        [string] $Option = 'ignoreIfAlreadyExists',

        [Parameter(Mandatory)]
        [ValidateSet('createEnterpriseID', 'addAdobeID', 'createFederatedID')]
        [string] $Type,
        [switch] $BulkProcessing
    )

    $List = @{
        createEnterpriseID = 'createEnterpriseID'
        addAdobeID         = 'addAdobeID'
        createFederatedID  = 'createFederatedID'
    }
    $OptionList = @{
        ignoreIfAlreadyExists = 'ignoreIfAlreadyExists'
        updateIfAlreadyExists = 'updateIfAlreadyExists'
    }
    $OptionConverted = $OptionList[$Option]

    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'New-AdobeUser - You need to connect to Adobe first using Connect-Adobe'
        return
    }
    # we need to convert the type to the correct format so it preservers problem casing
    $ConvertedType = $List[$Type]
    $Data = [ordered] @{
        user      = $EmailAddress
        requestID = "action_$(Get-Random)"
        do        = @(
            [ordered] @{
                $ConvertedType = [ordered] @{
                    email     = $EmailAddress
                    country   = $Country
                    firstname = $FirstName
                    lastname  = $LastName
                    option    = $OptionConverted
                }
            }
        )
    }
    if ($BulkProcessing) {
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($EmailAddress, 'Add Adobe User')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}


function Remove-AdobeGroup {
    <#
    .SYNOPSIS
    Removes an Adobe user group.
 
    .DESCRIPTION
    The Remove-AdobeGroup cmdlet deletes a specified user group from the Adobe system. It requires an active Adobe connection.
 
    .PARAMETER Name
    The name of the Adobe group to remove.
 
    .PARAMETER BulkProcessing
    Switch to enable bulk processing mode.
 
    .EXAMPLE
    Remove-AdobeGroup -Name "MarketingTeam"
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [alias('GroupName')][Parameter(Mandatory)][string] $Name,
        [switch] $BulkProcessing
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Remove-AdobeGroup - You need to connect to Adobe first using Connect-Adobe'
        return
    }
    $Data = [ordered] @{
        usergroup = $Name
        requestID = "action_$(Get-Random)"
        do        = @(
            [ordered] @{
                'deleteUserGroup' = [ordered] @{ }
            }
        )
    }

    Remove-EmptyValue -Hashtable $Data -Recursive -Rerun 2

    if ($BulkProcessing) {
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($Name, 'Remove Adobe Group Member')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}
function Remove-AdobeGroupMember {
    <#
    .SYNOPSIS
    Removes a member from one or more Adobe groups.
 
    .DESCRIPTION
    The Remove-AdobeGroupMember cmdlet removes a specified user from one or more Adobe groups. Use the -All switch to remove the user from all groups.
 
    .PARAMETER GroupName
    The name(s) of the Adobe group(s) from which the user will be removed.
 
    .PARAMETER Email
    The email address of the user to be removed from the group(s).
 
    .PARAMETER All
    Removes the user from all Adobe groups.
 
    .EXAMPLE
    Remove-AdobeGroupMember -GroupName "Marketing" -Email "user@example.com"
 
    .EXAMPLE
    Remove-AdobeGroupMember -All -Email "user@example.com"
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string[]] $GroupName,
        [Parameter(Mandatory)][string] $Email,
        [switch] $All
    )

    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Remove-AdobeGroupMember - You need to connect to Adobe first using Connect-Adobe'
        return
    }
    if ($GroupName.Count -gt 10) {
        Write-Warning -Message 'Remove-AdobeGroupMember - Only ten groups can be added at a time'
        return
    }
    if ($All) {
        $Data = [ordered] @{
            user      = $Email
            requestID = "action_$(Get-Random)"
            do        = @(
                @{
                    'remove' = 'all'
                }
            )
        }
    } else {
        $Data = [ordered] @{
            user      = $Email
            requestID = "action_$(Get-Random)"
            do        = @(
                @{
                    'remove' = @{
                        'group' = @(
                            $GroupName
                        )
                    }
                }
            )
        }
    }

    Remove-EmptyValue -Hashtable $Data -Recursive -Rerun 2

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($GroupName, 'Remove Adobe Group Member')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}
function Remove-AdobeUser {
    <#
    .SYNOPSIS
    Removes an Adobe user.
 
    .DESCRIPTION
    The Remove-AdobeUser cmdlet deletes a specified user from the Adobe system. Optionally, it can also delete the user's account.
 
    .PARAMETER EmailAddress
    The email address of the Adobe user to remove.
 
    .PARAMETER DoNotDeleteAccount
    When specified, the user's account will not be deleted, only their association with groups.
 
    .PARAMETER BulkProcessing
    Switch to enable bulk processing mode.
 
    .EXAMPLE
    Remove-AdobeUser -EmailAddress "jane.doe@example.com" -DoNotDeleteAccount
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string] $EmailAddress,
        [switch] $DoNotDeleteAccount,
        [switch] $BulkProcessing
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Remove-AdobeUser - You need to connect to Adobe first using Connect-Adobe'
        return
    }
    $Data = [ordered] @{
        user      = $EmailAddress
        requestID = "action_$(Get-Random)"
        do        = @(
            [ordered] @{
                'removeFromOrg' = [ordered] @{
                    deleteAccount = -not $DoNotDeleteAccount.IsPresent
                }
            }
        )
    }

    Remove-EmptyValue -Hashtable $Data -Recursive -Rerun 2

    if ($BulkProcessing) {
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($EmailAddress, 'Remove Adobe User')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}
function Set-AdobeGroup {
    <#
    .SYNOPSIS
    Updates an Adobe user group.
 
    .DESCRIPTION
    The Set-AdobeGroup cmdlet updates the name and/or description of an existing Adobe user group.
 
    .PARAMETER Name
    The current name of the Adobe group to update.
 
    .PARAMETER NewName
    The new name for the Adobe group.
 
    .PARAMETER Description
    The new description for the Adobe group.
 
    .PARAMETER BulkProcessing
    Switch to enable bulk processing mode.
 
    .EXAMPLE
    Set-AdobeGroup -Name "Developers" -NewName "Senior Developers" -Description "Group for senior developer roles"
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string] $Name,
        [Parameter()][string] $NewName,
        [Parameter()][string] $Description,
        [switch] $BulkProcessing
    )
    if (-not $Script:AdobeTokenInformation) {
        Write-Warning -Message 'Set-AdobeGroup - You need to connect to Adobe first using Connect-Adobe'
        return
    }
    if (-not $NewName -and -not $Description) {
        Write-Warning -Message 'Set-AdobeGroup - You need to provide either a new name or a description'
        return
    }

    $Data = [ordered] @{
        usergroup = $Name
        requestID = "action_$(Get-Random)"
        do        = @(
            [ordered] @{
                'updateUserGroup' = [ordered] @{
                    name        = $NewName
                    description = $Description
                }
            }
        )
    }

    Remove-EmptyValue -Hashtable $Data -Recursive -Rerun 2

    if ($BulkProcessing) {
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($Name, 'Set Adobe Group')) {
            $false 
        } else {
            $true 
        }
    }

    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}
function Set-AdobeUser {
    <#
    .SYNOPSIS
    Update Adobe User information using Adobe API
 
    .DESCRIPTION
    Update Adobe User information using Adobe API. Applies only to Enterprise and Federated users.
    Independent Adobe IDs are managed by the individual user and cannot be updated through the User Management API.
    Attempting to update information for a user who has an Adobe ID will result in an error.
 
    .PARAMETER EmailAddress
    The current email address of the Adobe user to update.
 
    .PARAMETER NewEmailAddress
    The new email address for the Adobe user.
 
    .PARAMETER Country
    The country of the Adobe user.
 
    .PARAMETER FirstName
    The first name of the Adobe user.
 
    .PARAMETER LastName
    The last name of the Adobe user.
 
    .PARAMETER UserName
    The username of the Adobe user.
 
    .PARAMETER BulkProcessing
    Switch to enable bulk processing mode.
 
    .EXAMPLE
    Set-AdobeUser -EmailAddress 'przemek@test.pl' -NewEmailAddress 'przemek@test1.pl' -LastName 'Klys' -WhatIf -Verbose
    $SetInformation
 
    .NOTES
    General notes
    #>

    [cmdletbinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][string] $EmailAddress,
        [string] $NewEmailAddress,
        [string] $Country,
        [string] $FirstName,
        [string] $LastName,
        [string] $UserName,
        [switch] $BulkProcessing
    )

    if (-not $Script:AdobeTokenInformation) {
        # Ensure Adobe is connected before proceeding
        Write-Warning -Message 'Set-AdobeUser - You need to connect to Adobe first using Connect-Adobe'
        return
    }

    $UpdateObject = [ordered] @{
        "update" = [ordered] @{
            email     = $NewEmailAddress
            country   = $Country
            firstname = $FirstName
            lastname  = $LastName
            username  = $UserName
        }
    }

    # Remove any empty values from the update object
    Remove-EmptyValue -Hashtable $UpdateObject -Recursive -Rerun 2
    if (-not $UpdateObject) {
        # Warn if no update values are provided
        Write-Warning -Message 'Set-AdobeUser - You need to provide at least one value to update'
        return
    }
    $Data = [ordered] @{
        user      = $EmailAddress
        requestID = "action_$(Get-Random)"
        do        = @(
            $UpdateObject
        )
    }

    if ($BulkProcessing) {
        # Return data for bulk processing
        return $Data
    }

    $Data | ConvertTo-Json -Depth 5 | Write-Verbose

    $QueryParameter = [ordered] @{
        testOnly = if ($PSCmdlet.ShouldProcess($EmailAddress, 'Update Adobe User')) {
            $false 
        } else {
            $true 
        }
    }

    # Invoke the Adobe API with the prepared data
    Invoke-AdobeQuery -Url "action" -Method 'POST' -Data $Data -QueryParameter $QueryParameter
}

if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -lt 379893) {
    Write-Warning "This module requires .NET Framework 4.5.2 or later."; return 
} 

# Export functions and aliases as required
Export-ModuleMember -Function @('Add-AdobeGroupMember', 'Connect-Adobe', 'Get-AdobeGroup', 'Get-AdobeGroupMember', 'Get-AdobeUser', 'Invoke-AdobeBulk', 'New-AdobeGroup', 'New-AdobeUser', 'Remove-AdobeGroup', 'Remove-AdobeGroupMember', 'Remove-AdobeUser', 'Set-AdobeGroup', 'Set-AdobeUser') -Alias @('Add-AdobeUser')
# SIG # Begin signature block
# MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCytbyNQI3bKaBx
# oN1yyDk3hPrYZA6K9dvVGyPUMBS2X6CCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp
# BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy
# NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD
# VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD
# Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo
# 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO
# H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R
# eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX
# j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV
# DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J
# idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C
# acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N
# Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X
# CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB
# AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X
# LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l
# U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU
# aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS
# R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB
# b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC
# zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1
# UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp
# ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE
# sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8
# a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF
# YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q
# EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE
# deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J
# ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq
# F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX
# DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz
# dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ
# cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA
# 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh
# vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn
# 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr
# W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w
# iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP
# lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf
# QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28
# eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4
# fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0
# 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC
# AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE
# FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz
# ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3
# dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu
# Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB
# CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E
# BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc
# 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ
# f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51
# 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev
# xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx
# h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY
# D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/
# Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq
# G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH
# v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP
# AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw
# PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2
# IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF
# AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor
# BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3
# DQEJBDEiBCA6X5qRzQSykYggEW82m29AFHlGVNq402WRnCt3QjeTBzANBgkqhkiG
# 9w0BAQEFAASCAgAgVPF846XoCxs8dDwpGkXgGRv3BQJuC4fI5wq8DwUfq7EKYuFB
# QcuMGg6hHykI/irLU3aFw6jzr8VjpdUTvM4h+8JVJ/JYyhCbjt+4bXmlWq0WqUu8
# wD2o7n7lpBWSJDM7HOx3Bq2SYkbtkHaR8vfeUFOGOCS2kxiXKCwyT8xm8DX6CM0c
# r9+ARdQiIabCVUFyPm9E58GgkvPKt32hMfxOgrSGg05KGO/qB2GrYBL5f3fJUykh
# nIplN+HgJpWWvpXZFqg19Ogn672ws5amKRqTSp4f646uT0wTiYozqu2hnU9qvx03
# G0OeNHVBl77QXffrrhtg/riHG7tA5gZbSGKwjnvflBRTxHMWOnCBfeGcjLy3azH/
# yFJVxNMpwP2gU5fwaOXnJgNwZilRCEN8D3u3ibA2DX3BC6fiEb7XzcPoJGnAIFVt
# vQGEl3XXdj86b5dokne7AQHaRVW+4ZU0f064/HyAez9eySmAgwuuiOmMXn/CQpmj
# eKMzzyISnDLhgLeTJpkUM4BgADRBEDikfZCxBDTRvBrEjvEvO+DXwY90KI1d9tWh
# AVcOO1hIyAif9raSIVWws1p6k20bCyUPqT7v5AGuOSUE7GlhVrEHeJmS+k9Gy4Tw
# ABuAzddEGgX42e4gYh+wtK7xAgHpy7uOiM999MZgC7JHV59X1OLG5I28IqGCAyAw
# ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj
# MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNDEyMTMwNzQzMzBaMC8GCSqGSIb3DQEJBDEiBCDn9Orb
# iz4ML8XDgOHwqYEJsHqpzaDC6i/1Kywrjv+DLTANBgkqhkiG9w0BAQEFAASCAgBc
# sTF6WwIP5h1Jt2NLSbMC5p1eYyGpnkCdg9WksZrJy0NzcPh/CspfsFZT6svD2UOZ
# hNmyIO7lbfIioAwGvLLrLBJy8ZdEFmAa+RzPRAVVFARGXYpb16P8pLVp0ywdF2xi
# 46PVpuhJbBE09+vGKo09m0J5mRdcoOcVeO8HvrQIfLa2OtvqyzwF9vnYhct4LsAq
# WF9VJ2RqkJin6qZxkojuLw8l2/9cdKP98gldfDS9MTpMSMQuMwoWM5ZPiKCPxxNf
# VQRY6eZOnRjqoOEH1X0SupdcoL394iWbukILYNSAYOUACPUKaxaJ7hDyTTx//jsk
# +MkWPxaQD6SjXjO/HzRH4zR9Y2BdCEDhzfTf13sLkLCvXzBo/P3FFq2mOP7jRZ68
# vQZa5qVw/IvtxvGhAvx4/qvqxg/jp31gqd0ro42v78zFU1G+muCHdS7SBE94W+KB
# GWntaqNdsDHgtklLo+mXGR8AuT1XTs2sL6/tmevF1o0FOjBoIEVaaxvsAdT0N7/m
# avYb9KAM23kFpRyhwHk106eH87SJyCdAacbcGp8KF9J9bY5iH79PDRStFO7YeBRw
# TVhZqir8QAW7R/EDAPSI5PCauhk67uDazUuTOXr6PQtosmSdxUIXVENUnPhurxP8
# rWVfkoYaguT1dAzjMzQ05EYy1CaWoUsFDaHgyHQEaw==
# SIG # End signature block