psresourceget.ps1

## Copyright (c) Microsoft Corporation. All rights reserved.
## Licensed under the MIT License.

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [ValidateSet('repository', 'psresource', 'repositorylist', 'psresourcelist')]
    [string]$ResourceType,
    [Parameter(Mandatory = $true)]
    [ValidateSet('get', 'set', 'test', 'delete', 'export')]
    [string]$Operation,
    [Parameter(ValueFromPipeline)]
    $stdinput,
    [switch]$WhatIf
)

enum Scope {
    CurrentUser
    AllUsers
}

enum ExitCode {
    Success = 0
    Error = 1
    RepositoryNotFound = 2
    RepositoryNotTrusted = 3
    InstallationFailed = 4
    UnknownResourceType = 5
    ResourceNotImplemented = 6
    TestNotImplemented = 7
    ExportNotImplemented = 8
    GetNotImplemented = 9
    SetNotImplemented = 10
    DeleteNotImplemented = 11
    UnknownOperation = 12
}

class PSResource {
    [string]$name
    [string]$version
    [Scope]$scope
    [string]$repositoryName
    [bool]$preRelease
    [bool]$_exist
    [bool]$_inDesiredState
    [object]$_metadata

    PSResource([string]$name, [string]$version, [Scope]$scope, [string]$repositoryName, [bool]$preRelease) {
        $this.name = $name
        $this.version = $version
        $this.scope = $scope
        $this.repositoryName = $repositoryName
        $this.preRelease = $preRelease
        $this._exist = $true
    }

    PSResource([string]$name) {
        $this.name = $name
        $this._exist = $false
    }

    [bool] IsInDesiredState([PSResource] $other) {
        $retValue = $true

        $psResourceSplat = @{
            Name           = $this.name
            Version        = if ($this.version) { $this.version } else { '*' }
        }

        Get-PSResource @psResourceSplat | Where-Object {
            ($null -eq $this.scope -or $_.Scope -eq $this.scope) -and
            ($null -eq $this.repositoryName -or $_.Repository -eq $this.repositoryName)
        } | Select-Object -First 1 | ForEach-Object {
            Write-Trace -message "Matching resource found: Name=$($_.Name), Version=$($_.Version), Scope=$($_.Scope), Repository=$($_.Repository), PreRelease=$($_.PreRelease)" -level debug
            $this._exist = $true
        }

        if ($this.name -ne $other.name) {
            Write-Trace -message "Name mismatch: $($this.name) vs $($other.name)" -level debug
            $retValue = $false
        }
        elseif ($null -ne $this.version -and $null -ne $other.version -and -not (SatisfiesVersion -version $this.version -versionRange $other.version)) {
            Write-Trace -message "Version mismatch: $($this.version) vs $($other.version)" -level debug
            $retValue = $false
        }
        elseif ($null -ne $this.scope -and $this.scope -ne $other.scope) {
            Write-Trace -message "Scope mismatch: $($this.scope) vs $($other.scope)" -level debug
            $retValue = $false
        }
        elseif ($null -ne $this.repositoryName -and $this.repositoryName -ne $other.repositoryName) {
            Write-Trace -message "Repository mismatch: $($this.repositoryName) vs $($other.repositoryName)" -level debug
            $retValue = $false
        }
        elseif ($this._exist -ne $other._exist) {
            Write-Trace -message "_exist mismatch: $($this._exist) vs $($other._exist)" -level debug
            $retValue = $false
        }

        return $retValue
    }

    [string] ToJson() {
        [string[]]$excludeProps = @('_inDesiredState')
        if ($null -eq $this._metadata) { $excludeProps += '_metadata' }
        $retVal = ($this | Select-Object -ExcludeProperty $excludeProps | ConvertTo-Json -Compress -EnumsAsStrings)
        Write-Trace -message "Serializing PSResource to JSON. Name: $($this.name), Version: $($this.version), Scope: $($this.scope), RepositoryName: $($this.repositoryName), PreRelease: $($this.preRelease), _exist: $($this._exist)" -level debug
        Write-Trace -message "Serialized JSON: $retVal" -level trace
        return $retVal
    }

    [string] ToJsonForTest() {
        [string[]]$excludeProps = @()
        if ($null -eq $this._metadata) { $excludeProps += '_metadata' }
        return ($this | Select-Object -ExcludeProperty $excludeProps | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings)
    }
}

class PSResourceList {
    [string]$repositoryName
    [PSResource[]]$resources
    [bool]$trustedRepository
    [bool]$_inDesiredState

    PSResourceList([string]$repositoryName, [PSResource[]]$resources, [bool]$trustedRepository) {
        $this.repositoryName = $repositoryName
        $this.resources = $resources
        $this.trustedRepository = $trustedRepository
    }

    [bool] IsInDesiredState([PSResourceList] $other) {
        if ($this.repositoryName -ne $other.repositoryName) {
            Write-Trace -message "RepositoryName mismatch: $($this.repositoryName) vs $($other.repositoryName)" -level debug
            return $false
        }

        if ($null -ne $this.resources -and $this.resources.Count -ne $other.resources.Count) {
            Write-Trace -message "Resources count mismatch: $($this.resources.Count) vs $($other.resources.Count)" -level debug
            return $false
        }

        foreach ($otherResource in $other.resources) {
            $found = $false
            foreach ($resource in $this.resources) {
                if ($resource.IsInDesiredState($otherResource)) {
                    $found = $true
                    break
                }
            }

            if ($found) {
                Write-Trace -message "Resource match found for: $($otherResource.name)" -level debug
                break
            }
            else {
                Write-Trace -message "Resource mismatch for: $($otherResource.name)" -level debug
                return $false
            }
        }

        return $true
    }

