EPM/Collections.ps1

class EpmCollectionInfo {
    [string]$CollectionUid
    [string]$CollectionType
    [string]$Name
}

function script:Resolve-KeeperEpmCollection {
    <#
    .Synopsis
        Resolve collection(s) by UID or name (case-insensitive). Returns matching collection(s) as an array.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string] $Identifier,
        [Parameter(Mandatory = $true)]
        [object] $Plugin
    )

    $id = $Identifier.Trim()
    if ([string]::IsNullOrEmpty($id)) { return @() }

    $collection = $Plugin.Collections.GetEntity($id)
    if ($null -ne $collection) { return @($collection) }

    $nameMatches = [System.Collections.Generic.List[object]]::new()
    foreach ($c in @($Plugin.Collections.GetAll() | Where-Object { $null -ne $_ })) {
        if (-not $c.CollectionData -or $c.CollectionData.Length -eq 0) { continue }
        try {
            $jsonStr = [System.Text.Encoding]::UTF8.GetString($c.CollectionData)
            $data = $jsonStr | ConvertFrom-Json
            if ($data.PSObject.Properties['Name'] -and $data.Name -and $data.Name.Equals($id, [System.StringComparison]::OrdinalIgnoreCase)) {
                $nameMatches.Add($c)
            }
        } catch {
            Write-Debug "Failed to parse CollectionData for $($c.CollectionUid): $($_.Exception.Message)"
        }
    }
    return @($nameMatches)
}

function script:Resolve-KeeperEpmSingleCollection {
    <#
    .Synopsis
        Resolve a single collection by UID or name. Errors if not found or not unique.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string] $Identifier,
        [Parameter(Mandatory = $true)][object] $Plugin
    )
    [array]$collections = @(Resolve-KeeperEpmCollection -Identifier $Identifier -Plugin $Plugin | Where-Object { $null -ne $_ })
    if ($collections.Count -eq 0) {
        Write-Error -Message "Collection '$Identifier' not found." -ErrorAction Stop
    }
    if ($collections.Count -gt 1) {
        Write-Warning "Multiple collections found with name `"$Identifier`":"
        foreach ($c in $collections) {
            $name = Get-KeeperEpmCollectionName -Collection $c
            Write-Warning " UID: $($c.CollectionUid) Name: $name"
        }
        Write-Error -Message "Collection name `"$Identifier`" is not unique. Use Collection UID." -ErrorAction Stop
    }
    return $collections[0]
}

function script:Get-KeeperEpmCollectionName {
    Param ([object] $Collection)
    if (-not $Collection.CollectionData -or $Collection.CollectionData.Length -eq 0) { return '' }
    try {
        $jsonStr = [System.Text.Encoding]::UTF8.GetString($Collection.CollectionData)
        $data = $jsonStr | ConvertFrom-Json
        if ($data.PSObject.Properties['Name'] -and $data.Name) { return [string]$data.Name }
    } catch {
        Write-Debug "Failed to parse CollectionData: $($_.Exception.Message)"
    }
    return ''
}

function script:ConvertFrom-KeeperEpmLinkType {
    Param ([string] $LinkType)
    $v = $LinkType.Trim().ToLowerInvariant()
    switch ($v) {
        'agent'      { return [int][PEDM.CollectionLinkType]::CltAgent }
        'policy'     { return [int][PEDM.CollectionLinkType]::CltPolicy }
        'collection' { return [int][PEDM.CollectionLinkType]::CltCollection }
        default      { return $null }
    }
}

