helpers/discovery/Setup-CiscoWLC-Discovery.ps1

#requires -Version 5.1

<#
.SYNOPSIS
    Discover Cisco WLC wireless infrastructure via SNMP and optionally push monitors to WUG.
 
.DESCRIPTION
    Standalone wrapper for CiscoWLC discovery provider. Uses WhatsUpGoldPS.Snmp module
    to query Cisco LWAPP MIBs and generate comprehensive wireless network inventory:
 
      - Access Points (health, uptime, model, location)
      - Wireless Clients (SSID, AP mapping, authentication, device type)
      - WLANs (SSIDs, security profiles, radio policy)
      - System information and statistics
 
    Actions:
      PushToWUG Create or reuse WUG devices and sync monitors.
      ExportJSON Export discovery data as JSON files.
      ExportCSV Export discovery data as CSV files.
      ShowTable Display results in console table format.
      Dashboard Generate interactive HTML dashboards.
      DashboardAndPush Generate dashboards and push to WUG.
      None Run discovery but skip export/push (useful for testing).
 
.PARAMETER Target
    Array of WLC IP addresses or hostnames to discover.
 
.PARAMETER Action
    Action to perform after discovery. Valid values:
    'PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'ExportToWUG', 'None'
    ExportToWUG: Export previously discovered data to WUG without running discovery again.
 
.PARAMETER SnmpVersion
    SNMP version to use. Valid values: 1, 2, 3. Default: 2
 
.PARAMETER Community
    SNMPv1/v2c community string. Default: 'public'
 
.PARAMETER SnmpUsername
    SNMP v3 username (security name).
 
.PARAMETER SnmpContext
    SNMP v3 context.
 
.PARAMETER SnmpAuthProtocol
    SNMP v3 authentication protocol. Valid values: 'None', 'MD5', 'SHA', 'SHA1', 'SHA256', 'SHA384', 'SHA512'.
 
.PARAMETER SnmpAuthPassword
    SNMP v3 authentication password.
 
.PARAMETER SnmpPrivacyProtocol
    SNMP v3 privacy protocol. Valid values: 'None', 'DES', '3DES', 'TripleDES', 'AES', 'AES128', 'AES192', 'AES256'.
 
.PARAMETER SnmpPrivacyPassword
    SNMP v3 privacy password.
 
.PARAMETER SnmpPort
    SNMP port. Default: 161.
 
.PARAMETER TimeoutMs
    SNMP query timeout in milliseconds. Default: 10000 (10 seconds).
 
.PARAMETER Retries
    SNMP retry count. Default: 1.
 
.PARAMETER VaultName
    Optional DPAPI vault bundle name for SNMP settings. Default: CiscoWLC.Snmp.
 
.PARAMETER WUGServer
    WhatsUp Gold server IP or hostname. Default: 192.168.74.74.
 
.PARAMETER WUGCredential
    PSCredential for WhatsUp Gold authentication.
 
.PARAMETER DeviceGroupId
    Device group ID in WhatsUp Gold for discovered devices. Default: 0 (root group).
 
.PARAMETER PollingIntervalSeconds
    Polling interval in seconds for created monitors. Valid range: 60-86400. Default: 300.
 
.PARAMETER OutputPath
    Directory for exported discovery plans. Default: %LOCALAPPDATA%\WhatsUpGoldPS\DiscoveryHelpers\Output (for non-interactive or WUG push), or %TEMP% for interactive mode.
 
.PARAMETER NonInteractive
    Suppress prompts. Requires direct SNMP parameters or a saved vault bundle.
 
.EXAMPLE
    # Standalone discovery with SNMPv2c
    .\Setup-CiscoWLC-Discovery.ps1 -Target '192.168.75.33' -SnmpVersion 2 -Community 'public' -Action Dashboard
 
.EXAMPLE
    # Discover with SNMPv3
    .\Setup-CiscoWLC-Discovery.ps1 `
        -Target '10.0.0.100' `
        -SnmpVersion 3 `
        -SnmpUsername 'snmpuser' `
        -SnmpAuthProtocol 'SHA' `
        -SnmpAuthPassword 'authpass123' `
        -SnmpPrivacyProtocol 'AES128' `
        -SnmpPrivacyPassword 'privpass123' `
        -Action Dashboard
 
.EXAMPLE
    # Non-interactive with WUG push
    .\Setup-CiscoWLC-Discovery.ps1 -Target '192.168.75.33' -SnmpVersion 2 -Community 'public' -Action PushToWUG -NonInteractive
 
.EXAMPLE
    # Export previously discovered data to WUG (no discovery)
    .\Setup-CiscoWLC-Discovery.ps1 -Target '192.168.75.33' -Action ExportToWUG -WUGServer 192.168.74.74
 
