helpers/cisco-wlc/Export-CiscoWLCAPDevicesToWUG.ps1

#requires -Version 5.1

<#
.SYNOPSIS
    Exports Cisco WLC Access Points as devices to WhatsUp Gold with per-AP SNMP monitors.
 
.DESCRIPTION
    Reads WLC discovery output (AP inventory from JSONL) and creates/updates AP devices in WhatsUp Gold.
    Each AP becomes a device with:
    - SNMP active monitor for AP admin status (OID 1.3.6.1.4.1.9.9.513.1.1.1.1.38 with per-AP instance)
    - Custom device attributes: WLC_Address, AP_Name, AP_MAC, AP_Model, AP_Serial, AP_Location, etc.
 
.PARAMETER InputDirectory
    Directory containing discovery output (full/ subdirectory with JSONL files).
    Default: $env:LOCALAPPDATA\WhatsUpGoldPS\DiscoveryHelpers\Output\full
 
.PARAMETER WLCAddress
    Primary WLC IP address (used for all AP SNMP monitors).
 
.PARAMETER WUGServer
    WhatsUp Gold server URI. Default: https://192.168.74.74:9644
 
.PARAMETER WUGCredential
    Credentials for WUG API connection.
 
.PARAMETER DeviceGroupId
    Device group ID for new AP devices. Default: 0 (Static group)
 
.PARAMETER SNMPCredentialName
    Name of the SNMP credential in WUG to assign to AP devices. If not specified,
    no SNMP credential is assigned (useful if the device inherits one).
 
.EXAMPLE
    .\Export-CiscoWLCAPDevicesToWUG.ps1 -InputDirectory 'C:\temp\wlc-output\full' `
        -WLCAddress 192.168.1.100 -WUGServer 'https://wug.lab:9644'
 
.EXAMPLE
    .\Export-CiscoWLCAPDevicesToWUG.ps1 -WLCAddress 192.168.75.33 `
        -SNMPCredentialName 'SNMP v2 - public'
 
.NOTES
    Requires: WhatsUpGoldPS module, active WUG connection
    Encoding: UTF-8 with BOM
    Author: WhatsUpGoldPS Discovery Framework
#>


[CmdletBinding()]
param(
    [Parameter()]
    [string]$InputDirectory,

    [Parameter(Mandatory)]
    [string]$WLCAddress,

    [Parameter()]
    [string]$WUGServer,

    [Parameter()]
    [pscredential]$WUGCredential,

    [Parameter()]
    [int]$DeviceGroupId = 0,

    [Parameter()]
    [string]$SNMPCredentialName,

    [Parameter()]
    [int]$LimitAPs = 10,

    [Parameter()]
    [switch]$DryRun
)

# ============================================================================
# Module / dependency loading
# ============================================================================

if (-not (Get-Command -Name 'Get-WUGAPIResponse' -ErrorAction SilentlyContinue)) {
    try {
        $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
        $repoPsd1 = Join-Path $repoRoot 'WhatsUpGoldPS.psd1'
        if (Test-Path $repoPsd1) {
            Import-Module $repoPsd1 -Force -ErrorAction Stop
        }
        else {
            Import-Module WhatsUpGoldPS -ErrorAction Stop
        }
    }
    catch {
        Write-Error "Could not load WhatsUpGoldPS module. $_"
        return
    }
}

# Resolve input directory
if ([string]::IsNullOrWhiteSpace($InputDirectory)) {
    $base = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output'
    $InputDirectory = Join-Path $base 'full'
}

if (-not (Test-Path $InputDirectory)) {
    Write-Error "Input directory not found: $InputDirectory"
    return
}

Write-Verbose "Using input directory: $InputDirectory"
Write-Verbose "WLC Address: $WLCAddress"

# ============================================================================
# STEP 1: Read AP discovery data from JSONL
# ============================================================================

Write-Host ""
Write-Host "Loading AP discovery data..." -ForegroundColor Cyan

$apItems = @()
# Read from wireless-ap-inventory-summary.json (produced by Build-Wireless-Summaries)
# This is in the summary subdirectory of the input directory's parent
$baseDir = Split-Path $InputDirectory -Parent
$summaryDir = Join-Path $baseDir 'summary'
$apInventoryFile = Join-Path $summaryDir 'wireless-ap-inventory-summary.json'

if (-not (Test-Path $apInventoryFile)) {
    Write-Warning "No wireless-ap-inventory-summary.json found at $apInventoryFile"
    Write-Warning "Please run discovery first to generate the AP inventory"
    return
}

Write-Verbose "Reading $apInventoryFile"
try {
    $apItems = @(Get-Content -Path $apInventoryFile -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop)
}
catch {
    Write-Warning "Error reading AP inventory: $_"
    return
}

$apCount = $apItems.Count
Write-Host " Found $apCount AP(s)" -ForegroundColor Green

if ($apCount -eq 0) {
    Write-Warning "No AP items found. Exiting."
    return
}

# ============================================================================
# STEP 2: Build device plan for APs
# ============================================================================

Write-Host ""
Write-Host "Building AP device plan..." -ForegroundColor Cyan

# Sanitize AP names: replace invalid hostname characters
function Sanitize-HostName {
    param([string]$Name)
    # Replace invalid hostname characters (/, \, :, etc.) with hyphens
    return $Name -replace '[/\\:*?"<>|]', '-'
}

