helpers/discovery/Setup-WindowsDiskIO-Discovery.ps1

<#
.SYNOPSIS
    Discovers physical disk instances on Windows devices and creates WMI Formatted
    performance monitors in WhatsUp Gold for disk IO metrics.
 
.DESCRIPTION
    This helper connects to Windows servers via WMI (Get-WmiObject) to enumerate
    physical disk instances from Win32_PerfFormattedData_PerfDisk_PhysicalDisk.
    For each unique disk instance found, it creates WmiFormatted performance monitors
    in the WhatsUp Gold library (if they do not already exist) and assigns them to
    the applicable devices.
 
    Physical disk instances in WMI can vary per machine (e.g., "0 C:", "1 D:",
    "2 D:" on different servers). This script scans each device to discover the
    actual instances present, creates one library monitor per unique
    class+property+instance combination, and reuses that monitor across all devices
    that share the same instance.
 
    Monitors created per disk instance:
      - Disk Reads/sec
      - Disk Writes/sec
      - Disk Transfers/sec (Total IOPS)
      - Disk Read Bytes/sec
      - Disk Write Bytes/sec
      - Disk Bytes/sec (Total Throughput)
      - Avg. Disk Queue Length
      - Pct Disk Time
 
    Prerequisites:
      1. WhatsUpGoldPS module loaded and connected (Connect-WUGServer)
      2. WMI/DCOM or WinRM access to target Windows servers
      3. Credentials with admin access on target servers
 
.PARAMETER DeviceId
    One or more WUG device IDs to scan. When omitted, devices are resolved
    automatically from a Windows device group (see DeviceGroupSearch).
 
.PARAMETER Credential
    One or more PSCredentials for WMI access to the target Windows servers.
    Multiple credentials are tried in order per device -- if the first fails
    with an access-denied error the next is attempted. If omitted, credentials
    are resolved from the DPAPI discovery vault. The vault prompts for one
    credential on first run, and you can add more via the interactive prompt.
 
.PARAMETER DeviceGroupSearch
    Search string used to find a WUG device group containing Windows devices.
    The script searches for a group matching this name, then pulls only those
    devices. Default: 'Windows Infrastructure'. Set to '' to skip group-based filtering.
 
.PARAMETER DeviceGroupId
    Explicit WUG device group ID. Overrides DeviceGroupSearch. Default: unset.
 
.PARAMETER WindowsOnly
    When enabled (default), only devices whose role contains 'Windows' are
    scanned. This prevents wasting time trying WMI against Linux boxes,
    switches, etc. Set -WindowsOnly:$false to scan all devices in the group.
 
.PARAMETER ExcludeInstance
    Array of instance name patterns to skip (e.g., '_Total'). Default: @('_Total').
    Use @() to include _Total.
 
.PARAMETER DiskType
    Which WMI disk class to query. Default: 'Physical'.
      Physical -- Win32_PerfFormattedData_PerfDisk_PhysicalDisk (raw hardware disks: 0, 1, 2...)
      Logical -- Win32_PerfFormattedData_PerfDisk_LogicalDisk (drive letters: C:, D:, E:...)
    Physical disks show hardware-level IO; Logical disks show per-volume IO.
 
.PARAMETER IncludeCounter
    Array of counter names to create monitors for. Default: DiskTransfersPersec
    (total IOPS). Use 'All' to include all 8 counters.
    Valid values: All, DiskReadsPersec, DiskWritesPersec, DiskTransfersPersec,
    DiskReadBytesPersec, DiskWriteBytesPersec, DiskBytesPersec,
    AvgDiskQueueLength, PercentDiskTime.
 
.PARAMETER PollingIntervalMinutes
    Polling interval for assigned monitors. Default: 5.
 
.PARAMETER WmiTimeout
    WMI query timeout in seconds. Default: 10.
 
.PARAMETER NamePrefix
    Prefix for monitor display names. Default: 'PhysDisk'.
 
.PARAMETER Action
    What to do with discovery results. When specified, skips the interactive menu.
    Valid values: PushToWUG, ExportJSON, ExportCSV, ShowTable, Dashboard, DashboardAndPush, None.
    PushToWUG requires devices resolved from WhatsUp Gold (not standalone -Target mode).
 
.PARAMETER Target
    Windows host(s) to scan via WMI -- IP address or FQDN. Accepts multiple values.
    Enables standalone mode (no WhatsUp Gold connection needed for device resolution).
    PushToWUG action is not available in standalone -Target mode.
 
.PARAMETER DryRun
    Show what would be created/assigned without making changes.
 
.PARAMETER WUGServer
    WhatsUp Gold server address. Default: resolved from vault.
 
.PARAMETER WUGCredential
    PSCredential for authenticating to WhatsUp Gold. When supplied, bypasses
    the vault-based WUG server resolution and connects directly.
 
.PARAMETER NonInteractive
    Suppress interactive prompts. Uses cached vault credentials.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1
 
    Interactive mode -- resolves creds from vault (prompts on first run),
    scans all WUG devices, discovers disks, creates monitors.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1 -Credential (Get-Credential)
 
    Interactive mode with explicit credential -- skips WMI vault lookup.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1 -DeviceId @(100,101,102) -NonInteractive
 
    Scans specific devices non-interactively using vault credentials.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1 -DeviceId 42 -Credential $cred -DryRun
 
    Shows what monitors would be created for device 42 without making changes.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1 -DeviceId 42 -Credential $cred -IncludeCounter @('DiskReadsPersec','DiskWritesPersec')
 
    Only creates Disk Reads/sec and Disk Writes/sec monitors.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1 -Target '10.0.0.5','10.0.0.6' -Action Dashboard
 
    Standalone mode -- scans hosts directly and generates an HTML dashboard.
 
.EXAMPLE
    .\Setup-WindowsDiskIO-Discovery.ps1 -Target '10.0.0.5' -Action ExportJSON -NonInteractive
 
    Scans a host and exports disk inventory to JSON. No WUG connection required.
 
.NOTES
    Author: Jason Alberino (jason@wug.ninja)
    Requires: WhatsUpGoldPS module, PowerShell 5.1+
    Encoding: UTF-8 with BOM
#>