    [string] ToJson() {
        ## Assign the array directly so that an empty list serializes as [] rather than null
        [object[]]$resourceObjects = @()
        if ($this.resources) {
            $resourceObjects = @($this.resources | ForEach-Object {
                [string[]]$excludeProps = @('_inDesiredState')
                if ($null -eq $_._metadata) { $excludeProps += '_metadata' }
                $_ | Select-Object -ExcludeProperty $excludeProps
            })
        }
        $retVal = [ordered]@{
            repositoryName = $this.repositoryName
            resources      = $resourceObjects
        } | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings
        Write-Trace -message "Serializing PSResourceList to JSON. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug
        Write-Trace -message "Serialized JSON: $retVal" -level trace
        return $retVal
    }

    [string] ToJsonForTest() {
        Write-Trace -message "Serializing PSResourceList to JSON for test output. RepositoryName: $($this.repositoryName), TrustedRepository: $($this.trustedRepository), Resources count: $($this.resources.Count)" -level debug
        [object[]]$resourceObjects = @()
        if ($this.resources) {
            $resourceObjects = @($this.resources | ForEach-Object {
                [string[]]$excludeProps = @()
                if ($null -eq $_._metadata) { $excludeProps += '_metadata' }
                if ($excludeProps.Count -gt 0) { $_ | Select-Object -ExcludeProperty $excludeProps } else { $_ }
            })
        }
        $retVal = [ordered]@{
            repositoryName    = $this.repositoryName
            resources         = $resourceObjects
            trustedRepository = $this.trustedRepository
            _inDesiredState   = $this._inDesiredState
        } | ConvertTo-Json -Compress -Depth 5 -EnumsAsStrings
        Write-Trace -message "Serialized JSON: $retVal" -level trace
        return $retVal
    }
}

class Repository {
    [string]$name
    [string]$uri
    [bool]$trusted
    [int]$priority
    [string]$repositoryType
    [bool]$_exist

    Repository([string]$name) {
        $this.name = $name
        $this._exist = $false
        $this.repositoryType = 'Unknown'
    }

    Repository([string]$name, [string]$uri, [bool]$trusted, [int]$priority, [string]$repositoryType) {
        $this.name = $name
        $this.uri = $uri
        $this.trusted = $trusted
        $this.priority = $priority
        $this.repositoryType = $repositoryType
        $this._exist = $true
    }

    Repository([PSCustomObject]$repositoryInfo) {
        $this.name = $repositoryInfo.Name
        $this.uri = $repositoryInfo.Uri
        $this.trusted = $repositoryInfo.Trusted
        $this.priority = $repositoryInfo.Priority
        $this.repositoryType = $repositoryInfo.ApiVersion
        $this._exist = $true
    }

    Repository([string]$name, [bool]$exist) {
        $this.name = $name
        $this._exist = $exist
        $this.repositoryType = 'Unknown'
    }

    [string] ToJson() {
        return ($this | ConvertTo-Json -Compress -EnumsAsStrings)
    }
}

function Write-Trace {
    param(
        [string]$message,

        [ValidateSet('error', 'warn', 'info', 'debug', 'trace')]
        [string]$level = 'trace'
    )

    $trace = [pscustomobject]@{
        $level.ToLower() = $message
    } | ConvertTo-Json -Compress

    $host.ui.WriteErrorLine($trace)
}

function SatisfiesVersion {
    param(
        [string]$version,
        [string]$versionRange
    )

    $typeName = 'NuGet.Versioning.VersionRange'

    Write-Trace -message "Checking if version '$version' satisfies version range '$versionRange'." -level debug

    if ($typeName -as [type]) {
        Write-Trace -message "NuGet.Versioning assembly is already loaded. Using existing assembly." -level debug
    }
    else {
        Write-Trace -message "Loading NuGet.Versioning assembly from $PSScriptRoot/dependencies/NuGet.Versioning.dll" -level debug
        Add-Type -Path "$PSScriptRoot/dependencies/NuGet.Versioning.dll" -ErrorAction Stop | Out-Null
    }

    try {
        $versionRangeObj = [NuGet.Versioning.VersionRange]::Parse($versionRange)
        $resourceVersion = [NuGet.Versioning.NuGetVersion]::Parse($version)
        return $versionRangeObj.Satisfies($resourceVersion)
    }
    catch {
        Write-Trace -message "Error parsing version or version range: $($_.Exception.Message)" -level error
        return $false
    }
}

function ConvertInputToPSResource(
    [PSCustomObject]$inputObj,
    [string]$repositoryName = $null
) {
    $scope = if ($inputObj.Scope) { [Scope]$inputObj.Scope } else { [Scope]"CurrentUser" }

    $psResource = [PSResource]::new(
        $inputObj.Name,
        $inputObj.Version,
        $scope,
        $inputObj.repositoryName ? $inputObj.repositoryName : $repositoryName,
        $inputObj.PreRelease
    )

    if ($null -ne $inputObj._exist) {
        $psResource._exist = $inputObj._exist
    }

    return $psResource
}

# catch any un-caught exception and write it to the error stream
trap {
    Write-Trace -message "Exiting with error code 1 due to unhandled exception: $($_.Exception.Message)" -level debug
    exit [ExitCode]::Error
}