.NOTES
    Author: Jason Alberino
    Requires: WhatsUpGoldPS.Snmp module, DiscoveryHelpers.ps1, DiscoveryProvider-CiscoWLC.ps1
    Optional: WhatsUpGoldPS module (for WUG integration), Export-DynamicDashboardHtml (for dashboards)
    Encoding: UTF-8 with BOM
#>


[CmdletBinding(SupportsShouldProcess)]
param(
    [string[]]$Target,

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

    [ValidateSet(1, 2, 3)]
    [int]$SnmpVersion = 2,

    [string]$Community = 'public',

    [string]$SnmpUsername,

    [string]$SnmpContext,

    [ValidateSet('None', 'MD5', 'SHA', 'SHA1', 'SHA256', 'SHA384', 'SHA512')]
    [string]$SnmpAuthProtocol = 'SHA',

    [string]$SnmpAuthPassword,

    [ValidateSet('None', 'DES', '3DES', 'TripleDES', 'AES', 'AES128', 'AES192', 'AES256')]
    [string]$SnmpPrivacyProtocol = 'AES128',

    [string]$SnmpPrivacyPassword,

    [int]$SnmpPort = 161,

    [int]$TimeoutMs = 10000,

    [int]$Retries = 1,

    [string]$VaultName = 'CiscoWLC.Snmp',

    [string]$WUGServer = '192.168.74.74',

    [PSCredential]$WUGCredential,

    [int]$DeviceGroupId = 0,

    [ValidateRange(60, 86400)]
    [int]$PollingIntervalSeconds = 300,

    [string]$OutputPath,

    [switch]$NonInteractive
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$script:InputSnmpVersionSpecified         = $PSBoundParameters.ContainsKey('SnmpVersion')
$script:InputSnmpAuthProtocolSpecified    = $PSBoundParameters.ContainsKey('SnmpAuthProtocol')
$script:InputSnmpPrivacyProtocolSpecified = $PSBoundParameters.ContainsKey('SnmpPrivacyProtocol')

$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
. (Join-Path $scriptDir 'DiscoveryHelpers.ps1')
. (Join-Path $scriptDir 'DiscoveryProvider-CiscoWLC.ps1')

# ============================================================================
# Helper: SecureString <-> plaintext (vault field I/O)
# ============================================================================
function ConvertTo-WLCPlainText {
    param($Value)
    if ($null -eq $Value) { return '' }
    if ($Value -is [System.Security.SecureString]) {
        $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Value)
        try { return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
        finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
    }
    return [string]$Value
}

function ConvertTo-WLCSecureString {
    param([string]$Value)
    if ([string]::IsNullOrEmpty($Value)) {
        $ss = New-Object System.Security.SecureString
        $ss.MakeReadOnly()
        return $ss
    }
    return (ConvertTo-SecureString -String $Value -AsPlainText -Force)
}

# ============================================================================
# Vault credential resolver for SNMP settings
# ============================================================================
function Get-WLCResolvedSnmpSettings {
    [CmdletBinding()]
    param()

    $saved = $null
    if ($VaultName) {
        try {
            $saved = Get-DiscoveryCredential -Name $VaultName -ErrorAction SilentlyContinue
        }
        catch { $saved = $null }
    }

    if ($saved) {
        $savedVersion = if ($saved.ContainsKey('SnmpVersion')) { [string]$saved['SnmpVersion'] } else { '2' }
        $savedLabel   = if ($savedVersion -eq '3') { 'SNMP v3' } else { "SNMP v$savedVersion" }

        # Auto-reset if caller explicitly requested a different SNMP version
        if ($script:InputSnmpVersionSpecified -and [int]$savedVersion -ne [int]$SnmpVersion) {
            $requestedLabel = if ($SnmpVersion -eq 3) { 'SNMP v3' } else { "SNMP v$SnmpVersion" }
            Write-Host " Vault: $VaultName" -ForegroundColor DarkGray
            Write-Host " Found: $savedLabel settings (switching to $requestedLabel)" -ForegroundColor Yellow
            Write-Host ' Removing old credential...' -ForegroundColor Yellow
            Remove-DiscoveryCredential -Name $VaultName -Confirm:$false -ErrorAction SilentlyContinue
            $saved = $null
        }
        elseif ($NonInteractive -or $Action) {
            Write-Host " Vault: $VaultName" -ForegroundColor DarkGray
            Write-Host " Found: $savedLabel settings" -ForegroundColor Green
            Write-Host ' Using existing credential.' -ForegroundColor Green
        }
        else {
            Write-Host " Vault: $VaultName" -ForegroundColor DarkGray
            Write-Host " Found: $savedLabel settings" -ForegroundColor Green

            $choice = Read-Host -Prompt ' Use existing? [Y]es / [R]eset / [N]o skip'
            switch -Regex ($choice) {
                '^[Rr]' {
                    Write-Host ' Removing old credential...' -ForegroundColor Yellow
                    Remove-DiscoveryCredential -Name $VaultName -Confirm:$false -ErrorAction SilentlyContinue
                    $saved = $null
                }
                '^[Nn]' {
                    Write-Host ' Skipped.' -ForegroundColor DarkGray
                    return $null
                }
                default { Write-Host ' Using existing credential.' -ForegroundColor Green }
            }
        }
    }

    # ---- Resolve each setting (explicit param wins over vault) ----
    $resolvedVersion = $SnmpVersion
    if ($saved -and $saved.ContainsKey('SnmpVersion') -and -not $script:InputSnmpVersionSpecified) {
        $resolvedVersion = [int]$saved['SnmpVersion']
    }

    $resolvedCommunity = $Community
    if ([string]::IsNullOrWhiteSpace($resolvedCommunity) -and $saved -and $saved.ContainsKey('Community')) {
        $resolvedCommunity = [string]$saved['Community']
    }

    $resolvedUsername = $SnmpUsername
    if ([string]::IsNullOrWhiteSpace($resolvedUsername) -and $saved -and $saved.ContainsKey('Username')) {
        $resolvedUsername = [string]$saved['Username']
    }

    $resolvedContext = $SnmpContext
    if ([string]::IsNullOrWhiteSpace($resolvedContext) -and $saved -and $saved.ContainsKey('Context')) {
        $resolvedContext = [string]$saved['Context']
    }

    $resolvedAuthProtocol = $SnmpAuthProtocol
    if ($saved -and $saved.ContainsKey('AuthProtocol') -and -not $script:InputSnmpAuthProtocolSpecified) {
        $resolvedAuthProtocol = [string]$saved['AuthProtocol']
    }

    $resolvedPrivacyProtocol = $SnmpPrivacyProtocol
    if ($saved -and $saved.ContainsKey('PrivacyProtocol') -and -not $script:InputSnmpPrivacyProtocolSpecified) {
        $resolvedPrivacyProtocol = [string]$saved['PrivacyProtocol']
    }

    $resolvedAuthPassword = ConvertTo-WLCPlainText $SnmpAuthPassword
    if ([string]::IsNullOrWhiteSpace($resolvedAuthPassword) -and $saved -and $saved.ContainsKey('AuthPassword')) {
        $resolvedAuthPassword = [string]$saved['AuthPassword']
    }

    $resolvedPrivacyPassword = ConvertTo-WLCPlainText $SnmpPrivacyPassword
    if ([string]::IsNullOrWhiteSpace($resolvedPrivacyPassword) -and $saved -and $saved.ContainsKey('PrivacyPassword')) {
        $resolvedPrivacyPassword = [string]$saved['PrivacyPassword']
    }

    # ---- Prompt for any missing required values ----
    if ($resolvedVersion -in @(1, 2) -and [string]::IsNullOrWhiteSpace($resolvedCommunity)) {
        if ($NonInteractive) {
            throw 'SNMP community is required for non-interactive SNMP v1/v2 discovery.'
        }
        $resolvedCommunity = ConvertTo-WLCPlainText (Read-Host -AsSecureString -Prompt 'SNMP read community')
    }

    if ($resolvedVersion -eq 3) {
        if ([string]::IsNullOrWhiteSpace($resolvedUsername)) {
            if ($NonInteractive) { throw 'SNMP v3 username is required for non-interactive discovery.' }
            $resolvedUsername = Read-Host -Prompt 'SNMP v3 username'
        }

        # Prompt for auth protocol if not explicitly specified and no vault value
        if (-not $script:InputSnmpAuthProtocolSpecified -and -not $saved -and -not $NonInteractive) {
            Write-Host ''
            Write-Host ' Authentication protocol:' -ForegroundColor Cyan
            Write-Host ' [1] SHA / SHA1 (default)' -ForegroundColor White
            Write-Host ' [2] SHA256' -ForegroundColor White
            Write-Host ' [3] SHA512' -ForegroundColor White
            Write-Host ' [4] MD5' -ForegroundColor White
            Write-Host ' [5] None (no authentication)' -ForegroundColor White
            $authChoice = Read-Host -Prompt ' Choice [default: 1]'
            $resolvedAuthProtocol = switch ($authChoice) {
                '2' { 'SHA256' }
                '3' { 'SHA512' }
                '4' { 'MD5' }
                '5' { 'None' }
                default { 'SHA' }
            }
        }

        if (-not [string]::IsNullOrWhiteSpace($resolvedAuthProtocol) -and
            $resolvedAuthProtocol -ne 'None' -and
            [string]::IsNullOrWhiteSpace($resolvedAuthPassword)) {
            if ($NonInteractive) { throw 'SNMP v3 auth password is required when SnmpAuthProtocol is set.' }
            $resolvedAuthPassword = ConvertTo-WLCPlainText (Read-Host -AsSecureString -Prompt 'SNMP v3 auth password')
        }

        # Prompt for privacy protocol if not explicitly specified and no vault value
        if (-not $script:InputSnmpPrivacyProtocolSpecified -and -not $saved -and -not $NonInteractive) {
            Write-Host ''
            Write-Host ' Privacy protocol:' -ForegroundColor Cyan
            Write-Host ' [1] AES / AES128 (default)' -ForegroundColor White
            Write-Host ' [2] AES256' -ForegroundColor White
            Write-Host ' [3] DES' -ForegroundColor White
            Write-Host ' [4] None (no encryption)' -ForegroundColor White
            $privChoice = Read-Host -Prompt ' Choice [default: 1]'
            $resolvedPrivacyProtocol = switch ($privChoice) {
                '2' { 'AES256' }
                '3' { 'DES' }
                '4' { 'None' }
                default { 'AES128' }
            }
        }

        if (-not [string]::IsNullOrWhiteSpace($resolvedPrivacyProtocol) -and
            $resolvedPrivacyProtocol -ne 'None' -and
            [string]::IsNullOrWhiteSpace($resolvedPrivacyPassword)) {
            if ($NonInteractive) { throw 'SNMP v3 privacy password is required when SnmpPrivacyProtocol is set.' }
            $resolvedPrivacyPassword = ConvertTo-WLCPlainText (Read-Host -AsSecureString -Prompt 'SNMP v3 privacy password')
        }
    }
    else {
        # v1/v2 -- clear v3-only fields
        $resolvedUsername        = ''
        $resolvedContext         = ''
        $resolvedAuthProtocol    = 'None'
        $resolvedAuthPassword    = ''
        $resolvedPrivacyProtocol = 'None'
        $resolvedPrivacyPassword = ''
    }

    $settings = [ordered]@{
        SnmpVersion      = $resolvedVersion
        Community        = $resolvedCommunity
        Username         = $resolvedUsername
        Context          = $resolvedContext
        AuthProtocol     = $resolvedAuthProtocol
        AuthPassword     = $resolvedAuthPassword
        PrivacyProtocol  = $resolvedPrivacyProtocol
        PrivacyPassword  = $resolvedPrivacyPassword
        Port             = $SnmpPort
        TimeoutMs        = $TimeoutMs
        Retries          = $Retries
    }

    # ---- Persist to vault (interactive mode only) ----
    if (-not $NonInteractive -and $VaultName) {
        $fields = [ordered]@{
            SnmpVersion = ConvertTo-WLCSecureString -Value ([string]$settings.SnmpVersion)
        }
        if ([int]$settings.SnmpVersion -in @(1, 2)) {
            if (-not [string]::IsNullOrWhiteSpace($settings.Community)) {
                $fields['Community'] = ConvertTo-WLCSecureString -Value $settings.Community
            }
        }
        else {
            if (-not [string]::IsNullOrWhiteSpace($settings.Username)) {
                $fields['Username'] = ConvertTo-WLCSecureString -Value $settings.Username
            }
            if (-not [string]::IsNullOrWhiteSpace($settings.Context)) {
                $fields['Context'] = ConvertTo-WLCSecureString -Value $settings.Context
            }
            $fields['AuthProtocol']    = ConvertTo-WLCSecureString -Value ([string]$settings.AuthProtocol)
            $fields['PrivacyProtocol'] = ConvertTo-WLCSecureString -Value ([string]$settings.PrivacyProtocol)
            if (-not [string]::IsNullOrWhiteSpace($settings.AuthPassword)) {
                $fields['AuthPassword'] = ConvertTo-WLCSecureString -Value $settings.AuthPassword
            }
            if (-not [string]::IsNullOrWhiteSpace($settings.PrivacyPassword)) {
                $fields['PrivacyPassword'] = ConvertTo-WLCSecureString -Value $settings.PrivacyPassword
            }
        }
        Save-DiscoveryCredential -Name $VaultName -Fields $fields -Description 'CiscoWLC SNMP settings' -Force | Out-Null
    }

    return $settings
}


if (-not $OutputPath) {
    if ($NonInteractive -or $Action -in @('PushToWUG', 'DashboardAndPush')) {
        $OutputPath = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output'
    }
    else {
        $OutputPath = $env:TEMP
    }
}
if (-not (Test-Path -LiteralPath $OutputPath)) {
    New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
}

# ============================================================================
# region Load Dependencies
# ============================================================================

# NonInteractive fallback: default to Dashboard if no Action specified
if ($NonInteractive -and -not $Action) { $Action = 'Dashboard' }

# Load dashboard generator for Dashboard/DashboardAndPush actions
$generateDashboard = $Action -in @('Dashboard', 'DashboardAndPush')
if ($generateDashboard) {
    $dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1'
    if (Test-Path $dynDashPath) {
        . $dynDashPath
    } else {
        Write-Warning "Dashboard generator not found at $dynDashPath"
        $generateDashboard = $false
    }
}

Write-Host "`n=== Cisco WLC Discovery ===" -ForegroundColor Cyan
Write-Host "Output Directory: $OutputPath`n" -ForegroundColor Yellow

# Create output subdirectories (needed for all modes)
$fullDir = Join-Path $OutputPath 'full'
$summaryDir = Join-Path $OutputPath 'summary'
$dashboardDir = Join-Path $summaryDir 'dashboards'

foreach ($dir in @($fullDir, $summaryDir, $dashboardDir)) {
    if (-not (Test-Path -LiteralPath $dir)) {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
    }
}

# ============================================================================
# region Discovery: Direct SNMP Walks
# ============================================================================

# Prompt for targets interactively if none provided
if (-not $Target -and -not $NonInteractive) {
    Write-Host 'Enter WLC IP address(es) or hostname(s) to discover.' -ForegroundColor Yellow
    Write-Host ' Separate multiple entries with commas (e.g., 10.0.0.1, 10.0.0.2)' -ForegroundColor DarkYellow
    $targetInput = Read-Host ' WLC Target(s)'
    if ($targetInput) {
        $Target = @($targetInput -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    }
    if (-not $Target -or $Target.Count -eq 0) {
        Write-Warning 'No targets specified. Exiting.'
        return
    }
}

if ($Target) {
    Write-Host "Mode: Direct SNMP bulk walks" -ForegroundColor Green
    Write-Host "Target: $($Target -join ', ')" -ForegroundColor Yellow

    # ---- Resolve SNMP credentials (vault or direct params) ----
    Write-Host "`nStep 0: Resolving SNMP credentials (vault: $VaultName)..." -ForegroundColor Cyan
    $snmpSettings = Get-WLCResolvedSnmpSettings
    if (-not $snmpSettings) {
        Write-Warning 'No SNMP credentials available. Exiting.'
        return
    }

    # Map numeric SnmpVersion (1, 2, 3) to version strings used by SNMP module (V1, V2, V3)
    $snmpVersionStr = @{ 1 = 'V1'; 2 = 'V2'; 3 = 'V3' }[[int]$snmpSettings.SnmpVersion]
    Write-Host "SNMP Version: $snmpVersionStr`n" -ForegroundColor Yellow

    # Load SNMP module
    Write-Host "Step 1: Loading SNMP module..." -ForegroundColor Cyan
    $snmpModulePath = Join-Path (Split-Path $scriptDir -Parent) 'snmp\WhatsUpGoldPS.Snmp\WhatsUpGoldPS.Snmp.psd1'
    Import-Module $snmpModulePath -Force -ErrorAction Stop
    Import-SharpSnmpLib -ErrorAction Stop | Out-Null
    Write-Host " [+] SNMP module loaded`n" -ForegroundColor Green

    # Define OID trees for discovery
    $oidTrees = @(
        @{ Name = 'wireless-sys'; OID = '1.3.6.1.4.1.9.9.618'; File = 'wireless-sys.jsonl'; Required = $false },
        @{ Name = 'wireless-ap'; OID = '1.3.6.1.4.1.9.9.513.1.1.1'; File = 'wireless-ap.jsonl'; Required = $true },
        @{ Name = 'wireless-dot11-client'; OID = '1.3.6.1.4.1.9.9.599.1.3.1'; File = 'wireless-dot11-client.jsonl'; Required = $true },
        @{ Name = 'wireless-wlan'; OID = '1.3.6.1.4.1.9.9.512.1.1.1'; File = 'wireless-wlan.jsonl'; Required = $true },
        @{ Name = 'wireless-dot11'; OID = '1.3.6.1.4.1.9.9.612'; File = 'wireless-dot11.jsonl'; Required = $false },
        @{ Name = 'wireless-rogue'; OID = '1.3.6.1.4.1.9.9.610'; File = 'wireless-rogue.jsonl'; Required = $false },
        @{ Name = 'wireless-rf'; OID = '1.3.6.1.4.1.9.9.778'; File = 'wireless-rf.jsonl'; Required = $false },
        @{ Name = 'wireless-mobility-ext'; OID = '1.3.6.1.4.1.9.9.846'; File = 'wireless-mobility-ext.jsonl'; Required = $false },
        @{ Name = 'wireless-airespace-client'; OID = '1.3.6.1.4.1.14179.2.1.4'; File = 'wireless-airespace-client.jsonl'; Required = $false },
        @{ Name = 'wireless-airespace-ap'; OID = '1.3.6.1.4.1.14179.2.2.1'; File = 'wireless-airespace-ap.jsonl'; Required = $false }
    )

    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'

    foreach ($wlcAddress in $Target) {
        Write-Host "Step 2: SNMP Bulk Walk for $wlcAddress" -ForegroundColor Cyan
        
        foreach ($tree in $oidTrees) {
            Write-Host " Walking $($tree.Name) ($($tree.OID))..." -ForegroundColor Gray
            
            try {
                $walkParams = @{
                    Target         = $wlcAddress
                    Table          = $tree.OID
                    MaxRepetitions = 25
                    Timeout        = $snmpSettings.TimeoutMs
                    ErrorAction    = 'Stop'
                }

                if ($snmpVersionStr -eq 'V3') {
                    $walkParams['Version']      = 'V3'
                    $walkParams['Username']     = $snmpSettings.Username
                    $walkParams['AuthProtocol'] = $snmpSettings.AuthProtocol
                    $walkParams['AuthPassword'] = $snmpSettings.AuthPassword
                    $walkParams['PrivProtocol'] = $snmpSettings.PrivacyProtocol
                    $walkParams['PrivPassword'] = $snmpSettings.PrivacyPassword
                    $walk = Invoke-SNMPBulkWalkFriendly @walkParams
                } else {
                    $walkParams['Version']   = $snmpVersionStr
                    $walkParams['Community'] = $snmpSettings.Community
                    $walk = Invoke-SNMPBulkWalk @walkParams
                }
                
                Write-Host " [+] Retrieved $($walk.Variables.Count) variables" -ForegroundColor Green
                
                # Export to JSONL
                $jsonlPath = Join-Path $fullDir $tree.File
                $writer = [System.IO.StreamWriter]::new($jsonlPath, $false, [System.Text.Encoding]::UTF8)
                try {
                    foreach ($var in $walk.Variables) {
                        $obj = @{
                            OID = [string]$var.Id
                            Value = [string]$var.Data
                            Type = $var.Data.GetType().Name
                        }
                        if ($var.Data -is [Lextm.SharpSnmpLib.OctetString]) {
                            $hexVal = $var.Data.ToHexString()
                            if (-not [string]::IsNullOrWhiteSpace($hexVal)) {
                                $obj['HexValue'] = $hexVal
                            }
                        }
                        $json = $obj | ConvertTo-Json -Compress
                        $writer.WriteLine($json)
                    }
                }
                finally {
                    $writer.Dispose()
                }
                
                Write-Host " [+] Exported to $($tree.File)" -ForegroundColor Green
            }
            catch {
                if ($tree.Required) {
                    Write-Host " [-] Failed: $_" -ForegroundColor Red
                    throw
                }
                else {
                    Write-Host " [!] Skipped (optional): $_" -ForegroundColor Yellow
                    $jsonlPath = Join-Path $fullDir $tree.File
                    '' | Out-File -FilePath $jsonlPath -Encoding UTF8
                }
            }
        }
    }

    # Step 3: Build summaries
    Write-Host "`nStep 3: Building summaries..." -ForegroundColor Cyan
    $buildScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Build-Wireless-Summaries.ps1'
    if (Test-Path $buildScript) {
        & $buildScript -InputDirectory $fullDir -OutputDirectory $summaryDir
        Write-Host " [+] Summaries built" -ForegroundColor Green
    }
    else {
        Write-Warning "Build-Wireless-Summaries.ps1 not found"
    }

    # Step 4: Post-discovery action
    # If no -Action was specified and not NonInteractive, show an interactive menu
    if (-not $Action -and -not $NonInteractive) {
        Write-Host ''
        Write-Host 'What would you like to do?' -ForegroundColor Cyan
        Write-Host ' [1] Generate HTML dashboards'
        Write-Host ' [2] Push AP devices to WhatsUp Gold'
        Write-Host ' [3] Dashboard + Push to WUG'
        Write-Host ' [4] Export inventory to JSON file'
        Write-Host ' [5] Exit (do nothing)'
        Write-Host ''
        $menuChoice = Read-Host -Prompt 'Choice [1-5]'
        switch ($menuChoice) {
            '1' { $Action = 'Dashboard' }
            '2' { $Action = 'PushToWUG' }
            '3' { $Action = 'DashboardAndPush' }
            '4' { $Action = 'ExportJSON' }
            '5' { $Action = 'None' }
            default { $Action = 'None' }
        }
        # Re-evaluate dashboard flag after menu selection
        $generateDashboard = $Action -in @('Dashboard', 'DashboardAndPush')
    }

    if ($generateDashboard) {
        Write-Host "`nStep 4: Generating dashboards..." -ForegroundColor Cyan
        $dashboardScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Export-Wireless-Dashboard-Pack.ps1'
        if (Test-Path $dashboardScript) {
            # Resolve repo root: scriptDir is helpers/discovery, go up to repo root
            $repoRoot = Split-Path (Split-Path $scriptDir -Parent) -Parent
            & $dashboardScript -SummaryDirectory $summaryDir -OutputDirectory $dashboardDir -WhatsUpGoldPsRepoPath $repoRoot
            Write-Host " [+] Dashboards generated" -ForegroundColor Green
            
            $indexPath = Join-Path $dashboardDir 'wireless-dashboard-index.html'
            if (Test-Path $indexPath) {
                Write-Host "`nDashboard: $indexPath" -ForegroundColor Yellow
                Write-Host "Opening in browser..." -ForegroundColor Gray
                Start-Process $indexPath
            }

            # Copy dashboards to WUG NmConsole if installed locally
            $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 }
                $dashboardFiles = @(Get-ChildItem $dashboardDir -Filter '*.html' -ErrorAction SilentlyContinue)
                foreach ($dashFile in $dashboardFiles) {
                    $destPath = Join-Path $wugDashDir $dashFile.Name
                    try {
                        Copy-Item -LiteralPath $dashFile.FullName -Destination $destPath -Force
                        Write-Host " Copied to WUG: $destPath" -ForegroundColor Green
                    }
                    catch {
                        Write-Warning "Could not copy dashboard to NmConsole (run as admin?): $_"
                    }
                }
                Deploy-DashboardWebConfig -Path $wugDashDir
                Write-Host " Access via WUG web UI: /NmConsole/dashboards/" -ForegroundColor Cyan
            }
        }
        else {
            Write-Warning "Export-Wireless-Dashboard-Pack.ps1 not found"
        }
    }

    Write-Host "`n=== Discovery Complete ===" -ForegroundColor Cyan
    Write-Host "Full data: $fullDir" -ForegroundColor Gray
    Write-Host "Summaries: $summaryDir" -ForegroundColor Gray
    if ($generateDashboard) {
        Write-Host "Dashboards: $dashboardDir" -ForegroundColor Gray
    }
}