$devicePlan = @{}

foreach ($ap in $apItems) {
    # Use properties from AP inventory JSON: APName, APMac
    $rawName = if ($ap.APName -and $ap.APName -ne '') { $ap.APName } else { $ap.APMac }
    $sanitizedName = Sanitize-HostName -Name $rawName
    
    $apKey = if ($ap.APMac) { "AP-$($ap.APMac)" } else { "AP-$sanitizedName" }
    $displayName = $sanitizedName
    $apIP = if ($ap.APIPAddress -and $ap.APIPAddress -ne '') { $ap.APIPAddress } else { $WLCAddress }

    if ($devicePlan.Keys -notcontains $apKey) {
        $devicePlan[$apKey] = @{
            Name      = $displayName
            IP        = $apIP
            WLCIP     = $WLCAddress
            Type      = 'AccessPoint'
            APMac     = $ap.APMac
            APIndex   = if ($ap.IndexSuffix) { $ap.IndexSuffix } else { $ap.APMac }  # Per-AP SNMP instance
            Attrs     = @{}
            Items     = [System.Collections.Generic.List[object]]::new()
        }
    }

    $dev = $devicePlan[$apKey]

    # Collect attributes from AP inventory JSON
    $dev.Attrs['AP_Name']        = $rawName  # Store original name in attribute
    $dev.Attrs['AP_MAC']         = if ($ap.APMac) { $ap.APMac } else { 'N/A' }
    $dev.Attrs['AP_Uptime']      = if ($ap.ApUptime) { $ap.ApUptime } else { '' }
    $dev.Attrs['AP_AdminStatus'] = if ($ap.AdminStatus) { $ap.AdminStatus } else { '' }
    $dev.Attrs['AP_PowerStatus'] = if ($ap.PowerStatus) { $ap.PowerStatus } else { '' }
    $dev.Attrs['AP_CpuCurrent']  = if ($ap.CpuCurrentPct) { "$($ap.CpuCurrentPct)%" } else { '' }
    $dev.Attrs['AP_CpuAvg']      = if ($ap.CpuAvgPct) { "$($ap.CpuAvgPct)%" } else { '' }
    $dev.Attrs['AP_MemCurrent']  = if ($ap.MemCurrentPct) { "$($ap.MemCurrentPct)%" } else { '' }
    $dev.Attrs['AP_MemAvg']      = if ($ap.MemAvgPct) { "$($ap.MemAvgPct)%" } else { '' }
    $dev.Attrs['AP_PrimaryController'] = if ($ap.PrimaryControllerAddress) { $ap.PrimaryControllerAddress } else { '' }
    $dev.Attrs['WLC_Address']    = $WLCAddress
    $dev.Attrs['AP_IPAddress']   = if ($ap.APIPAddress) { $ap.APIPAddress } else { '' }
    $dev.Attrs['AP_SNMPInstance'] = if ($ap.IndexSuffix) { $ap.IndexSuffix } else { '1' }  # Store instance for reference

    # Store SNMP instance on device for easy access during monitor assignment
    $dev.SNMPInstance = if ($ap.IndexSuffix) { $ap.IndexSuffix } else { '1' }

    # --- Monitor items ---
    # Active: Shared across all APs (instance passed per-device via Argument)
    
    # Active: AP Admin Status (SNMP health check - up if admin status = 1)
    $dev.Items.Add([PSCustomObject]@{
        Name        = "SNMP AP Admin Status"
        ItemType    = 'ActiveMonitor'
        MonitorType = 'SNMP'
    })
}

Write-Host " Created device plan for $($devicePlan.Count) unique AP(s)" -ForegroundColor Green

# --- Apply limit if specified (for testing) ---
if ($LimitAPs -gt 0 -and $LimitAPs -lt $devicePlan.Count) {
    $limitedPlan = @{}
    $count = 0
    foreach ($key in @($devicePlan.Keys)) {
        if ($count -ge $LimitAPs) { break }
        $limitedPlan[$key] = $devicePlan[$key]
        $count++
    }
    $devicePlan = $limitedPlan
    Write-Host " Limited to first $LimitAPs AP(s) for testing" -ForegroundColor Yellow
}

# ============================================================================
# DRY RUN MODE: Show plan without creating anything
# ============================================================================

if ($DryRun) {
    Write-Host ""
    Write-Host "=== DRY RUN MODE - Plan Preview ===" -ForegroundColor Yellow
    Write-Host "Would create $($devicePlan.Count) AP device(s) with the following monitors:" -ForegroundColor Cyan
    Write-Host ""
    
    $uniqueActNames = @()
    $uniquePerfNames = @()
    foreach ($key in @($devicePlan.Keys)) {
        $dev = $devicePlan[$key]
        foreach ($item in @($dev.Items)) {
            if ($item.ItemType -eq 'ActiveMonitor' -and $item.Name -notin $uniqueActNames) {
                $uniqueActNames += $item.Name
            }
            elseif ($item.ItemType -eq 'PerformanceMonitor' -and $item.Name -notin $uniquePerfNames) {
                $uniquePerfNames += $item.Name
            }
        }
    }
    
    Write-Host "Active Monitors to create (SHARED):" -ForegroundColor Green
    foreach ($actName in $uniqueActNames) {
        Write-Host " - $actName (OID: 1.3.6.1.4.1.9.9.513.1.1.1.1.38, instance per-AP via Argument)" -ForegroundColor Gray
    }
    
    Write-Host ""
    Write-Host "Devices to create:" -ForegroundColor Green
    foreach ($key in @($devicePlan.Keys)) {
        $dev = $devicePlan[$key]
        Write-Host " - $($dev.Name) @ $($dev.IP) (with $($dev.Items.Count) monitor assignments)" -ForegroundColor Gray
    }
    
    Write-Host ""
    Write-Host "=== End DRY RUN MODE ===" -ForegroundColor Yellow
    Write-Host "Run again WITHOUT -DryRun to create these devices and monitors." -ForegroundColor Cyan
    Write-Host ""
    return
}