[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [int[]]$DeviceId,

    [PSCredential[]]$Credential,

    [string]$DeviceGroupSearch = 'Windows Infrastructure',

    [string]$DeviceGroupId,

    [bool]$WindowsOnly = $true,

    [string[]]$ExcludeInstance = @('_Total'),

    [ValidateSet('All', 'Physical', 'Logical')]
    [string]$DiskType = 'Logical',

    [ValidateSet('All', 'DiskReadsPersec', 'DiskWritesPersec', 'DiskTransfersPersec',
                 'DiskReadBytesPersec', 'DiskWriteBytesPersec', 'DiskBytesPersec',
                 'AvgDiskQueueLength', 'PercentDiskTime')]
    [string[]]$IncludeCounter,

    [ValidateRange(1, 1440)]
    [int]$PollingIntervalMinutes = 10,

    [int]$WmiTimeout = 10,

    [string]$NamePrefix = 'PhysDisk',

    [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')]
    [string]$Action,

    [string[]]$Target,

    [string]$WUGServer,

    [PSCredential]$WUGCredential,

    [switch]$DryRun,

    [switch]$NonInteractive,

    [string]$OutputPath
)

# --- Handle 'All' by running twice with each type ----------------------------
if ($DiskType -eq 'All') {
    $scriptPath = $MyInvocation.MyCommand.Path
    # Load helpers so we can resolve credentials once
    $scriptDir = Split-Path $scriptPath -Parent
    . (Join-Path $scriptDir 'DiscoveryHelpers.ps1')

    # Resolve credentials once here, then pass via -Credential to both passes
    if (-not $PSBoundParameters.ContainsKey('Credential') -or -not $Credential) {
        $credList = @()
        $credIndex = 1
        $wmiVaultName = "Windows.WMI.Credential.$credIndex"
        $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; ProviderLabel = "Windows WMI #$credIndex" }
        if ($NonInteractive) { $credSplat.NonInteractive = $true }
        $firstCred = Resolve-DiscoveryCredential @credSplat
        if (-not $firstCred) {
            Write-Error "No WMI credentials available. Provide -Credential or configure the vault."
            return
        }
        $credList += $firstCred
        if (-not $NonInteractive) {
            while ($true) {
                Write-Host ""
                Write-Host " Credential #$($credList.Count) loaded: $($credList[-1].UserName)" -ForegroundColor Gray
                Write-Host " Add another credential to try if this one fails on some servers?" -ForegroundColor Cyan
                $addMore = Read-Host -Prompt " [Y]es / [N]o (default: N)"
                if ($addMore -notmatch '^[Yy]') { break }
                $credIndex++
                $wmiVaultName = "Windows.WMI.Credential.$credIndex"
                $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; ProviderLabel = "Windows WMI #$credIndex" }
                $nextCred = Resolve-DiscoveryCredential @credSplat
                if ($nextCred) { $credList += $nextCred } else { break }
            }
        }
        $Credential = $credList
    }

    # Build a splat of all bound params except DiskType
    $passThru = @{}
    foreach ($key in $PSBoundParameters.Keys) {
        if ($key -ne 'DiskType') { $passThru[$key] = $PSBoundParameters[$key] }
    }
    # Ensure resolved credentials are passed through
    $passThru['Credential'] = $Credential

    Write-Host ""
    Write-Host "DiskType=All: Running Logical pass first, then Physical." -ForegroundColor Cyan
    Write-Host ""
    & $scriptPath -DiskType Logical @passThru
    Write-Host ""
    & $scriptPath -DiskType Physical @passThru
    return
}

# --- Load discovery helpers (vault, credential resolver) ----------------------
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$discoveryHelpersPath = Join-Path $scriptDir 'DiscoveryHelpers.ps1'
if (Test-Path $discoveryHelpersPath) {
    . $discoveryHelpersPath
}
else {
    Write-Error "DiscoveryHelpers.ps1 not found at '$discoveryHelpersPath'. Cannot continue."
    return
}

# Load dynamic dashboard generator (fallback)
$dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1'
if (Test-Path $dynDashPath) { . $dynDashPath }

# Load provider-specific Windows dashboard functions
$winProviderPath = Join-Path $scriptDir 'DiscoveryProvider-Windows.ps1'
if (Test-Path $winProviderPath) { . $winProviderPath }

# =============================================================================
# Configuration: Counter definitions
# =============================================================================
# Each entry maps a friendly key to the WMI property name and a human-readable
# display label used in the WUG monitor name.
$script:CounterDefinitions = [ordered]@{
    DiskReadsPersec       = @{ Property = 'DiskReadsPersec';       Label = 'Disk Reads/sec' }
    DiskWritesPersec      = @{ Property = 'DiskWritesPersec';      Label = 'Disk Writes/sec' }
    DiskTransfersPersec   = @{ Property = 'DiskTransfersPersec';   Label = 'Disk Transfers/sec (IOPS)' }
    DiskReadBytesPersec   = @{ Property = 'DiskReadBytesPersec';   Label = 'Disk Read Bytes/sec' }
    DiskWriteBytesPersec  = @{ Property = 'DiskWriteBytesPersec';  Label = 'Disk Write Bytes/sec' }
    DiskBytesPersec       = @{ Property = 'DiskBytesPersec';       Label = 'Disk Bytes/sec (Total)' }
    AvgDiskQueueLength    = @{ Property = 'AvgDiskQueueLength';    Label = 'Avg. Disk Queue Length' }
    PercentDiskTime       = @{ Property = 'PercentDiskTime';       Label = '% Disk Time' }
}

# Default counters (IOPS only) when -IncludeCounter is not specified
$script:DefaultCounters = @('DiskTransfersPersec')

# Resolve WMI class and name prefix based on DiskType
if ($DiskType -eq 'Logical') {
    $WmiClass = 'Win32_PerfFormattedData_PerfDisk_LogicalDisk'
    if (-not $PSBoundParameters.ContainsKey('NamePrefix')) { $NamePrefix = 'LogDisk' }
}
else {
    $WmiClass = 'Win32_PerfFormattedData_PerfDisk_PhysicalDisk'
    if (-not $PSBoundParameters.ContainsKey('NamePrefix')) { $NamePrefix = 'PhysDisk' }
}

# Filter to requested counters only
if ($IncludeCounter -and $IncludeCounter -contains 'All') {
    # All counters
    $activeCounters = $script:CounterDefinitions
}
elseif ($IncludeCounter) {
    $activeCounters = [ordered]@{}
    foreach ($key in $IncludeCounter) {
        if ($script:CounterDefinitions.Contains($key)) {
            $activeCounters[$key] = $script:CounterDefinitions[$key]
        }
    }
}
else {
    # Default: IOPS only
    $activeCounters = [ordered]@{}
    foreach ($key in $script:DefaultCounters) {
        $activeCounters[$key] = $script:CounterDefinitions[$key]
    }
}