# ============================================================================
# region WUG Integration Mode
# ============================================================================

if ($Action -in @('PushToWUG', 'DashboardAndPush')) {
    Write-Host "`nMode: WUG Integration (Push to WhatsUp Gold)" -ForegroundColor Green
    
    # For each WLC target, create AP devices in WUG
    foreach ($wlcAddress in $Target) {
        Write-Host "`nStep 5: Creating/updating AP devices in WUG for $wlcAddress..." -ForegroundColor Cyan
        
        $apExportScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Export-CiscoWLCAPDevicesToWUG.ps1'
        if (Test-Path $apExportScript) {
            $apSplat = @{
                InputDirectory = $fullDir
                WLCAddress = $wlcAddress
                WUGServer = "https://$WUGServer`:9644"
                DeviceGroupId = $DeviceGroupId
                PollingIntervalMinutes = [math]::Max(1, [math]::Floor($PollingIntervalSeconds / 60))
                Verbose = $VerbosePreference -ne 'SilentlyContinue'
            }
            
            if ($WUGCredential) {
                $apSplat['WUGCredential'] = $WUGCredential
            }
            
            try {
                & $apExportScript @apSplat
                Write-Host " [+] AP device export complete" -ForegroundColor Green
            }
            catch {
                Write-Warning "AP device export failed: $_"
            }
        }
        else {
            Write-Warning "AP device export script not found at $apExportScript"
        }
    }
}