# ============================================================================
# STEP 3: Connect to WUG
# ============================================================================

Write-Host ""
Write-Host "Connecting to WhatsUp Gold..." -ForegroundColor Cyan

# Use existing connection if available; otherwise use vaulted credentials (no-prompt mode)
if (-not $global:WhatsUpServerBaseURI) {
    try {
        if ($WUGCredential) {
            Connect-WUGServer -serverUri $WUGServer -Credential $WUGCredential -IgnoreSSLErrors -ErrorAction Stop
        }
        else {
            # Use vaulted WUG server credentials; use -UseVault to prevent interactive prompts
            # If this param doesn't exist, fallback to regular connection
            $ConnectParams = @{ ErrorAction = 'Stop' }
            if ($WUGServer) { $ConnectParams['serverUri'] = $WUGServer }
            Connect-WUGServer @ConnectParams
        }
    }
    catch {
        Write-Error "Failed to connect to WhatsUp Gold: $_"
        Write-Error "Please ensure WUG connection is configured. Run: Connect-WUGServer"
        return
    }
}

if (-not $global:WhatsUpServerBaseURI) {
    Write-Error "No WhatsUp Gold connection established. Please use Connect-WUGServer first."
    return
}

# Capture the connected server URI and extract server address for reconnect (URI -> server address only)
$connectedServerURI = $global:WhatsUpServerBaseURI
# Extract server address from URI: "https://192.168.74.74:9644" -> "192.168.74.74"
$connectedServerAddress = if ($connectedServerURI -match '://([^/:]+)') { $Matches[1] } else { $connectedServerURI }

Write-Host " Connected to $connectedServerURI" -ForegroundColor Green

# ============================================================================
# STEP 4: Create SNMP active monitor in WUG library
# ============================================================================

Write-Host ""
Write-Host "Creating SNMP monitors in WUG library..." -ForegroundColor Cyan

$stats = @{
    HealthCreated  = 0
    HealthSkipped  = 0
    HealthFailed   = 0
    DevicesCreated = 0
    DevicesFound   = 0
    AttrsUpdated   = 0
}

# ---- 4a. Deduplicate active monitors across all devices --------------------
$uniqueActiveMonitors = @{}
foreach ($key in @($devicePlan.Keys)) {
    $dev = $devicePlan[$key]
    foreach ($actItem in @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) {
        $actName = $actItem.Name
        if (-not $actName -or $uniqueActiveMonitors.ContainsKey($actName)) { continue }
        $uniqueActiveMonitors[$actName] = $actItem
    }
}

# ---- 4b. Get ALL active monitors in ONE call (like we did with devices) ------
$existingActiveNames = @{}  # name -> library ID
$allActiveMonitors = @()

try {
    # Fetch all active monitors without filter
    $allActiveMonitors = @(Get-WUGActiveMonitor -ErrorAction SilentlyContinue)
    Write-Verbose "Retrieved $($allActiveMonitors.Count) active monitors from library"
    
    # Build lookup by name
    foreach ($mon in $allActiveMonitors) {
        $monName = if ($mon.PSObject.Properties['name']) { $mon.name } else { $mon.TemplateName }
        if ($monName) {
            $monId = if ($mon.PSObject.Properties['id']) { $mon.id } elseif ($mon.PSObject.Properties['TemplateId']) { $mon.TemplateId } else { $null }
            if ($monId -and -not $existingActiveNames.ContainsKey($monName)) {
                $existingActiveNames[$monName] = $monId
            }
        }
    }
    Write-Verbose "Built active monitor lookup with $($existingActiveNames.Count) entries"
}
catch {
    Write-Warning "Could not retrieve active monitors: $_"
}

$toCreateActive = @($uniqueActiveMonitors.Keys | Where-Object { -not $existingActiveNames.ContainsKey($_) })
$stats.HealthSkipped = $uniqueActiveMonitors.Count - $toCreateActive.Count