if ($activeCounters.Count -eq 0) {
    Write-Error "No valid counters selected. Exiting."
    return
}

# =============================================================================
# Preflight: WUG connection (only needed when resolving devices from WUG or pushing)
# =============================================================================
$needsWUG = (-not $Target) -or ($Action -eq 'PushToWUG') -or ($Action -eq 'DashboardAndPush')
if ($needsWUG) {
if (-not $global:WUGBearerHeaders -or -not $global:WhatsUpServerBaseURI) {
    if ($WUGCredential) {
        # Direct credential supplied -- bypass vault resolution
        $wugUri = if ($WUGServer) { $WUGServer } else { 'https://localhost:9644' }
        try {
            Connect-WUGServer -serverUri $wugUri -Credential $WUGCredential -IgnoreSSLErrors
            Write-Host "Connected to WhatsUp Gold." -ForegroundColor Green
        }
        catch {
            Write-Error "Failed to connect to WhatsUp Gold: $_"
            return
        }
    }
    else {
    Write-Host "Not connected to WhatsUp Gold. Resolving from vault..." -ForegroundColor Yellow
    $wugSplat = @{ Name = 'WUG.Server'; CredType = 'WUGServer'; ProviderLabel = 'WhatsUp Gold' }
    if ($NonInteractive) { $wugSplat.NonInteractive = $true }
    $wugResolved = Resolve-DiscoveryCredential @wugSplat
    if (-not $wugResolved) {
        Write-Error "No WhatsUp Gold server credentials available. Run Connect-WUGServer or configure the vault."
        return
    }
    # Connect using resolved vault credentials
    $wugConnSplat = @{}
    if ($wugResolved -is [hashtable]) {
        $wugConnSplat.Server = $wugResolved.Server
        if ($wugResolved.Port)     { $wugConnSplat.Port = $wugResolved.Port }
        if ($wugResolved.Protocol) { $wugConnSplat.Protocol = $wugResolved.Protocol }
        if ($wugResolved.Credential) { $wugConnSplat.Credential = $wugResolved.Credential }
        if ($wugResolved.IgnoreSSL) { $wugConnSplat.IgnoreSSL = $true }
    }
    elseif ($WUGServer) {
        $wugConnSplat.Server = $WUGServer
    }
    try {
        Connect-WUGServer @wugConnSplat
        Write-Host "Connected to WhatsUp Gold." -ForegroundColor Green
    }
    catch {
        Write-Error "Failed to connect to WhatsUp Gold: $_"
        return
    }
    }
}
} # end needsWUG

# =============================================================================
# Resolve WMI credentials (vault-backed, multi-credential support)
# =============================================================================
# Build an ordered list of credentials to try per device.
$credentialList = [System.Collections.Generic.List[PSCredential]]::new()

if ($Credential) {
    # Explicit parameter -- use as-is
    foreach ($c in $Credential) { $credentialList.Add($c) }
}
else {
    # Resolve first credential from vault
    $credIndex = 1
    $wmiVaultName = "Windows.WMI.Credential.$credIndex"
    $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; ProviderLabel = "Windows WMI #$credIndex" }
    if ($NonInteractive) { $credSplat.NonInteractive = $true }
    $firstCred = Resolve-DiscoveryCredential @credSplat
    if (-not $firstCred) {
        Write-Error "No WMI credentials available. Provide -Credential or configure the vault."
        return
    }
    $credentialList.Add($firstCred)

    # Offer to add more credentials (interactive only)
    if (-not $NonInteractive) {
        while ($true) {
            Write-Host ""
            Write-Host " Credential #$($credentialList.Count) loaded: $($credentialList[-1].UserName)" -ForegroundColor Gray
            Write-Host " Add another credential to try if this one fails on some servers?" -ForegroundColor Cyan
            $addMore = Read-Host -Prompt " [Y]es / [N]o (default: N)"
            if ($addMore -notmatch '^[Yy]') { break }

            $credIndex++
            $wmiVaultName = "Windows.WMI.Credential.$credIndex"
            $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; ProviderLabel = "Windows WMI #$credIndex" }
            $nextCred = Resolve-DiscoveryCredential @credSplat
            if ($nextCred) {
                $credentialList.Add($nextCred)
            }
            else {
                Write-Host " Skipped." -ForegroundColor Yellow
                break
            }
        }
    }
    else {
        # Non-interactive: load any additional vault entries that already exist
        while ($true) {
            $credIndex++
            $wmiVaultName = "Windows.WMI.Credential.$credIndex"
            $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; NonInteractive = $true }
            $nextCred = Resolve-DiscoveryCredential @credSplat
            if ($nextCred) {
                $credentialList.Add($nextCred)
            }
            else {
                break
            }
        }
    }
}

Write-Host ""
Write-Host " Credentials to try ($($credentialList.Count)):" -ForegroundColor Cyan
for ($ci = 0; $ci -lt $credentialList.Count; $ci++) {
    Write-Host " [$($ci + 1)] $($credentialList[$ci].UserName)" -ForegroundColor Gray
}

# =============================================================================
# STEP 1: Resolve target devices (smart Windows group detection)
# =============================================================================
Write-Host ""
Write-Host "=== Windows Physical Disk IO Discovery ===" -ForegroundColor Cyan
Write-Host ""