# ============================================================================
# region ExportToWUG Mode (Export Only, No Discovery)
# ============================================================================

if ($Action -eq 'ExportToWUG') {
    Write-Host "`nMode: Export to WhatsUp Gold (No Discovery)" -ForegroundColor Green
    
    # Verify output directory exists with summary data
    if (-not (Test-Path $summaryDir)) {
        Write-Warning "Summary directory not found: $summaryDir"
        Write-Host "Run discovery first (e.g., with -Action 'Dashboard' or 'PushToWUG')" -ForegroundColor Yellow
        exit 1
    }
    
    # For each WLC target, create/update AP devices in WUG
    foreach ($wlcAddress in $Target) {
        Write-Host "`nExporting AP devices in WUG for $wlcAddress..." -ForegroundColor Cyan
        
        $apExportScript = Join-Path (Split-Path $scriptDir -Parent) 'cisco-wlc\Export-CiscoWLCAPDevicesToWUG.ps1'
        if (Test-Path $apExportScript) {
            $apSplat = @{
                InputDirectory = $summaryDir
                WLCAddress = $wlcAddress
                WUGServer = "https://$WUGServer`:9644"
                DeviceGroupId = $DeviceGroupId
                PollingIntervalMinutes = [math]::Max(1, [math]::Floor($PollingIntervalSeconds / 60))
                Verbose = $VerbosePreference -ne 'SilentlyContinue'
            }
            
            if ($WUGCredential) {
                $apSplat['WUGCredential'] = $WUGCredential
            }
            
            try {
                & $apExportScript @apSplat
                Write-Host " [+] AP device export complete" -ForegroundColor Green
            }
            catch {
                Write-Warning "AP device export failed: $_"
            }
        }
        else {
            Write-Warning "AP device export script not found at $apExportScript"
        }
    }
}