# ---- 4c. Create missing active monitors (one at a time) ----------------------
if ($toCreateActive.Count -gt 0) {
    Write-Host " Creating $($toCreateActive.Count) new active monitor(s)..." -ForegroundColor DarkGray

    foreach ($actName in $toCreateActive) {
        try {
            # Try to create the monitor
            $monResult = Add-WUGActiveMonitor -Type SNMP `
                -Name $actName `
                -SnmpOID '1.3.6.1.4.1.9.9.513.1.1.1.1.38' `
                -SnmpCheckType 'constant' `
                -SnmpValue '1' `
                -Timeout 10 `
                -ErrorAction Stop
            
            # If we got a result, use it; otherwise assume the monitor exists by name
            if ($monResult) {
                $existingActiveNames[$actName] = $monResult
                $stats.HealthCreated++
                Write-Verbose "Created monitor '$actName' (ID: $monResult)"
            }
            else {
                # Monitor likely already existed (Add-WUGActiveMonitor skipped creation but returned $null)
                # Just use the name as the monitor identifier
                $existingActiveNames[$actName] = $actName
                $stats.HealthSkipped++
                Write-Verbose "Monitor '$actName' already exists or was skipped"
            }
        }
        catch {
            # If error mentions "already exists", still mark it as found
            if ($_ -match 'already exists|Skipping|Skip') {
                $existingActiveNames[$actName] = $actName
                $stats.HealthSkipped++
                Write-Verbose "Monitor '$actName' already exists (caught in error)"
            }
            else {
                Write-Warning "Failed to create monitor '$actName': $_"
                $stats.HealthFailed++
            }
        }
    }
}
Write-Host " Active monitors: $($stats.HealthCreated) created, $($stats.HealthSkipped) existing, $($stats.HealthFailed) failed" -ForegroundColor DarkGray

# ---- 4d. Final check: any monitors still not found? ------
$missingAct = @($uniqueActiveMonitors.Keys | Where-Object { -not $existingActiveNames.ContainsKey($_) })
if ($missingAct.Count -gt 0) {
    Write-Verbose "Re-loading active monitor library to find $($missingAct.Count) missing monitor(s)..."
    try {
        $allActiveMonitors = @(Get-WUGActiveMonitor -ErrorAction SilentlyContinue)
        foreach ($mon in $allActiveMonitors) {
            foreach ($actName in $missingAct) {
                if ($existingActiveNames.ContainsKey($actName)) { continue }
                $monName = if ($mon.PSObject.Properties['name']) { $mon.name } else { $mon.TemplateName }
                if ($monName -eq $actName) {
                    $monId = if ($mon.PSObject.Properties['id']) { $mon.id } elseif ($mon.PSObject.Properties['TemplateId']) { $mon.TemplateId } else { $null }
                    if ($monId) {
                        $existingActiveNames[$actName] = $monId
                    }
                }
            }
        }
    }
    catch { }
}

# ============================================================================
# STEP 5: Check for existing devices in WUG (bulk lookup - ONE API call)
# ============================================================================

Write-Host ""
Write-Host " Checking for existing devices..." -ForegroundColor Cyan

# ---- 5a. Get ALL WUG devices in ONE call (not per-device search) ------
$allWUGDevices = @()
try {
    $allWUGDevices = @(Get-WUGDevice -View 'basic' -ErrorAction Stop)
    Write-Verbose "Retrieved $($allWUGDevices.Count) devices from WUG"
}
catch {
    Write-Warning "Failed to retrieve device list from WUG: $_"
    $allWUGDevices = @()
}

# ---- 5b. Build local lookup hashtable (instant search, zero API calls) -----
$deviceLookup = @{}  # displayName -> id, hostName -> id

foreach ($wugDev in $allWUGDevices) {
    # Index by displayName
    if ($wugDev.displayName -and -not $deviceLookup.ContainsKey($wugDev.displayName)) {
        $deviceLookup[$wugDev.displayName] = $wugDev.id
    }
    # Index by hostName
    if ($wugDev.hostName -and -not $deviceLookup.ContainsKey($wugDev.hostName)) {
        $deviceLookup[$wugDev.hostName] = $wugDev.id
    }
    # Index by (AP) variant
    if ($wugDev.displayName) {
        $apVariant = "$($wugDev.displayName) (AP)"
        if (-not $deviceLookup.ContainsKey($apVariant)) {
            $deviceLookup[$apVariant] = $wugDev.id
        }
    }
}
Write-Verbose "Built device lookup table with $($deviceLookup.Count) entries"

# ---- 5c. Check each AP device (all lookups are now instant hashtable searches) -----
$wugDeviceMap    = @{}   # key -> deviceId (all devices)
$existingDevices = @{}   # key -> deviceId (already in WUG)
$newDeviceKeys   = [System.Collections.Generic.List[string]]::new()
$deviceKeys      = @($devicePlan.Keys)
$devTotal        = $deviceKeys.Count

foreach ($key in $deviceKeys) {
    $dev = $devicePlan[$key]
    $displayName = "$($dev.Name) (AP)"

    $deviceId = $null

    # Try multiple lookup keys (instant hashtable access - no API call)
    if ($deviceLookup.ContainsKey($dev.Name)) {
        $deviceId = $deviceLookup[$dev.Name]
    }
    elseif ($deviceLookup.ContainsKey($displayName)) {
        $deviceId = $deviceLookup[$displayName]
    }

    if ($deviceId) {
        $existingDevices[$key] = $deviceId
        $wugDeviceMap[$key] = $deviceId
        $stats.DevicesFound++
    }
    else {
        $newDeviceKeys.Add($key)
    }
}

Write-Host " Found $($stats.DevicesFound) existing, $($newDeviceKeys.Count) new to create" -ForegroundColor DarkGray

# ============================================================================
# STEP 6: Create new devices via Add-WUGDeviceTemplate
# ============================================================================