$standaloneMode = $false
if ($Target) {
    # Standalone mode: scan specified hosts directly (no WUG device lookup)
    $standaloneMode = $true
    Write-Host "Standalone mode: scanning $($Target.Count) specified host(s)..." -ForegroundColor Cyan
    $devices = @()
    $targetId = 0
    foreach ($t in $Target) {
        $targetId++
        $devices += [PSCustomObject]@{
            id             = "target-$targetId"
            displayName    = $t
            hostName       = $t
            networkAddress = $t
            role           = 'Windows'
        }
    }
}
elseif ($DeviceId) {
    Write-Host "Fetching $($DeviceId.Count) specified device(s) from WhatsUp Gold..." -ForegroundColor Cyan
    $devices = @()
    foreach ($dId in $DeviceId) {
        try {
            $dev = Get-WUGDevice -DeviceId $dId
            if ($dev) { $devices += $dev }
        }
        catch {
            Write-Warning "Could not retrieve device ID ${dId}: $_"
        }
    }
}
else {
    # --- Auto-resolve device group ------------------------------------------
    $resolvedGroupId = $null

    if ($DeviceGroupId) {
        # Explicit group ID provided
        $resolvedGroupId = $DeviceGroupId
        Write-Host "Using explicit device group ID: $resolvedGroupId" -ForegroundColor Cyan
    }
    elseif ($DeviceGroupSearch) {
        Write-Host "Searching for device group matching '$DeviceGroupSearch'..." -ForegroundColor Cyan
        try {
            $matchingGroups = @(Get-WUGDeviceGroup -SearchValue $DeviceGroupSearch)
            if ($matchingGroups.Count -eq 1) {
                $resolvedGroupId = $matchingGroups[0].id
                Write-Host " Found group: '$($matchingGroups[0].name)' (ID: $resolvedGroupId)" -ForegroundColor Green
            }
            elseif ($matchingGroups.Count -gt 1) {
                Write-Host " Found $($matchingGroups.Count) matching groups:" -ForegroundColor Cyan
                for ($gi = 0; $gi -lt $matchingGroups.Count; $gi++) {
                    $g = $matchingGroups[$gi]
                    $devCount = if ($g.deviceCount) { $g.deviceCount } else { '?' }
                    Write-Host " [$($gi + 1)] $($g.name) (ID: $($g.id), Devices: $devCount)" -ForegroundColor White
                }
                if (-not $NonInteractive) {
                    Write-Host ""
                    $groupChoice = Read-Host -Prompt " Select group number [default: 1]"
                    if (-not $groupChoice) { $groupChoice = '1' }
                    $groupIdx = [int]$groupChoice - 1
                    if ($groupIdx -ge 0 -and $groupIdx -lt $matchingGroups.Count) {
                        $resolvedGroupId = $matchingGroups[$groupIdx].id
                        Write-Host " Selected: '$($matchingGroups[$groupIdx].name)' (ID: $resolvedGroupId)" -ForegroundColor Green
                    }
                    else {
                        Write-Warning "Invalid selection. Using first match."
                        $resolvedGroupId = $matchingGroups[0].id
                    }
                }
                else {
                    # Non-interactive: pick the first match
                    $resolvedGroupId = $matchingGroups[0].id
                    Write-Host " Auto-selected: '$($matchingGroups[0].name)' (ID: $resolvedGroupId)" -ForegroundColor Gray
                }
            }
            else {
                Write-Host " No groups matching '$DeviceGroupSearch'. Falling back to all devices." -ForegroundColor Yellow
            }
        }
        catch {
            Write-Warning "Failed to search device groups: $_. Falling back to all devices."
        }
    }

    if (-not $resolvedGroupId) { $resolvedGroupId = '-1' }
    Write-Host "Fetching devices from group $resolvedGroupId..." -ForegroundColor Cyan
    $devices = @(Get-WUGDevice -DeviceGroupID $resolvedGroupId)

    # --- Filter to Windows-only by role --------------------------------------
    if ($WindowsOnly -and $devices.Count -gt 0) {
        $preCount = $devices.Count
        $devices = @($devices | Where-Object {
            ($_.role -and $_.role -match 'Windows') -or
            ($_.description -and $_.description -match 'Windows')
        })
        $filtered = $preCount - $devices.Count
        if ($filtered -gt 0) {
            Write-Host " Filtered to Windows devices: $($devices.Count) of $preCount (skipped $filtered non-Windows)." -ForegroundColor Gray
        }
    }
}

if (-not $devices -or $devices.Count -eq 0) {
    Write-Error "No devices found. Exiting."
    return
}

Write-Host "Found $($devices.Count) device(s)." -ForegroundColor Green
Write-Host ""

# =============================================================================
# STEP 2: Scan each device via WMI to discover disk instances
# =============================================================================
# Structure: $deviceDisks[deviceId] = @( "0 C:", "1 D:", ... )
$deviceDisks = @{}
$allInstances = @{}   # unique instance -> $true (dedup across all devices)
$deviceInfo = @{}     # deviceId -> @{ Name; IP }
$deviceDiskStats = @{} # deviceId -> @{ "instanceName" -> @{ CounterKey = value; ... } }