function GetPSResourceList {
    param(
        [PSCustomObject]$inputObj
    )

    $inputResources = @()
    $inputResources += if ($inputObj.resources) {
        $inputObj.resources | ForEach-Object {
            ConvertInputToPSResource -inputObj $_ -repositoryName $inputObj.repositoryName
        }
    }

    $repositoryState = Get-PSResourceRepository -Name $inputObj.repositoryName -ErrorAction SilentlyContinue

    if (-not $repositoryState) {
        Write-Trace -message "Repository not found: $($inputObj.repositoryName)" -level info
        $emptyResources = @()
        $emptyResources += $inputResources | ForEach-Object {
            [PSResource]::new($_.Name)
        }

        return [PSResourceList]::new($inputObj.repositoryName, $emptyResources, $false)
    }

    $inputPSResourceList = [PSResourceList]::new($inputObj.repositoryName, $inputResources, $repositoryState.Trusted)

    $allPSResources = @()

    if ($inputPSResourceList.repositoryName) {
        $currentUserPSResources = Get-PSResource -Scope CurrentUser -ErrorAction SilentlyContinue | Where-Object { $_.Repository -eq $inputPSResourceList.RepositoryName }
        $allUsersPSResources = Get-PSResource -Scope AllUsers -ErrorAction SilentlyContinue | Where-Object { $_.Repository -eq $inputPSResourceList.RepositoryName }
    }

    $allPSResources += $currentUserPSResources | ForEach-Object {
        [PSResource]::new(
            $_.Name,
            $_.Prerelease ? $_.Version.ToString() + "-" + $_.Prerelease : $_.Version.ToString(),
            [Scope]"CurrentUser",
            $_.Repository,
            $_.PreRelease
        )
    }

    $allPSResources += $allUsersPSResources | ForEach-Object {
        [PSResource]::new(
            $_.Name,
            $_.Prerelease ? $_.Version.ToString() + "-" + $_.Prerelease : $_.Version.ToString(),
            [Scope]"AllUsers",
            $_.Repository,
            $_.PreRelease ? $true : $false
        )
    }

    $resolvedResources = @()

    foreach ($inputResource in $inputResources) {
        $matchingResources = $allPSResources | Where-Object { $_.Name -eq $inputResource.Name }

        if ($matchingResources) {
            $preferred = $null
            if ($inputResource.Version) {
                $preferred = $matchingResources | Where-Object {
                    try { SatisfiesVersion -version $_.Version -versionRange $inputResource.Version } catch { $false }
                } | Select-Object -First 1
            }
            elseif (-not ($resolvedResources | Where-Object { $_.Name -eq $inputResource.Name })) {
                # No version constraint: any installed version means the resource exists.
                # Only record the first match so that one input resource maps to one current resource.
                Write-Trace -message "No version constraint for input: $($inputResource.Name). Treating installed version $($matchingResources[0].Version) as a match." -level debug
                $preferred = $matchingResources | Select-Object -First 1
            }

            if ($preferred) {
                Write-Trace -message "Resource '$($inputResource.Name)' version '$($preferred.Version)' satisfies requested range '$($inputResource.Version)'." -level debug
                $resolvedResources += $preferred
            }
            else {
                # Installed but doesn't satisfy the version range - report actual installed version with _exist = false
                $fallback = $matchingResources | Select-Object -First 1
                Write-Trace -message "Resource '$($inputResource.Name)' installed at '$($fallback.Version)' does not satisfy requested range '$($inputResource.Version)'. Reporting _exist = false." -level debug
                $fallback._exist = $false
                $resolvedResources += $fallback
            }
        }
        else {
            Write-Trace -message "Resource '$($inputResource.Name)' is not installed. Reporting _exist = false." -level debug
            $resolvedResources += [PSResource]::new($inputResource.Name)
        }
    }

    PopulatePSResourceListObjectByRepository -resourcesExist $resolvedResources -inputResources $inputResources -repositoryName $inputPSResourceList.RepositoryName -trustedRepository $inputPSResourceList.trustedRepository
}

function GetOperation {
    param(
        [string]$ResourceType
    )

    if ([string]::IsNullOrEmpty($stdinput)) {
        Write-Trace -level error -message "Get operation requires --input with the resource properties. No input was provided."
        exit [ExitCode]::Error
    }

    $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop

    Write-Trace -message "Starting Get operation for ResourceType: $ResourceType" -level trace

    switch ($ResourceType) {
        'repository' {
            $inputRepository = [Repository]::new($inputObj)
            $rep = Get-PSResourceRepository -Name $inputRepository.Name -ErrorVariable err -ErrorAction SilentlyContinue
            Write-Trace -message "Get-PSResourceRepository returned: $($rep | ConvertTo-Json -Compress)" -level trace

            $ret = if ($err.FullyQualifiedErrorId -eq 'ErrorGettingSpecifiedRepo,Microsoft.PowerShell.PSResourceGet.Cmdlets.GetPSResourceRepository') {
                Write-Trace -message "Repository not found: $($inputRepository.Name). Returning _exist = false" -level debug
                [Repository]::new(
                    $InputRepository.Name,
                    $false
                )
            }
            else {
                [Repository]::new(
                    $rep.Name,
                    $rep.Uri,
                    $rep.Trusted,
                    $rep.Priority,
                    $rep.ApiVersion
                )

                Write-Trace -message "Returning repository object for: $($ret.Name)" -level trace
            }

            Write-Trace -message "Serialized JSON output for Get operation: $($ret.ToJson())" -level trace

            return ( $ret.ToJson() )
        }

        'repositorylist' {
            Write-Trace -level error -message "Get operation is not implemented for RepositoryList resource."
            exit [ExitCode]::ResourceNotImplemented
        }
        'psresource' {
            Write-Trace -level error -message "Get operation is not implemented for PSResource resource."
            exit [ExitCode]::ResourceNotImplemented
        }
        'psresourcelist' {
            (GetPSResourceList -inputObj $inputObj).ToJson()
        }
        default {
            Write-Trace -level error -message "Unknown ResourceType: $ResourceType"
            exit [ExitCode]::ResourceNotImplemented
        }
    }
}