if ($newDeviceKeys.Count -gt 0) {
    Write-Host " Creating $($newDeviceKeys.Count) devices..." -ForegroundColor Yellow
    $devIdx = 0
    $devicesCreatedSinceReconnect = 0
    $reconnectInterval = 5  # Reconnect after every 5 devices to refresh token frequently

    foreach ($key in $newDeviceKeys) {
        $devIdx++
        $dev = $devicePlan[$key]
        $addIP = if ($dev.IP) { $dev.IP } else { '0.0.0.0' }
        $displayName = "$($dev.Name) (AP)"

        # Periodic token refresh: reconnect after every N devices
        if ($devicesCreatedSinceReconnect -ge $reconnectInterval) {
            Write-Verbose "Token refresh: Reconnecting to WUG after $reconnectInterval devices..."
            try {
                Disconnect-WUGServer -ErrorAction SilentlyContinue | Out-Null
                Start-Sleep -Milliseconds 500
                
                if ($WUGCredential) {
                    Connect-WUGServer -serverUri $connectedServerAddress -Credential $WUGCredential -ErrorAction Stop | Out-Null
                } else {
                    Connect-WUGServer -serverUri $connectedServerAddress -ErrorAction Stop | Out-Null
                }
                $devicesCreatedSinceReconnect = 0
                Write-Verbose "Session refreshed successfully"
            }
            catch {
                Write-Warning "Failed to refresh session: $_"
            }
        }

        Write-Progress -Activity 'Creating devices' `
            -Status "$devIdx / $($newDeviceKeys.Count) - $displayName" `
            -PercentComplete ([Math]::Round(($devIdx / $newDeviceKeys.Count) * 100))

        # Build attributes array
        $devAttrs = @()
        foreach ($attrName in $dev.Attrs.Keys) {
            $attrVal = $dev.Attrs[$attrName]
            if ($attrVal) { $devAttrs += @{ name = $attrName; value = "$attrVal" } }
        }

        # Collect unique active monitor names that exist in library
        $actNames = @()
        $seenActNames = @{}
        foreach ($actItem in @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) {
            if ($actItem.Name -and $existingActiveNames.ContainsKey($actItem.Name) -and -not $seenActNames.ContainsKey($actItem.Name)) {
                $actNames += $actItem.Name
                $seenActNames[$actItem.Name] = $true
            }
        }

        # Skip devices with no monitors to assign
        if ($actNames.Count -eq 0) {
            Write-Verbose "Skipping '$displayName' - no monitors to assign."
            continue
        }

        $devNote = "Cisco WLC AP (auto-created by CiscoWLC discovery)"

        $splat = @{
            displayName            = $displayName
            DeviceAddress          = $addIP
            Hostname               = $dev.Name
            Brand                  = 'Cisco'
            Note                   = $devNote
            NoDefaultActiveMonitor = $true
        }

        if ($actNames.Count -gt 0) { 
            $splat['ActiveMonitors'] = $actNames
        }
        if ($SNMPCredentialName) { $splat['CredentialSNMPv2'] = $SNMPCredentialName }

        try {
            $devResult = Add-WUGDeviceTemplate @splat -ErrorAction Stop

            # Detect error arrays: Add-WUGDeviceTemplate returns $result.data.errors
            # (array of objects with .templateId/.messages) on failure, or
            # $result.data (object with .idMap/.errors/.operations/.successful) on success.
            $isErrorResult = $false
            if ($devResult -is [array]) {
                # Error array returned — each element has .templateId and .messages
                $isErrorResult = $true
            } elseif ($devResult -and $devResult.PSObject.Properties['messages']) {
                # Single error object
                $isErrorResult = $true
            }

            if ($devResult -and -not $isErrorResult) {
                $newDeviceId = $null
                if ($devResult.idMap) {
                    $newDeviceId = ($devResult.idMap | Select-Object -First 1).resultId
                } elseif ($devResult.PSObject.Properties['resultId']) {
                    $newDeviceId = $devResult.resultId
                }
                if ($newDeviceId) {
                    $wugDeviceMap[$key] = $newDeviceId
                    $stats.DevicesCreated++
                    $devicesCreatedSinceReconnect++
                    Write-Verbose "Created device '$displayName' (ID: $newDeviceId)"
                } else {
                    Write-Warning "Device '$displayName' — API returned success but no device ID."
                }
            } else {
                $errMsgs = @()
                if ($devResult -is [array]) {
                    foreach ($e in $devResult) {
                        if ($e.PSObject.Properties['messages']) { $errMsgs += ($e.messages -join '; ') }
                    }
                } elseif ($devResult -and $devResult.PSObject.Properties['messages']) {
                    $errMsgs += ($devResult.messages -join '; ')
                }
                $errText = if ($errMsgs.Count -gt 0) { $errMsgs -join ' | ' } else { 'Unknown error (no details returned)' }
                
                # Detect token expiration (HTTP 500 errors may indicate expired token)
                if ($errText -match '500|Internal Server Error|token|authorization|auth|expired|invalid') {
                    Write-Warning "Detected possible token expiration. Attempting to refresh..."
                    try {
                        if ($WUGCredential) {
                            Connect-WUGServer -serverUri $connectedServerAddress -Credential $WUGCredential -ErrorAction Stop | Out-Null
                        } else {
                            Connect-WUGServer -serverUri $connectedServerAddress -ErrorAction Stop | Out-Null
                        }
                        Write-Host "Token refreshed. Retrying device creation..." -ForegroundColor Yellow
                        
                        # Retry the device creation
                        $devResult2 = $null
                        try {
                            $devResult2 = Add-WUGDeviceTemplate @splat -ErrorAction Stop
                            $isErrorResult2 = $devResult2 -is [array] -or ($devResult2 -and $devResult2.PSObject.Properties['messages'])
                            if ($devResult2 -and -not $isErrorResult2) {
                                $newDeviceId = if ($devResult2.idMap) { 
                                    ($devResult2.idMap | Select-Object -First 1).resultId 
                                } elseif ($devResult2.PSObject.Properties['resultId']) { 
                                    $devResult2.resultId 
                                }
                                if ($newDeviceId) {
                                    $wugDeviceMap[$key] = $newDeviceId
                                    $stats.DevicesCreated++
                                    $devicesCreatedSinceReconnect++
                                    Write-Verbose "Created device '$displayName' (ID: $newDeviceId) after token refresh"
                                }
                            }
                        }
                        catch {
                            Write-Warning "Retry failed for device '$displayName': $_"
                        }
                    }
                    catch {
                        Write-Warning "Failed to refresh token during error recovery: $_"
                    }
                } else {
                    Write-Warning "Failed to create device '$displayName': $errText"
                }
            }
        }
        catch {
            $errMsg = "$($_.Exception.Message)"
            Write-Warning "Error creating device '$displayName': $errMsg"
            
            # Detect token expiration (HTTP 500 errors may indicate expired token)
            if ($errMsg -match '500|Internal Server Error|token|authorization|auth|expired|invalid|Unauthorized') {
                Write-Host "HTTP 500 detected. Disconnecting and reconnecting to refresh session..." -ForegroundColor Yellow
                try {
                    # Full disconnect-reconnect cycle to clear bad session
                    Disconnect-WUGServer -ErrorAction SilentlyContinue | Out-Null
                    Start-Sleep -Milliseconds 500  # Brief pause to ensure disconnect completes
                    
                    if ($WUGCredential) {
                        Connect-WUGServer -serverUri $connectedServerAddress -Credential $WUGCredential -ErrorAction Stop | Out-Null
                    } else {
                        Connect-WUGServer -serverUri $connectedServerAddress -ErrorAction Stop | Out-Null
                    }
                    Write-Host "Session refreshed. Retrying device creation..." -ForegroundColor Green
                    
                    # Retry the device creation
                    try {
                        $devResult2 = Add-WUGDeviceTemplate @splat -ErrorAction Stop
                        $isErrorResult2 = $devResult2 -is [array] -or ($devResult2 -and $devResult2.PSObject.Properties['messages'])
                        if ($devResult2 -and -not $isErrorResult2) {
                            $newDeviceId = if ($devResult2.idMap) { 
                                ($devResult2.idMap | Select-Object -First 1).resultId 
                            } elseif ($devResult2.PSObject.Properties['resultId']) { 
                                $devResult2.resultId 
                            }
                            if ($newDeviceId) {
                                $wugDeviceMap[$key] = $newDeviceId
                                $stats.DevicesCreated++
                                $devicesCreatedSinceReconnect = 0  # Reset counter after successful reconnect
                                Write-Host "✓ Retry successful: Created device '$displayName' (ID: $newDeviceId)" -ForegroundColor Green
                            }
                        }
                    }
                    catch {
                        Write-Warning "Retry failed for device '$displayName': $($_.Exception.Message)"
                    }
                }
                catch {
                    Write-Warning "Failed to refresh session: $($_.Exception.Message)"
                }
            }
        }
    }

    Write-Progress -Activity 'Creating devices' -Completed
    Write-Host " Devices: $($stats.DevicesCreated) created" -ForegroundColor Green
}