$scanIndex = 0
foreach ($dev in $devices) {
    $scanIndex++
    $devId   = $dev.id
    $devName = if ($dev.displayName) { $dev.displayName } else { $dev.hostName }
    $devIP   = $dev.networkAddress

    if (-not $devIP) {
        Write-Warning "Device '$devName' (ID: $devId) has no network address. Skipping."
        continue
    }

    $deviceInfo[$devId] = @{ Name = $devName; IP = $devIP }

    $pct = [Math]::Round(($scanIndex / $devices.Count) * 100)
    Write-Progress -Activity "Scanning devices via WMI" `
        -Status "$devName ($devIP) [$scanIndex of $($devices.Count)]" `
        -PercentComplete $pct

    Write-Host " [$scanIndex/$($devices.Count)] Scanning $devName ($devIP)..." -ForegroundColor Gray -NoNewline

    # Try each credential in order; stop on first success or non-auth failure
    $scanSuccess = $false
    for ($ci = 0; $ci -lt $credentialList.Count; $ci++) {
        $tryCred = $credentialList[$ci]
        if ($credentialList.Count -gt 1 -and $ci -gt 0) {
            Write-Host "" -NoNewline  # newline before retry
            Write-Host " Retrying with credential #$($ci + 1) ($($tryCred.UserName))..." -ForegroundColor DarkGray -NoNewline
        }

        try {
            # WMI does not allow explicit credentials for local connections
            $isLocal = ($devIP -eq 'localhost' -or $devIP -eq '127.0.0.1' -or $devIP -eq '::1' -or $devIP -eq $env:COMPUTERNAME -or $devIP -eq '.')
            $wmiSplat = @{ Class = $WmiClass; ErrorAction = 'Stop' }
            if (-not $isLocal) { $wmiSplat.ComputerName = $devIP; $wmiSplat.Credential = $tryCred }
            $perfData = Get-WmiObject @wmiSplat |
                Where-Object { $_.Name -and $_.Name -ne '' }

            # Success -- process results
            if (-not $perfData) {
                Write-Host " no disks found." -ForegroundColor Yellow
                $scanSuccess = $true
                break
            }

            $instances = @()
            foreach ($disk in $perfData) {
                $instanceName = $disk.Name

                # Apply exclusion filter
                $excluded = $false
                foreach ($pattern in $ExcludeInstance) {
                    if ($instanceName -like "*$pattern*") {
                        $excluded = $true
                        break
                    }
                }
                if ($excluded) { continue }

                $instances += $instanceName
                $allInstances[$instanceName] = $true

                # Capture current counter values for dashboard/export
                if (-not $deviceDiskStats.ContainsKey($devId)) {
                    $deviceDiskStats[$devId] = @{}
                }
                $snap = [ordered]@{}
                foreach ($ck in $activeCounters.Keys) {
                    $prop = $activeCounters[$ck].Property
                    $val  = $disk.$prop
                    if ($null -ne $val) {
                        $snap[$ck] = [double]$val
                    }
                }
                $deviceDiskStats[$devId][$instanceName] = $snap
            }

            if ($instances.Count -gt 0) {
                $deviceDisks[$devId] = $instances
                $credLabel = if ($credentialList.Count -gt 1) { " [cred #$($ci + 1)]" } else { '' }
                Write-Host " found $($instances.Count) disk(s): $($instances -join ', ')$credLabel" -ForegroundColor Green
            }
            else {
                Write-Host " no disks after filtering." -ForegroundColor Yellow
            }
            $scanSuccess = $true
            break  # done with this device
        }
        catch [System.UnauthorizedAccessException] {
            # Auth failure -- try next credential if available
            if ($ci -lt ($credentialList.Count - 1)) {
                Write-Verbose "Credential #$($ci + 1) ($($tryCred.UserName)) access denied on ${devIP}. Trying next."
                continue
            }
            Write-Host " access denied (all $($credentialList.Count) credential(s) failed)." -ForegroundColor Red
            Write-Verbose "UnauthorizedAccessException on ${devIP}: $_"
        }
        catch [System.Runtime.InteropServices.COMException] {
            $comMsg = $_.Exception.Message
            # Access-denied COM errors (0x80070005) -- try next credential
            if ($comMsg -match 'Access is denied|0x80070005') {
                if ($ci -lt ($credentialList.Count - 1)) {
                    Write-Verbose "Credential #$($ci + 1) ($($tryCred.UserName)) COM access denied on ${devIP}. Trying next."
                    continue
                }
                Write-Host " access denied (all $($credentialList.Count) credential(s) failed)." -ForegroundColor Red
            }
            else {
                # RPC unavailable, network error, etc. -- no point trying another cred
                Write-Host " WMI/RPC unavailable." -ForegroundColor Red
            }
            Write-Verbose "COMException on ${devIP}: $_"
        }
        catch {
            # Non-auth error -- don't try more creds, skip device
            Write-Host " failed: $($_.Exception.Message)" -ForegroundColor Red
            Write-Verbose "Error on ${devIP}: $_"
            break
        }
    }
}

Write-Progress -Activity "Scanning devices via WMI" -Completed

if ($deviceDisks.Count -eq 0) {
    Write-Warning "No physical disks discovered on any device. Nothing to do."
    return
}

$sortedInstances = @($allInstances.Keys | Sort-Object)

Write-Host ""
Write-Host "--- Discovery Summary ---" -ForegroundColor Cyan
Write-Host " Devices scanned: $($devices.Count)" -ForegroundColor White
Write-Host " Devices with disks: $($deviceDisks.Count)" -ForegroundColor White
Write-Host " Unique disk instances: $($sortedInstances.Count)" -ForegroundColor White
Write-Host " Counters per disk: $($activeCounters.Count)" -ForegroundColor White
Write-Host " Unique instances: $($sortedInstances -join ', ')" -ForegroundColor Gray
Write-Host ""

# Show per-device breakdown
Write-Host " Per-device disk map:" -ForegroundColor White
foreach ($devId in ($deviceDisks.Keys | Sort-Object)) {
    $info = $deviceInfo[$devId]
    $disks = $deviceDisks[$devId] -join ', '
    Write-Host " $($info.Name) ($($info.IP)): $disks" -ForegroundColor Gray
}
Write-Host ""

$totalMonitors   = $sortedInstances.Count * $activeCounters.Count
$totalAssignments = 0
foreach ($devId in $deviceDisks.Keys) {
    $totalAssignments += $deviceDisks[$devId].Count * $activeCounters.Count
}

Write-Host " Monitors to create (library): up to $totalMonitors" -ForegroundColor White
Write-Host " Assignments to make (devices): up to $totalAssignments" -ForegroundColor White
Write-Host ""

if ($DryRun) {
    Write-Host "[DRY RUN] Listing planned monitors:" -ForegroundColor Yellow
    Write-Host ""
    foreach ($instance in $sortedInstances) {
        foreach ($counterKey in $activeCounters.Keys) {
            $def = $activeCounters[$counterKey]
            $monName = "${NamePrefix} - ${instance} - $($def.Label)"
            $assignTo = @()
            foreach ($devId in $deviceDisks.Keys) {
                if ($deviceDisks[$devId] -contains $instance) {
                    $assignTo += "$($deviceInfo[$devId].Name)(#$devId)"
                }
            }
            Write-Host " [CREATE] $monName" -ForegroundColor Gray
            Write-Host " Assign to: $($assignTo -join ', ')" -ForegroundColor DarkGray
        }
    }
    Write-Host ""
    Write-Host "[DRY RUN] No changes made." -ForegroundColor Yellow
    return
}

# =============================================================================
# STEP 3: Action routing (menu or -Action parameter)
# =============================================================================
if ($OutputPath) {
    $OutputDir = $OutputPath
} else {
    $OutputDir = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output'
}
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }

$choice = $null
if ($Action) {
    switch ($Action) {
        'PushToWUG'        { $choice = '1' }
        'ExportJSON'       { $choice = '2' }
        'ExportCSV'        { $choice = '3' }
        'ShowTable'        { $choice = '4' }
        'Dashboard'        { $choice = '5' }
        'None'             { $choice = '6' }
        'DashboardAndPush' { $choice = '7' }
    }
}

if (-not $choice -and $NonInteractive) { $choice = '5' }