function Get-KeeperEpmCollectionList {
    <#
    .Synopsis
        List all EPM collections.
    .Parameter CollectionType
        Optional collection type number to filter results (1=OS Build, 2=Application, 3=User Account, 4=Group Account, 202=OS Version).
    #>

    [CmdletBinding()]
    Param (
        [Parameter()]
        [int] $CollectionType = -1
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $hasTypeFilter = $PSBoundParameters.ContainsKey('CollectionType')
    if ($hasTypeFilter) {
        [array]$collections = @($plugin.Collections.GetAll() | Where-Object { $null -ne $_ -and $_.CollectionType -eq $CollectionType } | Sort-Object -Property CollectionType, CollectionUid)
    } else {
        [array]$collections = @($plugin.Collections.GetAll() | Where-Object { $null -ne $_ } | Sort-Object -Property CollectionType, CollectionUid)
    }

    if ($collections.Count -eq 0) {
        Write-Output "No collections found."
        return
    }

    $rows = [System.Collections.Generic.List[EpmCollectionInfo]]::new()
    foreach ($coll in $collections) {
        $typeName = getEpmCollectionTypeName -CollectionType $coll.CollectionType
        $name = Get-KeeperEpmCollectionName -Collection $coll
        $row = [EpmCollectionInfo]::new()
        $row.CollectionUid  = $coll.CollectionUid
        $row.CollectionType = $typeName
        $row.Name           = $name
        $rows.Add($row)
    }
    $rows | Format-Table -AutoSize
}

function Get-KeeperEpmCollection {
    <#
    .Synopsis
        View a single EPM collection by UID or name.
    .Parameter CollectionUidOrName
        Collection UID or name (case-insensitive).
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $CollectionUidOrName
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $collection = Resolve-KeeperEpmSingleCollection -Identifier $CollectionUidOrName -Plugin $plugin

    $created = [DateTimeOffset]::FromUnixTimeMilliseconds($collection.Created).ToString("yyyy-MM-dd HH:mm:ss")
    Write-Output "Collection: $($collection.CollectionUid)"
    Write-Output " Type: $(getEpmCollectionTypeName -CollectionType $collection.CollectionType)"
    Write-Output " Created: $created"

    if ($collection.CollectionData -and $collection.CollectionData.Length -gt 0) {
        try {
            $jsonStr = [System.Text.Encoding]::UTF8.GetString($collection.CollectionData)
            $data = $jsonStr | ConvertFrom-Json
            if ($data.PSObject.Properties['Name'] -and $data.Name) {
                Write-Output " Name: $($data.Name)"
            }
            foreach ($prop in $data.PSObject.Properties) {
                if ($prop.Name -eq 'Name') { continue }
                $val = $prop.Value
                if ($val -is [System.Management.Automation.PSCustomObject]) {
                    Write-Output " $($prop.Name):"
                    foreach ($inner in $val.PSObject.Properties) {
                        Write-Output " $($inner.Name): $($inner.Value)"
                    }
                } elseif ($val -is [System.Collections.IEnumerable] -and $val -isnot [string]) {
                    Write-Output " $($prop.Name): [$($val -join ', ')]"
                } else {
                    Write-Output " $($prop.Name): $val"
                }
            }
        } catch {
            try {
                $dataJson = [System.Text.Encoding]::UTF8.GetString($collection.CollectionData)
                Write-Output " Data: $dataJson"
            } catch {
                Write-Output " Data: (binary data, $($collection.CollectionData.Length) bytes)"
            }
        }
    }

    [array]$links = @($plugin.CollectionLinks.GetLinksForSubject($collection.CollectionUid) | Where-Object { $null -ne $_ })
    $agentLinkType = [int][PEDM.CollectionLinkType]::CltAgent
    [array]$agentLinks = @($links | Where-Object { $_.LinkType -eq $agentLinkType })

    if ($agentLinks.Count -gt 0) {
        Write-Output ""
        Write-Output " Linked Agents ($($agentLinks.Count)):"
        foreach ($link in $agentLinks) {
            $agent = $plugin.Agents.GetEntity($link.LinkUid)
            if ($agent) {
                $machineName = if ($agent.MachineId) { $agent.MachineId } else { '(unknown)' }
                Write-Output " $($link.LinkUid) ($machineName)"
            } else {
                Write-Output " $($link.LinkUid)"
            }
        }
    } else {
        Write-Output ""
        Write-Output " Linked Agents: (none)"
    }

    $policyLinkType = [int][PEDM.CollectionLinkType]::CltPolicy
    [array]$policyLinks = @($links | Where-Object { $_.LinkType -eq $policyLinkType })
    if ($policyLinks.Count -gt 0) {
        Write-Output ""
        Write-Output " Linked Policies ($($policyLinks.Count)):"
        foreach ($link in $policyLinks) {
            Write-Output " $($link.LinkUid)"
        }
    }

    $collLinkType = [int][PEDM.CollectionLinkType]::CltCollection
    [array]$collLinks = @($links | Where-Object { $_.LinkType -eq $collLinkType })
    if ($collLinks.Count -gt 0) {
        Write-Output ""
        Write-Output " Linked Collections ($($collLinks.Count)):"
        foreach ($link in $collLinks) {
            $linkedColl = $plugin.Collections.GetEntity($link.LinkUid)
            if ($linkedColl) {
                $linkedName = Get-KeeperEpmCollectionName -Collection $linkedColl
                if ($linkedName) {
                    Write-Output " $($link.LinkUid) ($linkedName)"
                } else {
                    Write-Output " $($link.LinkUid)"
                }
            } else {
                Write-Output " $($link.LinkUid)"
            }
        }
    }
}

function Add-KeeperEpmCollection {
    <#
    .Synopsis
        Add a new EPM collection.
    .Parameter CollectionUid
        Optional collection UID. If omitted, one is generated automatically.
    .Parameter CollectionType
        Collection type (required). 1=OS Build, 2=Application, 3=User Account, 4=Group Account, 202=OS Version.
    .Parameter Data
        Collection data as a JSON string.
    .Parameter DataFile
        Path to a file containing collection data JSON.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]
        [string] $CollectionUid,
        [Parameter(Mandatory = $true)]
        [int] $CollectionType,
        [Parameter()]
        [string] $Data,
        [Parameter()]
        [string] $DataFile
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $collUid = if ($CollectionUid) { $CollectionUid.Trim() }
    if ([string]::IsNullOrEmpty($collUid)) {
        $collUid = [KeeperSecurity.Utils.CryptoUtils]::GenerateUid()
        Write-Output "Generated Collection UID: $collUid"
    }

    if ($CollectionType -eq 0) {
        Write-Error -Message "Collection type is required for 'add' command. Use -CollectionType (e.g., 2 for Application)." -ErrorAction Stop
    }

    $dataJson = readEpmJsonText -Json $Data -FilePath $DataFile
    if ([string]::IsNullOrEmpty($dataJson)) { $dataJson = '{}' }

    $collectionData = New-Object KeeperSecurity.Plugins.EPM.CollectionData
    $collectionData.CollectionUid = $collUid
    $collectionData.CollectionType = $CollectionType
    $collectionData.CollectionDataJson = $dataJson

    try {
        $addStatus = $plugin.ModifyCollections(
            [KeeperSecurity.Plugins.EPM.CollectionData[]]@($collectionData),
            $null,
            $null
        ).GetAwaiter().GetResult()

        if ($addStatus.AddErrors -and $addStatus.AddErrors.Count -gt 0) {
            $err = $addStatus.AddErrors[0]
            Write-Error -Message "Failed to add collection `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
        }
        if ($addStatus.Add -and $addStatus.Add.Count -gt 0) {
            Write-Output "Collection '$collUid' added."
        } else {
            Write-Warning "No collection was added. Check server response."
        }
        writeEpmModifyStatus -Status $addStatus
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error adding collection: $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Update-KeeperEpmCollection {
    <#
    .Synopsis
        Update an existing EPM collection.
    .Parameter CollectionUidOrName
        Collection UID or name (case-insensitive).
    .Parameter CollectionType
        New collection type (optional). If omitted, keeps existing type.
    .Parameter Data
        Collection data as a JSON string.
    .Parameter DataFile
        Path to a file containing collection data JSON.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $CollectionUidOrName,
        [Parameter()]
        [int] $CollectionType = -1,
        [Parameter()]
        [string] $Data,
        [Parameter()]
        [string] $DataFile
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $collection = Resolve-KeeperEpmSingleCollection -Identifier $CollectionUidOrName -Plugin $plugin

    $dataJson = readEpmJsonText -Json $Data -FilePath $DataFile
    if ([string]::IsNullOrEmpty($dataJson)) {
        if ($collection.CollectionData -and $collection.CollectionData.Length -gt 0) {
            $dataJson = [System.Text.Encoding]::UTF8.GetString($collection.CollectionData)
        } else {
            $dataJson = '{}'
        }
    }

    $typeValue = if ($PSBoundParameters.ContainsKey('CollectionType') -and $CollectionType -ne 0) { $CollectionType } else { $collection.CollectionType }

    $collectionData = New-Object KeeperSecurity.Plugins.EPM.CollectionData
    $collectionData.CollectionUid = $collection.CollectionUid
    $collectionData.CollectionType = $typeValue
    $collectionData.CollectionDataJson = $dataJson

    try {
        $updateStatus = $plugin.ModifyCollections(
            $null,
            [KeeperSecurity.Plugins.EPM.CollectionData[]]@($collectionData),
            $null
        ).GetAwaiter().GetResult()

        if ($updateStatus.UpdateErrors -and $updateStatus.UpdateErrors.Count -gt 0) {
            $err = $updateStatus.UpdateErrors[0]
            Write-Error -Message "Failed to update collection `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
        }
        if ($updateStatus.Update -and $updateStatus.Update.Count -gt 0) {
            Write-Output "Collection '$($collection.CollectionUid)' updated."
        } else {
            Write-Warning "No collection was updated. Check server response."
        }
        writeEpmModifyStatus -Status $updateStatus
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error updating collection: $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Remove-KeeperEpmCollection {
    <#
    .Synopsis
        Remove an EPM collection by UID or name.
    .Parameter CollectionUidOrName
        Collection UID or name (case-insensitive).
    .Parameter Force
        If set, skip confirmation prompt before delete.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $CollectionUidOrName,
        [Parameter()]
        [switch] $Force
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $collection = Resolve-KeeperEpmSingleCollection -Identifier $CollectionUidOrName -Plugin $plugin

    $collName = Get-KeeperEpmCollectionName -Collection $collection
    $displayName = if ($collName) { $collName } else { $collection.CollectionUid }
    if (-not $Force -and -not $PSCmdlet.ShouldProcess("collection '$displayName'", "Delete")) {
        return
    }

    try {
        $removeStatus = $plugin.ModifyCollections(
            $null,
            $null,
            [string[]]@($collection.CollectionUid)
        ).GetAwaiter().GetResult()

        if ($removeStatus.RemoveErrors -and $removeStatus.RemoveErrors.Count -gt 0) {
            $err = $removeStatus.RemoveErrors[0]
            Write-Error -Message "Failed to remove collection `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Stop
        }
        if ($removeStatus.Remove -and $removeStatus.Remove.Count -gt 0) {
            Write-Output "Collection '$($collection.CollectionUid)' removed."
        } else {
            Write-Warning "No collection was removed. Check server response."
        }
        writeEpmModifyStatus -Status $removeStatus
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error removing collection: $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Connect-KeeperEpmCollection {
    <#
    .Synopsis
        Link a collection to agent(s), policy(ies), or other collection(s).
    .Parameter CollectionUidOrName
        Collection UID or name (case-insensitive).
    .Parameter LinkType
        Type of link: agent, policy, or collection.
    .Parameter LinkUid
        One or more UIDs or names to link.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $CollectionUidOrName,
        [Parameter(Mandatory = $true)]
        [ValidateSet('agent', 'policy', 'collection')]
        [string] $LinkType,
        [Parameter(Mandatory = $true)]
        [string[]] $LinkUid
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $collection = Resolve-KeeperEpmSingleCollection -Identifier $CollectionUidOrName -Plugin $plugin

    $linkTypeValue = ConvertFrom-KeeperEpmLinkType -LinkType $LinkType

    $links = [System.Collections.Generic.List[string]]::new()

    foreach ($uid in $LinkUid) {
        $trimmed = if ($uid) { $uid.Trim() }
        if ([string]::IsNullOrEmpty($trimmed)) { continue }

        switch ($linkTypeValue) {
            ([int][PEDM.CollectionLinkType]::CltAgent) {
                [array]$agentMatches = @(Resolve-KeeperEpmAgent -Identifier $trimmed -Plugin $plugin | Where-Object { $null -ne $_ })
                if ($agentMatches.Count -eq 0) {
                    Write-Warning "Agent '$trimmed' not found."
                    continue
                }
                if ($agentMatches.Count -gt 1) {
                    Write-Warning "Multiple agents match name '$trimmed'. Use Agent UID."
                    continue
                }
                $links.Add($agentMatches[0].AgentUid)
            }
            ([int][PEDM.CollectionLinkType]::CltPolicy) {
                [array]$policyMatches = @(Resolve-KeeperEpmPolicy -Identifier $trimmed -Plugin $plugin | Where-Object { $null -ne $_ })
                if ($policyMatches.Count -eq 0) {
                    Write-Warning "Policy '$trimmed' not found."
                    continue
                }
                if ($policyMatches.Count -gt 1) {
                    Write-Warning "Multiple policies match name '$trimmed'. Use Policy UID."
                    continue
                }
                $links.Add($policyMatches[0].PolicyUid)
            }
            ([int][PEDM.CollectionLinkType]::CltCollection) {
                [array]$collMatches = @(Resolve-KeeperEpmCollection -Identifier $trimmed -Plugin $plugin | Where-Object { $null -ne $_ })
                if ($collMatches.Count -eq 0) {
                    Write-Warning "Collection '$trimmed' not found."
                    continue
                }
                if ($collMatches.Count -gt 1) {
                    Write-Warning "Multiple collections match name '$trimmed'. Use Collection UID."
                    continue
                }
                $links.Add($collMatches[0].CollectionUid)
            }
        }
    }

    if ($links.Count -eq 0) {
        Write-Error -Message "No valid links found." -ErrorAction Stop
    }

    $setLinks = [System.Collections.Generic.List[KeeperSecurity.Plugins.EPM.CollectionLink]]::new()
    foreach ($linkUidValue in $links) {
        $cl = New-Object KeeperSecurity.Plugins.EPM.CollectionLink
        $cl.CollectionUid = $collection.CollectionUid
        $cl.LinkUid = $linkUidValue
        $cl.LinkType = $linkTypeValue
        $setLinks.Add($cl)
    }

    try {
        $status = $plugin.SetCollectionLinks($setLinks, $null).GetAwaiter().GetResult()

        $hasErrors = $false
        if ($status.AddErrors -and $status.AddErrors.Count -gt 0) {
            foreach ($err in $status.AddErrors) {
                Write-Error -Message "Failed to connect collection link `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Continue
                $hasErrors = $true
            }
        }
        if ($status.Add -and $status.Add.Count -gt 0) {
            Write-Output "$($status.Add.Count) link(s) connected."
        } elseif (-not $hasErrors) {
            Write-Warning "No links were connected. Check server response."
        }
        writeEpmModifyStatus -Status $status
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error connecting collection: $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Disconnect-KeeperEpmCollection {
    <#
    .Synopsis
        Unlink agent(s), policy(ies), or collection(s) from a collection.
    .Parameter CollectionUidOrName
        Collection UID or name (case-insensitive).
    .Parameter LinkUid
        One or more link UIDs to disconnect.
    .Parameter Force
        Skip confirmation prompt.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $CollectionUidOrName,
        [Parameter(Mandatory = $true)]
        [string[]] $LinkUid,
        [Parameter()]
        [switch] $Force
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    $collection = Resolve-KeeperEpmSingleCollection -Identifier $CollectionUidOrName -Plugin $plugin

    [array]$existingLinks = @($plugin.CollectionLinks.GetLinksForSubject($collection.CollectionUid) | Where-Object { $null -ne $_ })
    $toUnlink = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($u in $LinkUid) {
        $t = if ($u) { $u.Trim() }
        if (-not [string]::IsNullOrEmpty($t)) { $toUnlink.Add($t) | Out-Null }
    }

    $unsetLinks = [System.Collections.Generic.List[KeeperSecurity.Plugins.EPM.CollectionLink]]::new()
    foreach ($link in $existingLinks) {
        if ($toUnlink.Contains($link.LinkUid)) {
            $cl = New-Object KeeperSecurity.Plugins.EPM.CollectionLink
            $cl.CollectionUid = $collection.CollectionUid
            $cl.LinkUid = $link.LinkUid
            $cl.LinkType = $link.LinkType
            $unsetLinks.Add($cl)
            $toUnlink.Remove($link.LinkUid) | Out-Null
        }
    }

    if ($toUnlink.Count -gt 0) {
        Write-Warning "$($toUnlink.Count) link(s) cannot be removed from collection: $CollectionUidOrName"
    }

    if ($unsetLinks.Count -eq 0) { return }

    if (-not $Force -and -not $PSCmdlet.ShouldProcess("$($unsetLinks.Count) link(s) from collection '$CollectionUidOrName'", "Disconnect")) {
        return
    }

    try {
        $status = $plugin.SetCollectionLinks($null, $unsetLinks).GetAwaiter().GetResult()

        $hasErrors = $false
        if ($status.RemoveErrors -and $status.RemoveErrors.Count -gt 0) {
            foreach ($err in $status.RemoveErrors) {
                Write-Error -Message "Failed to disconnect collection link `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Continue
                $hasErrors = $true
            }
        }
        if ($status.Remove -and $status.Remove.Count -gt 0) {
            Write-Output "$($status.Remove.Count) link(s) disconnected."
        } elseif (-not $hasErrors) {
            Write-Warning "No links were disconnected. Check server response."
        }
        writeEpmModifyStatus -Status $status
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error disconnecting collection: $($_.Exception.Message)" -ErrorAction Stop
    }
}

function Remove-KeeperEpmCollectionsByType {
    <#
    .Synopsis
        Remove all EPM collections of a given type (wipe-out).
    .Parameter CollectionType
        Collection type to wipe (1=OS Build, 2=Application, 3=User Account, 4=Group Account, 202=OS Version).
    .Parameter Force
        Skip confirmation prompt.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [int] $CollectionType,
        [Parameter()]
        [switch] $Force
    )

    $plugin = ensureEpmPlugin
    if (-not $plugin) {
        Write-Error -Message "EPM plugin is not available. Enterprise admin access is required." -ErrorAction Stop
    }

    [array]$collectionUids = @($plugin.Collections.GetAll() | Where-Object { $null -ne $_ -and $_.CollectionType -eq $CollectionType } | ForEach-Object { $_.CollectionUid })

    if ($collectionUids.Count -eq 0) {
        $typeName = getEpmCollectionTypeName -CollectionType $CollectionType
        Write-Output "No collections found for type: $typeName ($CollectionType)"
        return
    }

    $typeName = getEpmCollectionTypeName -CollectionType $CollectionType
    if (-not $Force -and -not $PSCmdlet.ShouldProcess("$($collectionUids.Count) $typeName collection(s)", "Delete")) {
        return
    }

    try {
        $removeStatus = $plugin.ModifyCollections(
            $null,
            $null,
            [string[]]$collectionUids
        ).GetAwaiter().GetResult()

        $hasErrors = $false
        if ($removeStatus.RemoveErrors -and $removeStatus.RemoveErrors.Count -gt 0) {
            foreach ($err in $removeStatus.RemoveErrors) {
                if (-not $err.Success) {
                    Write-Error -Message "Failed to remove collection `"$($err.EntityUid)`": $($err.Message)" -ErrorAction Continue
                    $hasErrors = $true
                }
            }
        }
        if ($removeStatus.Remove -and $removeStatus.Remove.Count -gt 0) {
            Write-Output "$($removeStatus.Remove.Count) collection(s) removed."
        } elseif (-not $hasErrors) {
            Write-Warning "No collections were removed. Check server response."
        }
        writeEpmModifyStatus -Status $removeStatus
        $plugin.SyncDown($false).GetAwaiter().GetResult() | Out-Null
    } catch {
        Write-Error -Message "Error removing collections: $($_.Exception.Message)" -ErrorAction Stop
    }
}

New-Alias -Name kepm-collection-list       -Value Get-KeeperEpmCollectionList       -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-view       -Value Get-KeeperEpmCollection           -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-add        -Value Add-KeeperEpmCollection           -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-edit       -Value Update-KeeperEpmCollection        -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-delete     -Value Remove-KeeperEpmCollection        -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-connect    -Value Connect-KeeperEpmCollection       -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-disconnect -Value Disconnect-KeeperEpmCollection    -ErrorAction SilentlyContinue
New-Alias -Name kepm-collection-wipeout    -Value Remove-KeeperEpmCollectionsByType -ErrorAction SilentlyContinue

# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCoxZVCUH5fMS2Q
# drXUdA9qOuo9fqwbF1YRArAYY/W74aCCITswggWNMIIEdaADAgECAhAOmxiO+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
# twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg
# MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit
# eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS
# 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM
# swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC
# Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3
# /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j
# q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5
# OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo
# 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU
# tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm
# KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP
# TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/
# BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j
# BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0
# cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0
# cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E
# PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz
# dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq
# hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK
# r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda
# qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+
# lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a
# brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS
# y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK
# iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb
# KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q
# xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm
# zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn
# HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w
# gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1
# c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo
# dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi
# 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg
# xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF
# cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ
# m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS
# GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1
# ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9
# MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7
# Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG
# RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6
# X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd
# BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx
# XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo
# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy
# bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL
# BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj
# aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0
# hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0
# F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT
# mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf
# ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE
# wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh
# OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX
# gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO
# LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG
# WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg
# AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG
# EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0
# IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex
# MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx
# FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy
# NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI
# hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3
# zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch
# TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj
# FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo
# yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP
# KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS
# uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w
# JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW
# doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg
# rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K
# 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf
# gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy
# Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL
# TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG
# AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
# b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
# Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy
# dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j
# cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB
# CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ
# D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/
# ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu
# +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o
# bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h
# ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn
# M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol
# /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY
# xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc
# CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB
# ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx
# oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a
# vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ
# aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc
# vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC
# h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt
# 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk
# Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s
# FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm
# 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG
# 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6
# z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2
# IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL
# iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu
# FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc
# H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA
# 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB
# +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b
# WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A
# g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH
# 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCCKOy5s0QstUCtT13hT1N0s9Z1AwwC8eRvelS5/4LZOCjANBgkqhkiG9w0B
# AQEFAASCAYB4JMX5duU1tU/a0zp0RRUTJ0OOehnTemdoNFkcUZmTI5QrzKdEmY0/
# CFumeUJYz4zxSs7qZnA4rq4sFHX3s959EFaQWnFKhznuKccxB1xEh3pITGEkZx2t
# Kkpg9XJ0Z2RifkpgoXcnXS28jJ6djJbYw6862TjXdjkfQ9ZohfRWDGiizEkkH/Oj
# FSd1A7S2p2iHKUUhSoChzdA+qK6wV1z/RXvZ80tIp++Fb51y+YfvTTLZU4t4y7E5
# dHyT+5PAXgGPGVnBDbFgsg3lyHaGIbbzZvHLY2AyZdiCijxDrV9GS/H6FOGH+UN0
# h7MYsl0A4U38H+UPFdZhRojrtyt4pJN7FZkZkcoUd0nZt7J7q17y2rElokR4oYsP
# DBCNVHS6vRcm3uhI3OQcWVZCo7EflSAQyrlFDnCJY542esEMnuBWMTDUYCvGPcMO
# 0vzqdS+B1ZF9nSAsp+nwo6kA7xSN5vK3+YO1ZPwqKl9YUlvucgCI4TEn0TE6MPmc
# 2Sg3R2uPN8ihggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTI2MDQwNjQ2WjAv
# BgkqhkiG9w0BCQQxIgQg0Bsi1C//wZp9cPBcCeGntLSVnKbeJG826AtEGFbaI1Aw
# DQYJKoZIhvcNAQEBBQAEggIAr3/bmjeQ2XAUE3KExQ+H6YAGsEHZS3b3hMR2Azi5
# 8vJ8qW/qEu7ENtusVq5eBo6U6Gk04Ron9G8Qp+MWJu6WHd+WOoG07M0swmO65Vbt
# 21isoRkqbSbbW/NGNkjiMxY6O3B66GhT/CpDA4vgDqCo8WEIxHiMSHDgjfadXFof
# Q5RJiA46oZH8OfOXAW9mJlol5XnVXOBtxNsFbUfqbqCChb6rlxZ/7oZQSY/UTxV6
# 1TB8AQQrDTO5l+YBYD+UwWhUzX1NrAcY2YYVQ8Gs8E2RIDoCk0bMzUeKzbZUsQ/q
# LR4vCuW9umOjDwxwJAw+XDvlOcRrjB9hK5zzslERGQN/9NlzFxlAvFRzyrh4h9Ku
# GgXtcskvl7hJMbXD3pyrlhE6vrB1JqkUnJK25MiTrAEccL/BaZ4DBBT2plPNhvjH
# BA1yRg2liBKQL6ctN4Ei4DHmvEw5IQrbWBVnEpl6GZtTDf5GeeE/22awJci1ooJM
# XqkH+ypqmwaM9aaXSI6j2Ai5NtE6AFmnQjHmvd7cOTgyU/jr6MslNw4acO+ZJ+uO
# g2Gs4AA4c1gPWIUPVR6eqYCXAvNLCgcSBWXsZKUAsfUr/tf5TQVNDK5tJL/XiPLt
# jFkYYB8ZdUaab4IOrL9fHFQ16WsEGQftT++Qm8L6u+zmP578dgfOXCgs7E7ffNsh
# Zvk=
# SIG # End signature block