function TestPSResourceList {
    param(
        [PSCustomObject]$inputObj
    )

    $inputResources = @()
    $inputResources += $inputObj.resources | ForEach-Object { ConvertInputToPSResource -inputObj $_ -repositoryName $inputObj.repositoryName }

    $repositoryState = Get-PSResourceRepository -Name $inputObj.repositoryName -ErrorAction SilentlyContinue

    if (-not $repositoryState) {
        Write-Trace -message "Repository not found: $($inputObj.repositoryName). Returning PSResourceList with _inDesiredState = false." -level debug
        $retValue = [PSResourceList]::new($inputObj.repositoryName, $inputResources, $false)
        $retValue._inDesiredState = $false
        $retValue.ToJsonForTest()
        '["repositoryName", "resources"]'
    }

    $inputPSResourceList = [PSResourceList]::new($inputObj.repositoryName, $inputResources, $repositoryState.Trusted)

    $currentState = GetPSResourceList -inputObj $inputObj
    $inDesiredState = $currentState.IsInDesiredState($inputPSResourceList)

    $currentState._inDesiredState = $inDesiredState

    if ($inDesiredState) {
        Write-Trace -message "PSResourceList is in desired state." -level debug
        $currentState.ToJsonForTest()
        ## Return empty array as we are in desired state and there are no differing properties
        '[]'
    }
    else {
        Write-Trace -message "PSResourceList is NOT in desired state." -level debug
        $inputPSResourceList.ToJsonForTest()
        '["resources"]'
    }
}

function TestOperation {
    param(
        [string]$ResourceType
    )

    $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop

    switch ($ResourceType) {
        'repository' {
            Write-Trace -level error -message "Test operation is not implemented for Repository resource."
            exit [ExitCode]::TestNotImplemented
        }
        'repositorylist' {
            Write-Trace -level error -message "Test operation is not implemented for RepositoryList resource."
            exit [ExitCode]::TestNotImplemented
        }
        'psresource' {
            Write-Trace -level error -message "Test operation is not implemented for PSResource resource."
            exit [ExitCode]::TestNotImplemented
        }
        'psresourcelist' {
            TestPSResourceList -inputObj $inputObj
        }

        default {
            Write-Trace -level error -message "Unknown ResourceType: $ResourceType"
            exit [ExitCode]::UnknownResourceType
        }
    }
}

function ExportOperation {
    switch ($ResourceType) {
        'repository' {
            $rep = Get-PSResourceRepository -ErrorAction SilentlyContinue

            if (-not $rep) {
                Write-Trace -message "No repositories found. Returning empty array." -level debug
                return @()
            }

            $rep | ForEach-Object {
                [Repository]::new(
                    $_.Name,
                    $_.Uri,
                    $_.Trusted,
                    $_.Priority,
                    $_.ApiVersion
                ).ToJson()
            }
        }

        'repositorylist' {
            Write-Trace -level error -message "Export operation is not implemented for RepositoryList resource."
            exit [ExitCode]::ExportNotImplemented
        }
        'psresource' {
            Write-Trace -level error -message "Export operation is not implemented for PSResource resource."
            exit [ExitCode]::ExportNotImplemented
        }
        'psresourcelist' {
            $currentUserPSResources = Get-PSResource
            $allUsersPSResources = Get-PSResource -Scope AllUsers
            PopulatePSResourceListObject -allUsersPSResources $allUsersPSResources -currentUserPSResources $currentUserPSResources
        }
        default {
            Write-Trace -level error -message "Unknown ResourceType: $ResourceType"
            exit [ExitCode]::UnknownResourceType
        }
    }
}