# ============================================================================
# STEP 6b: Add WLC interface on new devices that have their own LAN IP
# ============================================================================

$newlyCreatedKeys = @($newDeviceKeys | Where-Object { $wugDeviceMap.ContainsKey($_) })
$devicesWithLanIP = @($newlyCreatedKeys | Where-Object {
    $d = $devicePlan[$_]
    $d.IP -ne $WLCAddress -and $d.WLCIP
})

if ($devicesWithLanIP.Count -gt 0) {
    Write-Host " Adding WLC interface to $($devicesWithLanIP.Count) device(s) with LAN IPs..." -ForegroundColor Cyan
    foreach ($key in $devicesWithLanIP) {
        $deviceId = $wugDeviceMap[$key]
        $dev = $devicePlan[$key]
        try {
            Add-WUGDeviceInterface -DeviceId $deviceId -Address $dev.WLCIP -HostName $dev.WLCIP -ErrorAction Stop | Out-Null
            Write-Verbose "Added WLC interface ($($dev.WLCIP)) to device $deviceId"
        }
        catch {
            Write-Verbose "Could not add WLC interface to device $deviceId`: $_"
        }
    }
}

# ============================================================================
# STEP 7: Assign monitors with per-AP SNMP instances & comments to new devices
# ============================================================================