if (-not $choice) {
    Write-Host "What would you like to do with the discovered disks?" -ForegroundColor Cyan
    if ($standaloneMode) {
        Write-Host " [1] Push monitors to WhatsUp Gold (not available in standalone mode)" -ForegroundColor DarkGray
    }
    else {
        Write-Host " [1] Push monitors to WhatsUp Gold (creates library monitors + assigns)"
    }
    Write-Host " [2] Export to JSON file"
    Write-Host " [3] Export to CSV file"
    Write-Host " [4] Show full table in console"
    Write-Host " [5] Generate HTML dashboard (disk inventory)"
    Write-Host " [6] Exit (do nothing)"
    if (-not $standaloneMode) {
        Write-Host " [7] Dashboard + Push to WUG"
    }
    Write-Host ""
    $choice = Read-Host -Prompt "Choice [1-7]"
}

# Handle DashboardAndPush: run Dashboard then PushToWUG sequentially
if ($choice -eq '7') {
    $actionsToRun = @('5', '1')
} else {
    $actionsToRun = @($choice)
}

# Build export-friendly data for JSON/CSV/Table/Dashboard
$exportData = @()
foreach ($devId in ($deviceDisks.Keys | Sort-Object)) {
    $info  = $deviceInfo[$devId]
    foreach ($instance in $deviceDisks[$devId]) {
        $row = [ordered]@{
            DeviceId = $devId
            Host     = $info.Name
            IP       = $info.IP
            DiskType = $DiskType
            Instance = $instance
        }
        # Add actual counter values from the WMI snapshot
        $snap = $null
        if ($deviceDiskStats.ContainsKey($devId) -and $deviceDiskStats[$devId].ContainsKey($instance)) {
            $snap = $deviceDiskStats[$devId][$instance]
        }
        foreach ($ck in $activeCounters.Keys) {
            $label = $activeCounters[$ck].Label
            if ($snap -and $snap.Contains($ck)) {
                $row[$label] = $snap[$ck]
            }
            else {
                $row[$label] = $null
            }
        }
        $exportData += [PSCustomObject]$row
    }
}