function WhatIfPSResourceList {
    param(
        $inputObj
    )

    $repositoryName = $inputObj.repositoryName
    $currentState = GetPSResourceList -inputObj $inputObj
    $projectedResources = @()
    $inputObj.resources | ForEach-Object {
        $resourceDesiredState = ConvertInputToPSResource -inputObj $_ -repositoryName $repositoryName
        $name = $resourceDesiredState.name
        $version = $resourceDesiredState.version
        $scope = if ($resourceDesiredState.scope) { $resourceDesiredState.scope } else { [Scope]'CurrentUser' }
        $currentResource = $currentState.resources | Where-Object { $_.name -eq $name } | Select-Object -First 1

        if (-not $resourceDesiredState._exist -and $null -ne $currentResource -and $currentResource._exist) {
            $msg = "Would uninstall resource '$name'"
            Write-Trace -message "WhatIf: $msg." -level debug
            $resource = [PSResource]::new(
                $currentResource.name,
                $currentResource.version,
                $currentResource.scope,
                $currentResource.repositoryName,
                $currentResource.preRelease
            )
            $resource._exist = $false
            $resource._metadata = [pscustomobject]@{ whatIf = @($msg) }
            $projectedResources += $resource
        }
        elseif ($resourceDesiredState._exist -and ($null -eq $currentResource -or -not $currentResource._exist)) {
            $versionStr = if ($version) { $version } else { 'latest' }
            $msg = "Would install resource '$name' version '$versionStr'"
            Write-Trace -message "WhatIf: $msg." -level debug
            $resource = [PSResource]::new($name, $versionStr, [Scope]$scope, $repositoryName, $resourceDesiredState.preRelease)
            $resource._metadata = [pscustomobject]@{ whatIf = @($msg) }
            $projectedResources += $resource
        }
        else {
            Write-Trace -message "WhatIf: Resource '$name' is already in desired state." -level debug
            if ($null -ne $currentResource) {
                $projectedResources += $currentResource
            }
            else {
                $projectedResources += $resourceDesiredState
            }
        }
    }

    ## Report the same failures a real set operation would hit before installing anything
    $installRequired = @($projectedResources | Where-Object { $_._exist -and $null -ne $_._metadata }).Count -gt 0
    if ($installRequired) {
        $psRepository = Get-PSResourceRepository -Name $repositoryName -ErrorAction SilentlyContinue

        if (-not $psRepository) {
            Write-Trace -level error -message "Repository '$repositoryName' not found. Cannot install resources."
            exit [ExitCode]::RepositoryNotFound
        }

        if (-not $psRepository.Trusted -and -not $inputObj.trustedRepository) {
            Write-Trace -level error -message "Repository '$repositoryName' is not trusted. Cannot install resources."
            exit [ExitCode]::RepositoryNotTrusted
        }
    }

    $list = [PSResourceList]::new($repositoryName, $projectedResources, $currentState.trustedRepository)
    $list.ToJson()
}

function SetPSResourceList {
    param(
        $inputObj,
        [switch]$WhatIf
    )

    if ($WhatIf) {
        return WhatIfPSResourceList -inputObj $inputObj
    }

    $repositoryName = $inputObj.repositoryName
    $resourcesToUninstall = @()
    $resourcesToInstall = [System.Collections.Generic.Dictionary[string, psobject]]::new()

    $resourcesChanged = $false

    $currentState = GetPSResourceList -inputObj $inputObj

    $inputObj.resources | ForEach-Object {
        $resourceDesiredState = ConvertInputToPSResource -inputObj $_ -repositoryName $repositoryName
        $name = $resourceDesiredState.name
        $version = $resourceDesiredState.version
        $scope = if ($resourceDesiredState.scope) { $resourceDesiredState.scope } else { "CurrentUser" }

        # Resource should not exist - uninstall if it does
        $currentState.resources | ForEach-Object {

            $isInDesiredState = $_.IsInDesiredState($resourceDesiredState)

            # Uninstall if resource should not exist but does
            if (-not $resourceDesiredState._exist -and $_._exist) {
                Write-Trace -message "Resource $($resourceDesiredState.name) exists but _exist is false. Adding to uninstall list." -level debug
                $resourcesToUninstall += $_
            }
            # Install if resource should exist but doesn't, or exists but not in desired state
            elseif ($resourceDesiredState._exist -and (-not $_._exist -or -not $isInDesiredState)) {
                Write-Trace -message "Resource $($resourceDesiredState.name) needs to be installed." -level debug
                $versionStr = if ($version) { $resourceDesiredState.version } else { 'latest' }
                $key = $name.ToLowerInvariant() + '-' + $versionStr.ToLowerInvariant()
                if (-not $resourcesToInstall.ContainsKey($key)) {
                    $resourcesToInstall[$key] = $resourceDesiredState
                }
            }
            # Otherwise resource is in desired state, no action needed
            else {
                Write-Trace -message "Resource $($resourceDesiredState.name) is in desired state." -level debug
            }
        }
    }

    if ($resourcesToUninstall.Count -gt 0) {
        Write-Trace -message "Uninstalling resources: $($resourcesToUninstall | ForEach-Object { "$($_.Name) - $($_.Version)" })" -level debug
        $resourcesToUninstall | ForEach-Object {
            $cmdWarnings = $null
            Uninstall-PSResource -Name $_.Name -Scope $scope -ErrorAction Stop -WarningVariable cmdWarnings
            foreach ($w in $cmdWarnings) {
                Write-Trace -message ([string]$w) -level warn
            }
        }
        $resourcesChanged = $true
    }

    if ($resourcesToInstall.Count -gt 0) {
        $psRepository = Get-PSResourceRepository -Name $repositoryName -ErrorAction SilentlyContinue

        if (-not $psRepository) {
            Write-Trace -level error -message "Repository '$repositoryName' not found. Cannot install resources."
            exit [ExitCode]::RepositoryNotFound
        }

        if (-not $psRepository.Trusted -and -not $inputObj.trustedRepository) {
            Write-Trace -level error -message "Repository '$repositoryName' is not trusted. Cannot install resources."
            exit [ExitCode]::RepositoryNotTrusted
        }

        Write-Trace -message "Installing resources: $($resourcesToInstall.Values | ForEach-Object { " $($_.Name) -- $($_.Version) " })" -level debug
        $resourcesToInstall.Values | ForEach-Object {
            $usePrerelease = if ($_.preRelease) { $true } else { $false }

            $installErrors = @()

            $name = $_.Name
            $version = $_.Version

            try {
                $cmdWarnings = $null
                Install-PSResource -Name $_.Name -Version $_.Version -Scope $scope -Repository $repositoryName -ErrorAction Stop -TrustRepository:$inputObj.trustedRepository -Prerelease:$usePrerelease -Reinstall -WarningVariable cmdWarnings
                foreach ($w in $cmdWarnings) {
                    Write-Trace -message ([string]$w) -level warn
                }
            } catch {
                Write-Trace -level error -message "Failed to install resource '$name' with version '$version'. Error: $($_.Exception.Message)"
                $installErrors += $_.Exception.Message
            }

            if ($installErrors.Count -gt 0) {
                Write-Trace -level error -message "One or more errors occurred while installing resource '$name' with version '$version': $($installErrors -join '; ')"
                Write-Trace -level trace -message "Exiting with error code 4 due to installation failure."
                exit [ExitCode]::InstallationFailed
            }
        }

        $resourcesChanged = $true
    }

    (GetPSResourceList -inputObj $inputObj).ToJson()
    if ($resourcesChanged) {
        '["resources"]'
    }
    else {
        '[]'
    }
}