if ($newlyCreatedKeys.Count -gt 0) {
    Write-Host " Assigning SNMP monitors with per-AP instances..."-ForegroundColor Cyan
    $monAssignIdx = 0

    foreach ($key in $newlyCreatedKeys) {
        $monAssignIdx++
        $deviceId = $wugDeviceMap[$key]
        $dev = $devicePlan[$key]
        $displayName = "$($dev.Name) (AP)"

        Write-Progress -Activity 'Assigning monitors to new devices' `
            -Status "$monAssignIdx / $($newlyCreatedKeys.Count) - $displayName" `
            -PercentComplete ([Math]::Round(($monAssignIdx / $newlyCreatedKeys.Count) * 100))

        # Assign active monitors with per-AP SNMP instance and comment
        foreach ($actItem in @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) {
            if ($actItem.Name -and $existingActiveNames.ContainsKey($actItem.Name)) {
                $monTemplateId = $existingActiveNames[$actItem.Name]
                $snmpInstance = if ($dev.SNMPInstance) { $dev.SNMPInstance } else { '1' }
                $monComment = "$($dev.APMac)"
                
                try {
                    $monAssignResult = Add-WUGActiveMonitorToDevice -DeviceId $deviceId -MonitorId $monTemplateId -Argument $snmpInstance -Comment $monComment -ErrorAction Stop
                    Write-Verbose "Assigned monitor '$($actItem.Name)' to device $deviceId with instance=$snmpInstance and comment"
                }
                catch {
                    if ($_.Exception.Message -notmatch 'already|assigned|exists|duplicate') {
                        Write-Warning "Failed to assign monitor '$($actItem.Name)' to device $deviceId`: $_"
                    }
                }
            }
        }
    }

    Write-Progress -Activity 'Assigning monitors to new devices' -Completed
}

# ============================================================================
# STEP 8: Set device attributes on newly created devices
# ============================================================================

if ($newlyCreatedKeys.Count -gt 0) {
    Write-Host " Setting device attributes on newly created devices..." -ForegroundColor Cyan
    $attrIdx = 0

    foreach ($key in $newlyCreatedKeys) {
        $attrIdx++
        $deviceId = $wugDeviceMap[$key]
        $dev = $devicePlan[$key]
        $displayName = "$($dev.Name) (AP)"

        Write-Progress -Activity 'Setting attributes on new devices' `
            -Status "$attrIdx / $($newlyCreatedKeys.Count) - $displayName" `
            -PercentComplete ([Math]::Round(($attrIdx / $newlyCreatedKeys.Count) * 100))

        foreach ($attrName in $dev.Attrs.Keys) {
            $attrVal = $dev.Attrs[$attrName]
            if ($attrVal) {
                try {
                    $null = Set-WUGDeviceAttribute -DeviceId $deviceId -Name $attrName -Value "$attrVal" -ErrorAction SilentlyContinue
                    $stats.AttrsUpdated++
                }
                catch {
                    Write-Verbose "Could not set attribute '$attrName' on device $deviceId`: $_"
                }
            }
        }
    }

    Write-Progress -Activity 'Setting attributes on new devices' -Completed
}

# ============================================================================
# STEP 9: Handle existing devices (credential + monitor assignment + attributes)
# ============================================================================

if ($existingDevices.Count -gt 0) {
    Write-Host " Updating $($existingDevices.Count) existing devices (credentials + monitors + attributes)..." -ForegroundColor Cyan
    $existIdx = 0

    foreach ($key in $existingDevices.Keys) {
        $existIdx++
        $deviceId = [int]$existingDevices[$key]
        $dev = $devicePlan[$key]
        $displayName = "$($dev.Name) (AP)"

        Write-Progress -Activity 'Updating existing devices' `
            -Status "$existIdx / $($existingDevices.Count) - $displayName" `
            -PercentComplete ([Math]::Round(($existIdx / $existingDevices.Count) * 100))

        # Assign SNMP credential if specified
        if ($SNMPCredentialName) {
            try {
                $credResult = @(Get-WUGCredential -Type snmpv2 -SearchValue $SNMPCredentialName)
                $credMatch = $credResult | Where-Object { $_.name -eq $SNMPCredentialName } | Select-Object -First 1
                if ($credMatch) {
                    $null = Set-WUGDeviceCredential -DeviceId $deviceId -CredentialId $credMatch.id -Assign
                }
            }
            catch {
                if ($_.Exception.Message -notmatch 'already|assigned|exists|duplicate') {
                    Write-Verbose "SNMP credential assign error for device $deviceId`: $_"
                }
            }
        }

        # Assign active monitors with per-AP SNMP instance and comment
        foreach ($actItem in @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' })) {
            if ($actItem.Name -and $existingActiveNames.ContainsKey($actItem.Name)) {
                $monTemplateId = $existingActiveNames[$actItem.Name]
                $snmpInstance = if ($dev.SNMPInstance) { $dev.SNMPInstance } else { '1' }
                $monComment = "$($dev.APMac)"
                
                try {
                    $monAssignResult = Add-WUGActiveMonitorToDevice -DeviceId $deviceId -MonitorId $monTemplateId -Argument $snmpInstance -Comment $monComment -ErrorAction Stop
                    Write-Verbose "Assigned monitor '$($actItem.Name)' to device $deviceId with instance=$snmpInstance and comment"
                }
                catch {
                    if ($_.Exception.Message -notmatch 'already|assigned|exists|duplicate') {
                        Write-Warning "Failed to assign monitor '$($actItem.Name)' to device $deviceId`: $_"
                    }
                }
            }
        }

        # Update device attributes
        foreach ($attrName in $dev.Attrs.Keys) {
            $attrVal = $dev.Attrs[$attrName]
            if ($attrVal) {
                try {
                    $null = Set-WUGDeviceAttribute -DeviceId $deviceId -Name $attrName -Value "$attrVal" -ErrorAction SilentlyContinue
                    $stats.AttrsUpdated++
                }
                catch {
                    Write-Verbose "Could not set attribute '$attrName' on device $deviceId`: $_"
                }
            }
        }
    }
    Write-Progress -Activity 'Updating existing devices' -Completed
}