foreach ($currentChoice in $actionsToRun) {
switch ($currentChoice) {
    '1' {
        # PushToWUG
        if ($standaloneMode) {
            Write-Warning "Push to WUG requires devices resolved from WhatsUp Gold. Omit -Target or use -DeviceId."
            continue
        }

        # --- Fetch existing WUG performance monitors to avoid duplicates ------
        Write-Host "Checking existing performance monitors in WUG library..." -ForegroundColor Cyan

        $existingMonitors = @(Get-WUGPerformanceMonitor -Search $NamePrefix -View 'info')
        $existingLookup = @{}
        foreach ($mon in $existingMonitors) {
            $existingLookup[$mon.Name] = $mon
        }

        Write-Host " Found $($existingMonitors.Count) existing monitor(s) matching prefix '$NamePrefix'." -ForegroundColor Gray

        # Pre-fetch existing monitor assignments per device
        Write-Host "Checking existing device monitor assignments..." -ForegroundColor Cyan
        $deviceExistingMonitors = @{}
        foreach ($devId in $deviceDisks.Keys) {
            $deviceExistingMonitors[$devId] = @{}
            try {
                $assigned = @(Get-WUGPerformanceMonitor -DeviceId $devId -Search $NamePrefix -View 'basic')
                foreach ($a in $assigned) {
                    $typeId = if ($a.MonitorTypeId) { $a.MonitorTypeId } elseif ($a.monitorTypeId) { $a.monitorTypeId } else { $null }
                    if ($typeId) {
                        $deviceExistingMonitors[$devId]["$typeId"] = $true
                    }
                }
                Write-Verbose " Device $devId ($($deviceInfo[$devId].Name)): $($assigned.Count) existing '$NamePrefix' monitors."
            }
            catch {
                Write-Verbose " Could not check existing monitors for device ${devId}: $_"
            }
        }
        Write-Host " Done." -ForegroundColor Gray
        Write-Host ""

        # --- Create library monitors and assign to devices --------------------
        $createdCount        = 0
        $skippedCount        = 0
        $assignedCount       = 0
        $alreadyAssignedCount = 0
        $failedCount         = 0

        $monitorIdCache = @{}
        foreach ($mon in $existingMonitors) {
            $monitorIdCache[$mon.Name] = if ($mon.MonitorId) { $mon.MonitorId } else { $mon.Id }
        }

        $totalSteps = $sortedInstances.Count * $activeCounters.Count
        $stepIndex  = 0

        foreach ($instance in $sortedInstances) {
            foreach ($counterKey in $activeCounters.Keys) {
                $stepIndex++
                $def     = $activeCounters[$counterKey]
                $monName = "${NamePrefix} - ${instance} - $($def.Label)"

                $pct = [Math]::Round(($stepIndex / $totalSteps) * 100)
                Write-Progress -Activity "Creating disk IO monitors" `
                    -Status "$monName [$stepIndex of $totalSteps]" `
                    -PercentComplete $pct

                if ($monitorIdCache.ContainsKey($monName)) {
                    $libId = $monitorIdCache[$monName]
                    Write-Verbose "Monitor '$monName' already exists (ID: $libId). Skipping creation."
                    $skippedCount++
                }
                else {
                    Write-Host " Creating: $monName" -ForegroundColor White
                    try {
                        $result = Add-WUGPerformanceMonitor `
                            -Type WmiFormatted `
                            -Name $monName `
                            -WmiFormattedRelativePath $WmiClass `
                            -WmiFormattedPropertyName $def.Property `
                            -WmiFormattedDisplayname "$($def.Label) ($instance)" `
                            -WmiFormattedInstanceName $instance `
                            -WmiFormattedTimeout $WmiTimeout

                        if ($result -and $result.data -and $result.data.idMap) {
                            $libId = $result.data.idMap.resultId
                        }
                        elseif ($result -match 'library ID:\s*(\d+)') {
                            $libId = $Matches[1]
                        }
                        else {
                            $refetch = @(Get-WUGPerformanceMonitor -Search $monName -View 'info')
                            $match = $refetch | Where-Object { $_.Name -eq $monName } | Select-Object -First 1
                            if ($match) {
                                $libId = if ($match.MonitorId) { $match.MonitorId } else { $match.Id }
                            }
                            else {
                                Write-Warning " Could not determine library ID for '$monName'. Skipping assignments."
                                $failedCount++
                                continue
                            }
                        }

                        $monitorIdCache[$monName] = $libId
                        $createdCount++
                        Write-Host " Created (ID: $libId)" -ForegroundColor Green
                    }
                    catch {
                        Write-Error " Failed to create '$monName': $_"
                        $failedCount++
                        continue
                    }
                }

                $libId = $monitorIdCache[$monName]
                foreach ($devId in $deviceDisks.Keys) {
                    if ($deviceDisks[$devId] -contains $instance) {
                        $info = $deviceInfo[$devId]

                        if ($deviceExistingMonitors.ContainsKey($devId) -and $deviceExistingMonitors[$devId].ContainsKey("$libId")) {
                            Write-Verbose " '$monName' already assigned to $($info.Name) (ID: $devId). Skipping."
                            $alreadyAssignedCount++
                            continue
                        }

                        Write-Verbose " Assigning '$monName' to $($info.Name) (ID: $devId)"
                        try {
                            Add-WUGPerformanceMonitorToDevice `
                                -DeviceId $devId `
                                -MonitorId $libId `
                                -PollingIntervalMinutes $PollingIntervalMinutes `
                                -Enabled "true"
                            $assignedCount++
                            if ($deviceExistingMonitors.ContainsKey($devId)) {
                                $deviceExistingMonitors[$devId]["$libId"] = $true
                            }
                        }
                        catch {
                            $errMsg = $_.Exception.Message
                            if ($errMsg -match '409|already assigned|already exists|duplicate') {
                                Write-Host " Already assigned: $monName -> $($info.Name)" -ForegroundColor DarkGray
                                $alreadyAssignedCount++
                            }
                            else {
                                Write-Warning " Failed to assign '$monName' to $($info.Name) (ID: ${devId}): $errMsg"
                                $failedCount++
                            }
                        }
                    }
                }
            }
        }

        Write-Progress -Activity "Creating disk IO monitors" -Completed

        Write-Host ""
        Write-Host "=== Disk IO Push Complete ===" -ForegroundColor Cyan
        Write-Host " Monitors created: $createdCount" -ForegroundColor Green
        Write-Host " Monitors reused: $skippedCount" -ForegroundColor Gray
        Write-Host " Assignments made: $assignedCount" -ForegroundColor Green
        Write-Host " Already assigned: $alreadyAssignedCount" -ForegroundColor Gray
        Write-Host " Failures: $failedCount" -ForegroundColor $(if ($failedCount -gt 0) { 'Red' } else { 'Gray' })
        Write-Host ""
    }
    '2' {
        # ExportJSON
        $jsonPath = Join-Path $OutputDir "WindowsDiskIO-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
        $exportData | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8
        Write-Host "Exported to: $jsonPath" -ForegroundColor Green
    }
    '3' {
        # ExportCSV
        $csvPath = Join-Path $OutputDir "WindowsDiskIO-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
        $exportData | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
        Write-Host "Exported to: $csvPath" -ForegroundColor Green
    }
    '4' {
        # ShowTable
        $exportData | Format-Table -AutoSize
    }
    '5' {
        # Dashboard - generate HTML disk inventory report
        $dashPath = Join-Path $OutputDir 'WindowsDiskIO-Dashboard.html'

        if (Get-Command -Name 'Export-WindowsDiskIODashboardHtml' -ErrorAction SilentlyContinue) {
            Export-WindowsDiskIODashboardHtml -DashboardData $exportData `
                -OutputPath $dashPath `
                -ReportTitle 'Windows Disk IO Inventory'
        }
        elseif (Get-Command -Name 'Export-DynamicDashboardHtml' -ErrorAction SilentlyContinue) {
            # Build threshold rules for counter columns that have meaningful thresholds
            $thresholds = @()
            if ($activeCounters.ContainsKey('PercentDiskTime')) {
                $thresholds += @{ Field = '% Disk Time'; Warning = 70; Critical = 90 }
            }
            if ($activeCounters.ContainsKey('AvgDiskQueueLength')) {
                $thresholds += @{ Field = 'Avg. Disk Queue Length'; Warning = 2; Critical = 5 }
            }

            $dashSplat = @{
                Data         = $exportData
                OutputPath   = $dashPath
                ReportTitle  = 'Windows Disk IO Inventory'
                CardField    = @('Host', 'DiskType')
                ExportPrefix = 'WindowsDiskIO'
            }
            if ($thresholds.Count -gt 0) { $dashSplat.ThresholdField = $thresholds }

            Export-DynamicDashboardHtml @dashSplat
        }
        else {
            Write-Warning "No dashboard function available. Exporting as JSON instead."
            $jsonPath = Join-Path $OutputDir "WindowsDiskIO-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
            $exportData | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8
            Write-Host "JSON exported: $jsonPath" -ForegroundColor Green
            break
        }

        Write-Host "Dashboard generated: $dashPath" -ForegroundColor Green
        Write-Host " Devices: $($deviceDisks.Count) | Instances: $($sortedInstances.Count) | Counters: $($activeCounters.Count)" -ForegroundColor Gray

        # Try to copy to WUG NmConsole
        $nmConsolePaths = @(
            "${env:ProgramFiles(x86)}\Ipswitch\WhatsUp\Html\NmConsole"
            "${env:ProgramFiles}\Ipswitch\WhatsUp\Html\NmConsole"
        )
        $nmConsolePath = $nmConsolePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
        if ($nmConsolePath) {
            $wugDashDir = Join-Path $nmConsolePath 'dashboards'
            if (-not (Test-Path $wugDashDir)) { New-Item -ItemType Directory -Path $wugDashDir -Force | Out-Null }
            $wugDashPath = Join-Path $wugDashDir 'WindowsDiskIO-Dashboard.html'
            try {
                Copy-Item -Path $dashPath -Destination $wugDashPath -Force
                Write-Host "Copied to WUG: $wugDashPath" -ForegroundColor Green
                Write-Host " Access via WUG web UI: /NmConsole/dashboards/WindowsDiskIO-Dashboard.html" -ForegroundColor Cyan
            }
            catch {
                Write-Warning "Could not copy to NmConsole (run as admin?): $_"
            }
            Deploy-DashboardWebConfig -Path $wugDashDir
        }
    }
    '6' {
        Write-Host "No action taken." -ForegroundColor Gray
    }
    default {
        Write-Warning "Invalid choice '$currentChoice'."
    }
}
} # end foreach actionsToRun

Write-Host ""
Write-Host "=== Disk IO Discovery Complete ===" -ForegroundColor Cyan
Write-Host ""

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAPyFJnlMl5Z7j/
# JU3uCWCrT4hnovnIbzQ+J8CgVlPMp6CCJQ0wggVvMIIEV6ADAgECAhBI/JO0YFWU
# jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI
# DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM
# EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy
# dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG
# EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv
# IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s
# hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD
# J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7
# P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme
# me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz
# T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q
# RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz
# mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc
# QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T
# OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/
# AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID
# AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD
# VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV
# HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE
# VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v
# ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE
# KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI
# hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF
# OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC
# J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ
# pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl
# d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH
# +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO
# +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi
# BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw
# MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp
# Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb
# hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT
# cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb
# NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP
# XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr
# VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP
# ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv
# kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM
# cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls
# 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR
# a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6
# MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY
# rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E
# BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig
# NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg
# v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT
# SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh
# 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw
# uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO
# QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD
# TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G
# CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp
# bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290
# IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT
# AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g
# UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB
# jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe
# AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV
# oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk
# 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh
# P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41
# aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+
# ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/
# 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g
# Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA
# QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B
# Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD
# AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG
# Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n
# Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v
# Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu
# cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG
# 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW
# 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh
# rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp
# mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc
# ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE
# Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz
# NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw
# 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6
# E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6
# xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt
# g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+
# MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ
# BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl
# Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw
# WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj
# dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB
# bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW
# kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw
# zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1
# DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb
# w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1
# Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k
# dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J
# BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d
# n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs
# CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU
# q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE
# LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW
# gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G
# aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI
# KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC
# ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT
# aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz
# Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG
# SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3
# wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9
# vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd
# vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6
# LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO
# WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7
# nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6
# g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7
# z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G
# CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy
# NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT
# CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh
# of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA
# xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv
# MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps
# 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF
# 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi
# UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM
# CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP
# pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU
# G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+
# sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI
# GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+
# IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M
# yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2
# th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj
# V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2
# Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze
# GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG
# 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N
# Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT
# etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW
# kPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0B
# AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVow
# YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD
# EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw
# MjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5g
# VrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN
# +vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qo
# me7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ/
# /nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouT
# MYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8
# DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnp
# JeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP5
# 1ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49
# kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5P
# WPsWeupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7Y
# ufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA
# MB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK
# 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI
# KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT
# SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2
# U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9
# bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP
# 2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O
# 6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskg
# iC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMU
# BaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDF
# kxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+
# zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/l
# wd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxl
# RcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2
# zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJg
# baP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOC
# IUjsarfNZzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl
# Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu
# aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY
# BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi
# BCAXZX5t8CcO1ix91BCVqLnHw/zbdJzr8gK1Z8IerUENPDANBgkqhkiG9w0BAQEF
# AASCAgDr28r/3SMCV9p7kiRqbobuq4UG4wXW/5iSX3WkEa6NSptcffPpvQbIbhOU
# csfI5X/th4iABZCCp3WGfJXgr8FEj0Ts1BykEvr4EX27y65wyBo6UrGjSbLfm7lX
# R7c0HiWlgWjMVD7qQhQ+jxMoU1KBEf29xdLNudBSBTThmGWX08q4TE8LmIv4QZHk
# 9wrVxPpZNDW2nnBKHvXSUHvwTuNnDQJYijHE3Z49mezxUeVRPSWcmG8P6EWEv8eK
# ApX/Jl7qYHlAgO7ADivJHx8W4QBy+HViR4IvQ3Y/9MrQkP9MBnOT8xAaBNzI9iwb
# NmSBi+SOrz7CEQ0k8cTOHGBf7ghdQyPWYCZaggf6ILPzdwkmW3Ux8lNM1LTmmmlF
# Upx48wMk086fwwpE+jVFB/UXV7Lb2HZpIUZO4P4DgOTTrTtK9zPkUwxs5mgdThwg
# juY/Lw+hioWJazTKIPajAVOPqk2GuzWOIGbVJ1/fzkfV5rLsoXE/okpaFZnAF59w
# gwdQq7JdwnI02NLetW/4f84eELJLrsR1jkUb9dqIr04LcAjoHO6HyvC3DgYYySL3
# 7H+K6WzlRvneR9Kyzwof33/PzlCoFr0fo2crDq5aqgWgU58cQPMj96y5PDRP8fyS
# vNmZNeIPhL6ERJNYDcjx9eiXa4yLF0ORIUFuf3NNvWxz5WscY6GCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA1MjgyMDE2MjBaMC8GCSqGSIb3DQEJBDEiBCCePe5X
# N66RRBKPDaW3TKPOt5B6ghdUgIJlum1k4b5L6TANBgkqhkiG9w0BAQEFAASCAgC8
# RIpYCm/NzwBRYzyLiydjbFLca2I0hrk/3dmvuBnFDbFYQSjwuXPjvEmFs7NO9EIh
# dA8RqVn9j6kKfgMm0DoZBnGGe5NgZoZ53OPNYaYqj9zxvVsd93f+kTJa5vgjMoqT
# BYz9aPmz+t1jNvKCnFSqqA7B1Zon8nVRtpTPkxFWc/CYQPu1QwYYhzPlOi2Po60Z
# uVEJNBsTl1dWY6Sd8T+OsVxA7SmsZak0x3Olbfd+q6YHCRGRC26WQKwbiLQv9x2u
# dAr0ftGRwgGWDWkAy3CxM31UHhuYl5vNbeCtDvH5eOfwINzQiJherif3ZZltTpA9
# LALd9xQXm1jpbkNVZBEBTknMeKRtIuaAc5H831knNYEfJaBDH8ukLVURJBSijtXj
# i0tHcL5R4Z1iFkRoYY7e7MEhKhJ3k8zFijjLOozpHc4Z3WUQzSfdAENM/NGt947v
# oPUbLVXeAJ2foNhlXQXbhve05XMnWMhDGRiDZeI3iOY3eoLOwk2hM6m9TgwKpEZR
# 6sacbvGGRh5nBrG1F6ZL5zkV3OCkDjidRCq7N0nCELmp+Kvs0RLIi6u3dqfgsFd8
# yyNXayynEVm7Fkk23+lfvIzYY0pXc0HdtTCHbAPaX5zabhuEvPt+UvzEckGkOCRD
# lgPdmQb+/bMzG3VshaHwr/inpos/4g3t2S7lc15mSg==
# SIG # End signature block