function SetOperation {
    param(
        [string]$ResourceType
    )

    $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop

    switch ($ResourceType) {
        'repository' {
            $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorAction SilentlyContinue

            $properties = @('name', 'uri', 'trusted', 'priority', 'repositoryType')

            $splatt = @{}

            foreach ($property in $properties) {
                if ($null -ne $inputObj.PSObject.Properties[$property]) {
                    if ($property -eq 'repositoryType') {
                        $splatt['ApiVersion'] = $inputObj.$property
                    }
                    else {
                        $splatt[$property] = $inputObj.$property
                    }
                }
            }

            if ($null -eq $rep -and $inputObj._exist -ne $false) {
                Register-PSResourceRepository @splatt
            }
            else {
                if ($inputObj._exist -eq $false) {
                    Write-Trace -message "Repository $($inputObj.Name) exists and _exist is false. Deleting it." -level debug
                    Unregister-PSResourceRepository -Name $inputObj.Name
                }
                else {
                    Set-PSResourceRepository @splatt
                }
            }

            return GetOperation -ResourceType $ResourceType
        }

        'repositorylist' {
            Write-Trace -level error -message "Set operation is not implemented for RepositoryList resource."
            exit [ExitCode]::SetNotImplemented
        }
        'psresource' {
            Write-Trace -level error -message "Set operation is not implemented for PSResource resource."
            exit [ExitCode]::SetNotImplemented
        }
        'psresourcelist' { return SetPSResourceList -inputObj $inputObj -WhatIf:$WhatIf }
        default {
            Write-Trace -level error -message "Unknown ResourceType: $ResourceType"
            exit [ExitCode]::UnknownResourceType
        }
    }
}

function DeleteOperation {
    param(
        [string]$ResourceType
    )

    $inputObj = $stdinput | ConvertFrom-Json -ErrorAction Stop
    switch ($ResourceType) {
        'repository' {
            if ($inputObj._exist -ne $false) {
                throw "_exist property is not set to false for the repository. Cannot delete."
            }

            $rep = Get-PSResourceRepository -Name $inputObj.Name -ErrorAction SilentlyContinue

            if ($null -ne $rep) {
                Unregister-PSResourceRepository -Name $inputObj.Name
            }
            else {
                Write-Trace -message "Repository not found: $($inputObj.Name). Nothing to delete." -level debug
            }

            return GetOperation -ResourceType $ResourceType
        }
        'repositorylist' {
            Write-Trace -level error -message "Delete operation is not implemented for RepositoryList resource."
            exit [ExitCode]::DeleteNotImplemented
        }
        'psresource' {
            Write-Trace -level error -message "Delete operation is not implemented for PSResource resource."
            exit [ExitCode]::DeleteNotImplemented
        }
        'psresourcelist' {
            Write-Trace -level error -message "Delete operation is not implemented for PSResourceList resource."
            exit [ExitCode]::DeleteNotImplemented
        }
        default {
            Write-Trace -level error -message "Unknown ResourceType: $ResourceType"
            exit [ExitCode]::UnknownResourceType
        }
    }
}

function PopulatePSResourceListObjectByRepository {
    param (
        $resourcesExist,
        $inputResources,
        $repositoryName,
        $trustedRepository
    )

    $resources = @()

    if (-not $resourcesExist) {
        $resources = $inputResources | ForEach-Object {
            [PSResource]::new(
                $_.Name
            )
        }
    }
    else {
        $resources += $resourcesExist | ForEach-Object {
            $srcExist = $_._exist
            $r = if ($_.version) {
                [PSResource]::new(
                    $_.Name,
                    $_.Version.ToString(),
                    $_.Scope,
                    $_.RepositoryName,
                    $_.PreRelease ? $true : $false
                )
            } else {
                [PSResource]::new($_.Name)
            }
            $r._exist = $srcExist
            $r
        }
    }

    $psresourceListObj =
    [PSResourceList]::new(
        $repositoryName,
        $resources,
        $trustedRepository
    )

    return $psresourceListObj
}

function PopulatePSResourceListObject {
    param (
        $allUsersPSResources,
        $currentUserPSResources
    )

    $allPSResources = @()

    $allPSResources += $allUsersPSResources | ForEach-Object {
        return [PSResource]::new(
            $_.Name,
            $_.Version,
            [Scope]"AllUsers",
            $_.Repository,
            $_.PreRelease ? $true : $false
        )
    }

    $allPSResources += $currentUserPSResources | ForEach-Object {
        return [PSResource]::new(
            $_.Name,
            $_.Version,
            [Scope]"CurrentUser",
            $_.Repository,
            $_.PreRelease ? $true : $false
        )
    }

    $repoGrps = $allPSResources | Group-Object -Property repositoryName

    $repoGrps | ForEach-Object {
        $repositoryTrust = if ($_.Name) { (Get-PSResourceRepository -Name $_.Name -ErrorAction SilentlyContinue).Trusted } else { $false }
        $repoName = $_.Name
        $resources = $_.Group
        [PSResourceList]::new($repoName, $resources, $repositoryTrust).ToJson()
    }
}