# ============================================================================
# STEP 10: Summary
# ============================================================================

Write-Host ""
Write-Host "Push complete!" -ForegroundColor Green
Write-Host " Active monitors: $($stats.HealthCreated) created, $($stats.HealthSkipped) existing, $($stats.HealthFailed) failed" -ForegroundColor White
Write-Host " Devices: $($stats.DevicesCreated) created, $($stats.DevicesFound) existing" -ForegroundColor White
Write-Host " Attributes: $($stats.AttrsUpdated) set/updated" -ForegroundColor White
Write-Host ""
Write-Host "Done! Devices pushed to WhatsUp Gold." -ForegroundColor Green

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD8Hbzqi30uH4mX
# FT4tFaLhsNVZI2XMLvhnMEZTFUzPqqCCJQ0wggVvMIIEV6ADAgECAhBI/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
# BCAXqW08iHJNwz1YTAzA6Q/rBdTzWY3jKqUr25cqAFSRIjANBgkqhkiG9w0BAQEF
# AASCAgCC0YvGO3CLtAtICCOvZtH3EabAr0bfEdupbcVud1OT0ruo9by8xq6zZzAV
# pbXS+U3f/Pme+ikpn/bCSc6jot+rqevxX7zrfL1pckP5odRledu6wJijioVksl/2
# 8PWH95eAM+lDfaPT3E/HdlCrYFjMUxaDXjMtw3NgS/LwqoMzdhQVeoUnZEnNXRIT
# 0K7JUX+S84pDNB2Mkqu9AJs1RvrTO0d7Iv62JCoEQQcCAl2Rjd9fazgQAqzCr9bf
# WehyXVaYmaCu713YL0gql5qUBdfJQw0NX3CRMj/ofLeblIgV+tszJg/5mitEcchr
# jgUnU0qq+n+U41x+lKNWDgejQT5Ppb5hLUZmmTc0FeTfTx8ww6ka1x8LsxVOXD7q
# JVj5rsbo8lguI8XapHAJJh0W3a2Z5f4MgRG+vqYyK7x9m9c/uA8XTDhiDZHgqsG4
# 5gWae4SBbzMqf2OcF9Yj1Ra1Tv2wVZSIX9ZL+uPn29Pg5bHB8M2T2l2gE29ODvUL
# VKzc/wZ90fS+xxjhxfbnXwUvkSLIgLBRDW3GfFyJp+Gg9MlEwpyQrcCCG73PtY8R
# VOcV943wF2b/pA2/2VccwtNrqmQWjSjqwyFY1+00/ueuRzeIuAvheU5Qs879hYul
# 2RiaBEo9y6EeKEYLFuohza0z1NwMF5DTiSo+xRxPDcHdloXG2KGCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA3MTQwMDM5MTZaMC8GCSqGSIb3DQEJBDEiBCAzVPrH
# H50d4fY0I6sjJMw5JRiCqyOI7TrvM8/JeymgETANBgkqhkiG9w0BAQEFAASCAgCj
# dtSgDCBF9201QrzZfq3asEcjKMpddLzYbSPpj0nAJd7sxxBJfjXLmxovmwuFI1x0
# 9xr4vo06lhI1v1VFhqaUhRMymvy3wy/x3iEvYrJDWAwOQbrA9KfzhCAw+YaH5rPp
# rnYHSjE9xoxW/teo1kcgw1QvaGLQMnurhgDf3yD9WrN94xFcjasPcQx0mo9Ql34Z
# wiOMZDyfFN0Si282pEPY1r5IvL2qCbgvnBTd9WW2v28VKHvIcbG+5u0AxMG1ntkt
# T6fbSIW2i4GIWDF637jmqzcG64y0Nil4VG9B4Lmi57gE4yMitlOQ+47T2WhHDmlt
# ZfMzq+baKsH+u4GkFpKh4h/bAfjkF/lwhuZZhLN+VD7SMWEZ4IwwfARQsmVjQDsE
# 6EbYgSA9q9yy+EEeUXgB4HFxbhP+h8GFnsnVoOrnaZeA4OpRcd3OsrrNjW5UIo8/
# c6S6TGIM4JaEazmm22v2gSyqtXLpcbCe3Y4XC6EO4nSXUU9aEHlNwaxuEsm0+n6o
# bzGAdxzPFcyvW2FxGDAh1vq/mpRveU2kPiRuveGKBMq4FzmNTfCJVsxC64dxNd76
# 09vjdPXGoxDNITr6i00hRC0NCRsky/USMD+b4eoAeDGg259nHAJY4F50YP3o0Hg1
# s9dSEhapbynwdW6hUF8yoXYIMSUxRGG9vO/8fHrorQ==
# SIG # End signature block