Write-Host "`n=== Discovery Complete ===" -ForegroundColor Cyan

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBEOso+DDjztn1L
# mwo6HSiESRvGvF5O42TKE0kwr0a1hqCCJQ0wggVvMIIEV6ADAgECAhBI/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
# BCD7WzxViXD4cS62nMsJKWj2wjdBW5CTZqZaq6uy4QyuITANBgkqhkiG9w0BAQEF
# AASCAgB4NWke5tRqDY2bILa/fsHtRPfnNp+VR8Q39BfsA5yWT/GRL+tHdlDJm7gF
# 3ujYCuQTRSv6dOFnJUTaWofNWJ5M3pnnqhSSMHtP9d10OnawqDUF2hRuvLafIO8+
# sKrT2UeIUmxiva1S78PEO1wMONvcvURMR5z86e74FDzAI+DV9D/quq/+/nV9L0cz
# 4vufe2NfUKOIcGI8k5FGT5sC1ZbdbE0S3hxNt3Z4VWWDFUU6NuIIDDNLxA8tE4Zw
# Rw1I85oXt3KueA1vbWBUSpKGAvXD24yd+1B+EYlUi6XAPMOlHFZ+Xd34TWRA29eG
# En/Xe9dlKUKXrvh1H8clIYJWLd0+RPIwTgAuHeQULgkeBqZnBJOhhqamyOv/2IbB
# 47Qv9UqBRgdc7D+Cx3OfVWwitTTUiHZuIvq/IxKoxgJn7lZaJLJ1ZjGVB0zJSGNd
# 1y5uupjzDfnBqy2iPaX1zJKyUkHqO4Yq40FKLhKIFzZpRKT/6quKamWw9+NyFptS
# dBYL3kE3IKjQasG3QC6RYUpHN1wlSMVHLyoM2VJqHNusxgm5Yv1haRVgGZjDXELX
# UvpejukvNyJ8KHUCJoeNjzegrszxx9vzxeEXgRIaeagco2rF4Ugzn675ZJdkUukJ
# A4rr39RLPnQY/epTLlj8SRUqmhZQaPWTPWvPmD1J0X5V269Hh6GCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA3MTQwMDE5MTBaMC8GCSqGSIb3DQEJBDEiBCB7l4fk
# 2mO1342AHnPBSjmIbkA47YrVn968nQen4OxUFzANBgkqhkiG9w0BAQEFAASCAgAd
# q4oGcdYSE6nxhckpJ2qZqqHSNfPCJojRD3Vr8t+J7AE0eo0ZmsUXFlfz4qDZCGM/
# 3EqO4NpW31MkKyrxnUxvgH4eyCTeTiPngeYFUWQLMbnW5yzkX4/oKQ9uKsUc9Qcx
# JipfXdzY6kJRxLByBqAUEXoDfmEG11PAudTrP1BhdkabDvvvDGtkGFv94AfH8AxX
# KJpV/2fbZEPf5r69R7iEW5DddFQFBW6KJq3mORcep/BxCOvD5xtxvQJX57NP064p
# 45kl8GEZx5E3HdOgrglrR2qQ7tcrzAOE7DcIA5aQnMJyv1Je/q6bQpbjhSIxeK0i
# KDNa0ib6yYJTEAwXxaEhU6m2m0wdoCQ7Y4PJDQQ60Z+BWhGRsgU15AqP7FrTkZRD
# 8Y7vJEA8narSkkjkPWlGAxh/aYH3cDhYiIwrUaQxyqAqe0YE8m3V3brZq9Y64aRQ
# Z2vc20xAsXzUYuZZm0rpM5FWfkUfEdhkvnz6KFf+gC4uaF62aKcvGvVv4kBUP7zx
# hDthsvS91VgPZ79vlMUbx+Ns9WwmRLUkijB63ytWrXYqh0dZBx5wh6cbpKzcuz8z
# JZAALc1PE6/+NgWn55bEMpsaNcIws5zWCWwLWMEI+jTsw3eBMKaaTSUPyOPbVt/l
# xXgke9QVEtSbk1lj5ETd8FS2OKiQOnZtShnplD0dOw==
# SIG # End signature block