## This is mostly needed for CI tests as the PSModulePath has a different version PSResourceGet
## If the module is loaded from a different path, then we get an error "Assembly with same name is already loaded"
if ($null -eq (Get-Module -Name Microsoft.PowerShell.PSResourceGet)) {
    $path = Join-Path -Path $PSScriptRoot -ChildPath "Microsoft.PowerShell.PSResourceGet.psd1"
    Write-Trace -level trace -message "Importing Microsoft.PowerShell.PSResourceGet module from path: $path"
    Import-Module -Name $path -Force -ErrorAction Stop
}

# Suppress warnings from PSResourceGet cmdlets to prevent them from reaching stdout and
# breaking DSC's JSON parsing. Warnings should be captured on individual cmdlets
$WarningPreference = 'SilentlyContinue'

switch ($Operation.ToLower()) {
    'get' { return (GetOperation -ResourceType $ResourceType) }
    'set' { return (SetOperation -ResourceType $ResourceType) }
    'test' { return (TestOperation -ResourceType $ResourceType) }
    'export' { return (ExportOperation -ResourceType $ResourceType) }
    'delete' { return (DeleteOperation -ResourceType $ResourceType) }
    default {
        Write-Trace -level error -message "Unknown Operation: $Operation"
        exit [ExitCode]::UnknownOperation
    }
}

# SIG # Begin signature block
# MIInRAYJKoZIhvcNAQcCoIInNTCCJzECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDtmX6pofbNRiNn
# hIbPhM7MJAxcS0SAfz0Q5EY34FcuCKCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghngMIIZ3AIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ
# KoZIhvcNAQkEMSIEIO1Xft2IOfoKZzgDkhl5ppa0TDCYoERiUWtemTPoYlU2MEIG
# CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAMpXtcqxBza11eZD3
# FsPkujgvu65ARx97Se4Wh/j5hc2WhsuuDIHoz+wMUIP2dgxMU7SEJB9MkUx15uDb
# VlQJrAOfonX1+MBVXhaP5BaqaIOvqWsZiij8jqWML71CdXUBjDakLRyhzT6L08tP
# D+BLSrQ5IpVkfTVvDPXHQbTRHJQhZRoIXqVr9HOvgdMwOgA994fSLGo0JdqTMwC3
# qTWEinD7/lYg+3+ke7k1j/lWSFHlVoCUOqVClScPonoWWYLusTKXQugk/EPFoHQy
# bWjQm8mW2JaM9m11xntgSHPmwf4hHBlXkWw8wU5mOP+qGcF4+rdLNPnZje6GthYC
# T5vkr6GCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kw
# gheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF
# MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCChy+V/jB5gIItv
# IVDLN9/pQkkF01ZgMx4Z7ZCBDS/YawIGaomHjvsdGBMyMDI2MDkxODIyMTg1Ni4z
# MDdaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo1OTFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIF
# EKADAgECAhMzAAACFI3NI0TuBt9yAAEAAAIUMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxOFoXDTI2MTEx
# MzE4NDgxOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjU5MUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAyU+nWgCUyvfyGP1zTFkLkdgOutXcVteP/0CeXfrF/66c
# hKl4/MZDCQ6E8Ur4kqgCxQvef7Lg1gfso1EWWKG6vix1VxtvO1kPGK4PZKmOeoeL
# 68F6+Mw2ERPy4BL2vJKf6Lo5Z7X0xkRjtcvfM9T0HDgfHUW6z1CbgQiqrExs2NH2
# 7rWpUkyTYrMG6TXy39+GdMOTgXyUDiRGVHAy3EqYNw3zSWusn0zedl6a/1DbnXIc
# vn9FaHzd/96EPNBOCd2vOpS0Ck7kgkjVxwOptsWa8I+m+DA43cwlErPaId84GbdG
# zo3VoO7YhCmQIoRab0d8or5Pmyg+VMl8jeoN9SeUxVZpBI/cQ4TXXKlLDkfbzzSQ
# riViQGJGJLtKS3DTVNuBqpjXLdu2p2Yq9ODPqZCoiNBh4CB6X2iLYUSO8tmbUVLM
# MEegbvHSLXQR88QNICjFoBBDCDydoTo9/TNkq80mO77wDM04tPdvbMmxT01GTod6
# 0JJxUGmMTgseghdBGjkN+D6GsUpY7ta7hP9PzLrs+Alxu46XT217bBn6EwJsAYAc
# 9C28mKRUcoIZWQRb+McoZaSu2EcSzuIlAaNIQNtGlz2PF3foSeGmc/V7gCGs8AHk
# iKwXzJSPftnsH8O/R3pJw2D/2hHE3JzxH2SrLX1FdI7Drw145PkL0hbFL6MVCCkC
# AwEAAaOCAUkwggFFMB0GA1UdDgQWBBTbX/bs1cSpyTYnYuf/Mt9CPNhwGzAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH
# gDANBgkqhkiG9w0BAQsFAAOCAgEAP3xp9D4Gu0SH9B+1JH0hswFquINaTT+RjpfE
# r8UmUOeDl4U5uV+i28/eSYXMxgem3yBZywYDyvf4qMXUvbDcllNqRyL2Rv8jSu8w
# clt/VS1+c5cVCJfM+WHvkUr+dCfUlOy9n4exCPX1L6uWwFH5eoFfqPEp3Fw30irM
# N2SonHBK3mB8vDj3D80oJKqe2tatO38yMTiREdC2HD7eVIUWL7d54UtoYxzwkJN1
# t7gEEGosgBpdmwKVYYDO1USWSNmZELglYA4LoVoGDuWbN7mD8VozYBsfkZarOyrJ
# YlF/UCDZLB8XaLfrMfMyZTMCOuEuPD4zj8jy/Jt40clrIW04cvLhkhkydBzcrmC2
# HxeE36gJsh+jzmivS9YvyiPhLkom1FP0DIFr4VlqyXHKagrtnqSF8QyEpqtQS7wS
# 7ZzZF0eZe0fsYD0J1RarbVuDxmWsq45n1vjRdontuGUdmrG2OGeKd8AtiNghfnab
# VBbgpYgcx/eLyW/n40eTbKIlsm0cseyuWvYFyOqQXjoWtL4/sUHxlWIsrjnNarNr
# +POkL8C1jGBCJuvm0UYgjhIaL+XBXavrbOtX9mrZ3y8GQDxWXn3mhqM21ZcGk83x
# SRqB9ecfGYNRG6g65v635gSzUmBKZWWcDNzwAoxsgEjTFXz6ahfyrBLqshrjJXPK
# fO+9Ar8wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCC
# AkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo1OTFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO
# AwIaAxUA2RysX196RXLTwA/P8RFWdUTpUsaggYMwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO5Xm5QwIhgPMjAyNjA5
# MTgxMTE0MjhaGA8yMDI2MDkxOTExMTQyOFowdzA9BgorBgEEAYRZCgQBMS8wLTAK
# AgUA7leblAIBADAKAgEAAgIP9AIB/zAHAgEAAgITUTAKAgUA7ljtFAIBADA2Bgor
# BgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAID
# AYagMA0GCSqGSIb3DQEBCwUAA4IBAQA0z+sL6CmLDqZbs2+kgzs2oQzxNj3sbeFC
# zUGrXnOWirv1AMVFNs6ZOuemgRxbmvVGxCMTdVgXK9ltM+O1P3KQpNtMuCOauesw
# NTf3tscJlAOXF/FKHHD7/S8XUmEcSLRqb8V41gSVDf5vLLCXkzVsGOnnnPkMGRzC
# 2IVWrYWatUcYmHDa+2YqNr4uEGmy6fr4DtR0GT4DDs7LYnA3ksemU/Emi8NyqVa1
# QBzc/OCTVrkbYVylp9386AsTKc87BtedOWIHlsbYDM7lze1C6lQN9hJB5FAmhsYg
# YZvG/hB/TdbZjQQiWhhdk0Tai0VLQl6QKlaRgD3vEvCiLo6FyQtWMYIEDTCCBAkC
# AQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIUjc0jRO4G
# 33IAAQAAAhQwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG
# 9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgEuDGwFx/5tqgn4WDFACSbRgn0oRhVSYe
# eEXxPKgDaqYwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCA2eKvvWx5bcoi4
# 3bRO3+EttQUCvyeD2dbXy/6+0xK+xzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwAhMzAAACFI3NI0TuBt9yAAEAAAIUMCIEILrSIngGFJ1M7WoB
# HCFG3yhVHqDKBgKBvyKChLcbbtFBMA0GCSqGSIb3DQEBCwUABIICACYZaBqyzv0P
# YCr9EBVU2alhgVEYr48TZcLefvTRQgo/iQYJN1tGuDJCWfoz0+mj7g5+grCwtl91
# IkDEF07LF3zK7uQuN6nxqlJ4i8Hnqxm29eGJyHmhbkP+8gNbK8L55eDblWlHzE9I
# 57WakEWIhO6Zp2FRhOCuDkzE98I4M1xbyljBbZlyBQCLuU6bJ6eJ9IKDZmcFonqn
# fNy8iTmto6JNGa8nu2UTonNJQb/NxlXmmiAy6RxlI+S5oEOfjr/mPfhSopC4iRWL
# LGQ2w3aWZdvddpBkHSztgbLUoMZ4maZ2/jA0D3gxDhxHilaQ+dAo2tlWpa9HPli7
# 3xEfUr4FprGhgMH1bWfaf2YzpvARhNzFr+RGQpMfr0HPJ5OUvS00e93FcmEFb1X4
# Iqu3QCwIKmht586y6FJV+EQ4fo1ZM77JmyWqT9n53eP4++HdCEJucZIwYuOXunxq
# 3GxNxUb+ex4YI/Nv0mqyRuqfB+zt/CO0XPT9sWqC7w5o/VBYC7tEaDZeOeF5tbv6
# HKDnYm+AcWM0z5psnfyp/9xOKcEnPOlnd2qz58KxW4LFd3+1yz61QhH5Rn3cb40a
# X5FTIz7kg/RDJvFuSfx+XYhPLkPqWq4X2ZRCmgMtQrg34nT/ccGt63VI6Y96J/gZ
# FRImr3c11iVmrK1Bl5Oj5JaXRRJQ+Hsq
# SIG # End signature block