Microsoft.AzLocal.CSSTools.psm1

#################################################################
# #
# Copyright (C) Microsoft Corporation. All rights reserved. #
# #
#################################################################

[CmdletBinding()]
param (
    [Parameter(Mandatory = $false, Position = 0)]
    [Bool]$SkipStartupActions = $false,

    [Parameter(Mandatory = $false, Position = 1)]
    [Bool]$SkipUpdateCheck = $false
)
$skipExplicitlySet = $PSBoundParameters.ContainsKey('SkipStartupActions')
[void]$PSBoundParameters.Remove('SkipStartupActions')
[void]$PSBoundParameters.Remove('SkipUpdateCheck')

# check if we are within a PSSession or runspace
# if we are and SkipStartupActions wasn't explicitly set, we skip the startup actions
if ($PSSenderInfo -and -not $skipExplicitlySet) {
    $SkipStartupActions = $true
    $SkipUpdateCheck = $true
}

#################################################################
# #
# STARTUP ACTIONS ON IMPORT #
# #
#################################################################

Import-LocalizedData -BindingVariable 'msg' -BaseDirectory "$PSScriptRoot\locale" -UICulture (Get-Culture) -WarningAction SilentlyContinue

$configurationData = Import-PowerShellDataFile -Path "$PSScriptRoot\config\Microsoft.AzLocal.CSSTools.Config.psd1"
New-Variable -Name 'CSSTools_AzsSupport' -Scope 'Global' -Force -Value @{
    Cache           = @{}
    Config          = $configurationData
    EnvironmentInfo = @{
        WindowsProductName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name 'ProductName' -ErrorAction Ignore).ProductName
        OSDisplayVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name 'DisplayVersion' -ErrorAction Ignore).DisplayVersion
        CloudName = [string]::Empty # this is set later
        DisconnectedOps = $false
        SolutionVersion = [string]::Empty # this is set later
        SolutionInstalled = $false # this is set later
        Platform = [string]::Empty # this is set later and will be HCI_OS or HCI_OS_AZLOCAL_SOLUTION
    }
    ModuleVersion   = $null
}

function Get-AzsSupportInstalledModuleVersion {
    <#
    .SYNOPSIS
        Returns the highest installed version of a module under the Windows PowerShell modules path.
    .DESCRIPTION
        Inspects $env:ProgramFiles\WindowsPowerShell\Modules\<Name> for version-named subfolders
        and returns the highest [System.Version], or $null when the module is not installed there.
    .PARAMETER Name
        The module name to inspect.
    .OUTPUTS
        System.Version. The highest installed version, or $null when none is found.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]$Name
    )

    $modulePath = Join-Path -Path (Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules') -ChildPath $Name
    if (-not (Test-Path -Path $modulePath -PathType Container)) {
        return $null
    }

    $highestVersion = $null
    foreach ($versionDirectory in (Get-ChildItem -Path $modulePath -Directory -ErrorAction Ignore)) {
        $parsedVersion = $null
        if ([System.Version]::TryParse($versionDirectory.Name, [ref]$parsedVersion)) {
            if ($null -eq $highestVersion -or $parsedVersion -gt $highestVersion) {
                $highestVersion = $parsedVersion
            }
        }
    }

    return $highestVersion
}

function Get-AzsSupportPackagePath {
    <#
    .SYNOPSIS
        Resolves the full filesystem path to a package bundled with Microsoft.AzLocal.CSSTools.
    .DESCRIPTION
        Reads PackageManifest.psd1, locates the requested package entry, and returns the full path to
        the highest shipped '<Package>.<Version>' folder under the package Root.
 
        This is primarily intended for packages flagged with Install = $false, which ship with the
        module but are NOT copied to the Windows PowerShell modules path (for example, to avoid
        interop issues with other features). The returned path can be passed directly to
        Import-Module so the specific bundled package can be loaded on demand.
    .PARAMETER Name
        The package name. Must match a Package value in the manifest.
    .PARAMETER ManifestPath
        Optional explicit path to the packages manifest. Defaults to PackageManifest.psd1 next to
        this module.
    .OUTPUTS
        System.String. The full path to the shipped package version folder, or $null when the
        package source folder or a versioned folder cannot be found.
    .EXAMPLE
        PS> Import-Module -Name (Get-AzsSupportPackagePath -Name 'SdnDiagnostics')
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [System.String]$ManifestPath
    )

    $resolvedManifestPath = $ManifestPath
    if ([System.String]::IsNullOrWhiteSpace($resolvedManifestPath)) {
        $resolvedManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'PackageManifest.psd1'
    }

    if (-not (Test-Path -Path $resolvedManifestPath -PathType Leaf)) {
        throw ("Unable to locate packages manifest '{0}'." -f $resolvedManifestPath)
    }

    $manifest = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath
    $manifestRoot = Split-Path -Path $resolvedManifestPath -Parent

    # Locate the requested package entry (case-insensitive match on the manifest Package value).
    $package = $manifest.Packages.Where({ $_.Package -ieq $Name }, 'First')[0]
    if ($null -eq $package) {
        throw ("Package '{0}' was not found in the packages manifest '{1}'." -f $Name, $resolvedManifestPath)
    }

    # Root is a path prefix relative to the manifest folder; the shipped folder is '<Root>.<Version>'
    # (for example packages\SdnDiagnostics.4.2604.29.191).
    $rootResolved = Join-Path -Path $manifestRoot -ChildPath $package.Root
    $searchParent = Split-Path -Path $rootResolved -Parent

    if (-not (Test-Path -Path $searchParent -PathType Container)) {
        Trace-Output -Level 'Warning' -Message ("Package source folder '{0}' for '{1}' was not found." -f $searchParent, $package.Package)
        return $null
    }

    # Discover the highest shipped '<Package>.<Version>' folder, deriving the version by stripping the
    # known package name prefix (handles variable-length version strings).
    $sourceDirectory = $null
    $sourceVersion = $null
    foreach ($candidate in (Get-ChildItem -Path $searchParent -Directory -Filter ("{0}.*" -f $package.Package) -ErrorAction Ignore)) {
        $versionString = $candidate.Name.Substring($package.Package.Length + 1)
        $parsedVersion = $null
        if ([System.Version]::TryParse($versionString, [ref]$parsedVersion)) {
            if ($null -eq $sourceVersion -or $parsedVersion -gt $sourceVersion) {
                $sourceVersion = $parsedVersion
                $sourceDirectory = $candidate
            }
        }
    }

    if ($null -eq $sourceDirectory) {
        Trace-Output -Level 'Warning' -Message ("No shipped '{0}.<Version>' folder was found under '{1}'." -f $package.Package, $searchParent)
        return $null
    }

    return $sourceDirectory.FullName
}

function Install-AzsSupportPackage {
    <#
    .SYNOPSIS
        Installs bundled packages declared in the central packages manifest into the Windows
        PowerShell modules path.
    .DESCRIPTION
        Reads PackageManifest.psd1 and, for every entry flagged with
        Install = $true, locates the shipped '<Package>.<Version>' folder under the package Root,
        derives the version by stripping the known package name prefix, and copies it to
        $env:ProgramFiles\WindowsPowerShell\Modules\<Package>\<Version>.
 
        The copy is performed only when the shipped version is newer than the highest version
        already installed (or when nothing is installed). When the same or a newer version is
        already installed the copy is skipped. Specify -Force to always overwrite the matching
        version.
 
        Entries flagged with Install = $false ship with the module but are not installed to the
        system modules path and are skipped.
    .PARAMETER Name
        Optional package name. When supplied, only the matching manifest entry is processed.
    .PARAMETER ManifestPath
        Optional explicit path to the packages manifest. Defaults to
        PackageManifest.psd1 next to this module.
    .PARAMETER Force
        Overwrites the matching installed version even when it already exists.
    .OUTPUTS
        System.String. The install path for each package that was installed or already present.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [System.String]$ManifestPath,

        [Parameter(Mandatory = $false)]
        [Switch]$Force
    )

    $resolvedManifestPath = $ManifestPath
    if ([System.String]::IsNullOrWhiteSpace($resolvedManifestPath)) {
        $resolvedManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'PackageManifest.psd1'
    }

    if (-not (Test-Path -Path $resolvedManifestPath -PathType Leaf)) {
        throw ("Unable to locate packages manifest '{0}'." -f $resolvedManifestPath)
    }

    $manifest = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath
    $manifestRoot = Split-Path -Path $resolvedManifestPath -Parent
    $modulesBasePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules'

    foreach ($package in $manifest.Packages) {
        # Honor an explicit package filter when supplied.
        if (-not [System.String]::IsNullOrWhiteSpace($Name) -and $package.Package -ine $Name) {
            continue
        }

        # Entries not flagged for installation ship with the module but are not copied to the
        # system modules path.
        if (-not $package.Install) {
            Trace-Output -Level 'Verbose' -Message ("Package '{0}' is not flagged for installation. Skipping." -f $package.Package)
            continue
        }

        try {
            # Root is a path prefix relative to the manifest (config) folder; the shipped folder is
            # '<Root>.<Version>' (for example packages\SdnDiagnostics.4.2604.29.191).
            $rootResolved = Join-Path -Path $manifestRoot -ChildPath $package.Root
            $searchParent = Split-Path -Path $rootResolved -Parent

            if (-not (Test-Path -Path $searchParent -PathType Container)) {
                Trace-Output -Level 'Warning' -Message ("Package source folder '{0}' for '{1}' was not found. Skipping installation." -f $searchParent, $package.Package)
                continue
            }

            # Discover the highest shipped '<Package>.<Version>' folder, deriving the version by
            # stripping the known package name prefix (handles variable-length version strings).
            $sourceDirectory = $null
            $sourceVersion = $null
            foreach ($candidate in (Get-ChildItem -Path $searchParent -Directory -Filter ("{0}.*" -f $package.Package) -ErrorAction Ignore)) {
                $versionString = $candidate.Name.Substring($package.Package.Length + 1)
                $parsedVersion = $null
                if ([System.Version]::TryParse($versionString, [ref]$parsedVersion)) {
                    if ($null -eq $sourceVersion -or $parsedVersion -gt $sourceVersion) {
                        $sourceVersion = $parsedVersion
                        $sourceDirectory = $candidate
                    }
                }
            }

            if ($null -eq $sourceDirectory) {
                Trace-Output -Level 'Warning' -Message ("No shipped '{0}.<Version>' folder was found under '{1}'. Skipping installation." -f $package.Package, $searchParent)
                continue
            }

            $installPath = Join-Path -Path (Join-Path -Path $modulesBasePath -ChildPath $package.Package) -ChildPath $sourceVersion.ToString()
            $installedVersion = Get-AzsSupportInstalledModuleVersion -Name $package.Package

            # Copy when forced, when nothing is installed, or when the shipped version is newer than
            # the highest installed version. Otherwise the current (same or newer) version is kept.
            $shouldCopy = $Force.IsPresent -or ($null -eq $installedVersion) -or ($sourceVersion -gt $installedVersion)
            if (-not $shouldCopy) {
                Trace-Output -Level 'Verbose' -Message ("Package '{0}' version '{1}' already satisfied by installed version '{2}'. Skipping copy." -f $package.Package, $sourceVersion, $installedVersion)
                $installPath
                continue
            }

            if (Test-Path -Path $installPath -PathType Container) {
                Trace-Output -Level 'Verbose' -Message ("Removing existing install for package '{0}' at '{1}'." -f $package.Package, $installPath)
                Remove-Item -Path $installPath -Recurse -Force -ErrorAction Stop
            }

            New-Item -Path $installPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
            Copy-Item -Path (Join-Path -Path $sourceDirectory.FullName -ChildPath '*') -Destination $installPath -Recurse -Force -ErrorAction Stop

            Trace-Output -Level 'Verbose' -Message ("Installed package '{0}' version '{1}' to '{2}'." -f $package.Package, $sourceVersion, $installPath)
            $installPath
        }
        catch {
            $_ | Trace-Exception
            Trace-Output -Level 'Warning' -Message ("Failed to install package '{0}'. {1}" -f $package.Package, $_.Exception.Message)
        }
    }
}

function Import-AzsSupportModule {
    <#
    .SYNOPSIS
        Imports the module dependencies declared in the central modules manifest.
    .DESCRIPTION
        Reads ModuleManifest.psd1 and imports each declared dependency
        by splatting its Parameters hashtable to Import-Module. When a Parameters entry defines
        RequiredVersion the specific version is loaded; otherwise the latest available version is
        used.
 
        Imports honor per-entry Import-Module parameters from the manifest (including
        ErrorAction). Failed imports are treated as optional by default (logged as warnings and
        loading continues). If a dependency entry sets Parameters.ErrorAction = 'Stop', a failed
        import is rethrown as a critical failure that stops module loading.
 
        When a dependency entry sets ImportFromBundle = $true, the module is imported from its
        bundled package location that ships with CSSTools (the shipped '<Package>.<Version>' folder
        resolved from PackageManifest.psd1 via Get-AzsSupportPackagePath) instead of by name. Use
        this for packages that are intentionally NOT installed to the Windows PowerShell modules
        path (Install = $false). A bundled dependency always takes precedence over a same-named
        module already present in the runspace (for example a native module shipped with the OS):
        the already-loaded short-circuit compares against the bundled version and force-reimports
        the bundled module when a different version is loaded, so the bundled version wins.
    .PARAMETER Name
        Optional dependency name (matching the manifest Package value). When supplied, only the
        matching entry is imported.
    .PARAMETER ModuleFile
        Optional leaf file name to import from the resolved, installed package folder instead of
        importing the module by name. Use this to import a module's .psm1 directly (for access to
        non-exported functions). Requires the package to be installed under the Windows PowerShell
        modules path.
    .PARAMETER ManifestPath
        Optional explicit path to the modules manifest. Defaults to
        ModuleManifest.psd1 next to this module.
    .PARAMETER Force
        Forces a reimport even when the module is already loaded.
    .PARAMETER Scope
        Optional override to control module import scope. When specified, overrides the Scope
        parameter from the manifest for the imported module(s). By default, the manifest setting
        is used. Use 'Local' to import into the local/script scope (restricting visibility of
        non-exported functions to the importing script).
    .OUTPUTS
        System.Management.Automation.PSModuleInfo. The imported module(s); nothing for entries that
        were skipped.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String]$Name,

        [Parameter(Mandatory = $false)]
        [System.String]$ModuleFile,

        [Parameter(Mandatory = $false)]
        [System.String]$ManifestPath,

        [Parameter(Mandatory = $false)]
        [Switch]$Force,

        [Parameter(Mandatory = $false)]
        [System.String]$Scope
    )

    $resolvedManifestPath = $ManifestPath
    if ([System.String]::IsNullOrWhiteSpace($resolvedManifestPath)) {
        $resolvedManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'ModuleManifest.psd1'
    }

    if (-not (Test-Path -Path $resolvedManifestPath -PathType Leaf)) {
        throw ("Unable to locate modules manifest '{0}'." -f $resolvedManifestPath)
    }

    $manifest = Import-PowerShellDataFile -LiteralPath $resolvedManifestPath
    $modulesBasePath = Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules'

    foreach ($dependency in $manifest.ModuleDependencies) {
        # Honor an explicit dependency filter when supplied.
        if (-not [System.String]::IsNullOrWhiteSpace($Name) -and $dependency.Package -ine $Name) {
            continue
        }

        # Build the Import-Module splat from the manifest Parameters.
        $importParams = @{}
        if ($null -ne $dependency.Parameters) {
            foreach ($key in $dependency.Parameters.Keys) {
                $importParams[$key] = $dependency.Parameters[$key]
            }
        }

        # Apply Scope override if explicitly specified by the caller.
        if (-not [System.String]::IsNullOrWhiteSpace($Scope)) {
            $importParams['Scope'] = $Scope
        }

        $moduleName = $importParams['Name']
        if ([System.String]::IsNullOrWhiteSpace($moduleName)) {
            $moduleName = $dependency.Package
            $importParams['Name'] = $moduleName
        }

        $errorAction = 'Continue'
        if ($importParams.ContainsKey('ErrorAction') -and -not [System.String]::IsNullOrWhiteSpace([System.String]$importParams['ErrorAction'])) {
            $errorAction = [System.String]$importParams['ErrorAction']
        }

        # A dependency is considered mandatory when the manifest requests terminating behavior
        # for import failures.
        $mustSucceed = ($errorAction -ieq 'Stop')

        $requiredVersion = $null
        $hasRequiredVersion = -not [System.String]::IsNullOrWhiteSpace([System.String]$importParams['RequiredVersion'])
        if ($hasRequiredVersion) {
            $requiredVersion = [System.Version]$importParams['RequiredVersion']
        }

        # When the dependency is imported by name from the standard module path (not from a bundled
        # package and not as a specific module file), confirm it is actually available before
        # attempting the import. Optional Azure Local solution components are absent on plain HCI OS,
        # and importing a missing module by name emits a non-terminating error that is not catchable
        # here. Skip quietly with a verbose trace so the module loads cleanly on systems without the
        # Azure Local solution installed. Mandatory dependencies (ErrorAction = Stop) are left to the
        # import attempt so their absence still surfaces as a terminating failure.
        if (-not $mustSucceed -and -not $dependency.ImportFromBundle -and [System.String]::IsNullOrWhiteSpace($ModuleFile)) {
            if (-not (Get-Module -Name $moduleName -ListAvailable -ErrorAction Ignore)) {
                Trace-Output -Level 'Verbose' -Message ($msg.dependencyModuleNotAvailable -f $moduleName)
                continue
            }

            # Some optional dependencies cannot initialize without other modules, declared via the
            # DependsOn array in ModuleManifest.psd1 (for example Support.AksArc requires MOC). Skip
            # the dependency when any required module is absent
            if ($null -ne $dependency.DependsOn) {
                $missingDependency = @($dependency.DependsOn).Where({ -not (Get-Module -Name $_ -ListAvailable -ErrorAction Ignore) }, 'First')[0]
                if (-not [System.String]::IsNullOrWhiteSpace($missingDependency)) {
                    Trace-Output -Level Warning -Message ($msg.dependencyModuleSkippedMissingDependency -f $dependency.Package, $missingDependency)
                    continue
                }
            }
        }

        try {
            # For bundled dependencies, resolve the shipped package path and derive its version up
            # front so the already-loaded short-circuit below can compare against the bundled
            # version. A bundled module must supersede any same-named module already present in the
            # runspace (for example a native SdnDiagnostics shipped with the OS), so this resolution
            # happens before the skip check rather than only at import time.
            $bundledPackagePath = $null
            $bundleVersion = $null
            if ($dependency.ImportFromBundle) {
                $bundledPackagePath = Get-AzsSupportPackagePath -Name $dependency.Package
                if ([System.String]::IsNullOrWhiteSpace($bundledPackagePath)) {
                    throw ("Bundled package '{0}' could not be resolved from the packages manifest; cannot import from the bundled location." -f $dependency.Package)
                }

                # The shipped folder is named '<Package>.<Version>'; derive the version by stripping
                # the known package-name prefix from the leaf folder name.
                $bundleLeaf = Split-Path -Path $bundledPackagePath -Leaf
                $bundleVersionString = $bundleLeaf.Substring($dependency.Package.Length + 1)
                $parsedBundleVersion = $null
                if ([System.Version]::TryParse($bundleVersionString, [ref]$parsedBundleVersion)) {
                    $bundleVersion = $parsedBundleVersion
                }
            }

            # Skip when the module is already loaded, unless a reimport is forced or a specific
            # module file is being imported. Bundled dependencies are compared against the bundled
            # version and force-reimported when a different version is loaded, so the bundled module
            # always supersedes a same-named module already present in the runspace.
            $forceBundleReimport = $false
            if (-not $Force -and [System.String]::IsNullOrWhiteSpace($ModuleFile)) {
                $loadedModules = @(Get-Module -Name $moduleName)
                if ($loadedModules.Count -gt 0) {
                    # For bundled dependencies, compare against the bundled version (RequiredVersion
                    # is ignored for bundle entries) rather than the manifest RequiredVersion.
                    $compareVersion = $requiredVersion
                    $hasCompareVersion = $hasRequiredVersion
                    if ($dependency.ImportFromBundle -and $null -ne $bundleVersion) {
                        $compareVersion = $bundleVersion
                        $hasCompareVersion = $true
                    }

                    if ($hasCompareVersion) {
                        if ($loadedModules.Where({ $_.Version -eq $compareVersion }, 'First')) {
                            Trace-Output -Level 'Verbose' -Message ("Module '{0}' required version '{1}' is already loaded. Skipping import." -f $moduleName, $compareVersion)
                            continue
                        }

                        $loadedVersions = ($loadedModules | Select-Object -ExpandProperty Version | ForEach-Object { $_.ToString() }) -join ', '
                        Trace-Output -Level 'Verbose' -Message ("Module '{0}' is loaded (version(s): {1}), but required version '{2}' is not loaded. Proceeding with import." -f $moduleName, $loadedVersions, $compareVersion)

                        # A same-named module from a different source/version is loaded; force the
                        # reimport so the bundled module supersedes it.
                        if ($dependency.ImportFromBundle) {
                            $forceBundleReimport = $true
                        }
                    }
                    elseif ($dependency.ImportFromBundle) {
                        # Bundled dependency whose shipped version could not be parsed; force the
                        # reimport so the bundled module still supersedes the already-loaded one.
                        $loadedVersions = ($loadedModules | Select-Object -ExpandProperty Version | ForEach-Object { $_.ToString() }) -join ', '
                        Trace-Output -Level 'Verbose' -Message ("Bundled module '{0}' is already loaded (version(s): {1}); forcing reimport so the bundled version is preferred." -f $moduleName, $loadedVersions)
                        $forceBundleReimport = $true
                    }
                    else {
                        Trace-Output -Level 'Verbose' -Message ("Module '{0}' is already loaded. Skipping import." -f $moduleName)
                        continue
                    }
                }
            }

            # When the dependency is flagged to import from the bundled package location, import from
            # the shipped '<Package>.<Version>' folder (resolved above) rather than by name. This
            # supports packages that are intentionally NOT installed to the Windows PowerShell
            # modules path (Install = $false in PackageManifest.psd1).
            if ($dependency.ImportFromBundle) {
                if (-not [System.String]::IsNullOrWhiteSpace($ModuleFile)) {
                    # An explicit module file was requested; target it within the bundled folder.
                    $importTarget = Join-Path -Path $bundledPackagePath -ChildPath $ModuleFile
                    if (-not (Test-Path -Path $importTarget -PathType Leaf)) {
                        throw ("Module file '{0}' was not found in bundled package folder '{1}' for module '{2}'." -f $ModuleFile, $bundledPackagePath, $moduleName)
                    }
                }
                else {
                    # The bundled folder is named '<Package>.<Version>', which does not match the module
                    # manifest name, so Import-Module cannot auto-discover the manifest from the folder.
                    # Resolve the actual module file: prefer the manifest (.psd1), then the script module
                    # (.psm1), matching the package name first and otherwise the only such file present.
                    $importTarget = $null
                    foreach ($extension in @('psd1', 'psm1')) {
                        $namedFile = Join-Path -Path $bundledPackagePath -ChildPath ("{0}.{1}" -f $moduleName, $extension)
                        if (Test-Path -Path $namedFile -PathType Leaf) {
                            $importTarget = $namedFile
                            break
                        }

                        $candidates = @(Get-ChildItem -Path $bundledPackagePath -Filter ("*.{0}" -f $extension) -File -ErrorAction Ignore)
                        if ($candidates.Count -eq 1) {
                            $importTarget = $candidates[0].FullName
                            break
                        }
                    }

                    if ([System.String]::IsNullOrWhiteSpace($importTarget)) {
                        throw ("No module manifest (.psd1) or script module (.psm1) was found in bundled package folder '{0}' for module '{1}'." -f $bundledPackagePath, $moduleName)
                    }
                }

                $importParams['Name'] = $importTarget
                $importParams.Remove('RequiredVersion')
            }
            # When a specific module file is requested, resolve it from the installed package folder
            # (RequiredVersion when declared, otherwise the highest installed version) and import
            # that file directly rather than importing the module by name.
            elseif (-not [System.String]::IsNullOrWhiteSpace($ModuleFile)) {
                $targetVersion = $null
                if ($hasRequiredVersion) {
                    $targetVersion = $requiredVersion
                }
                else {
                    $targetVersion = Get-AzsSupportInstalledModuleVersion -Name $moduleName
                }

                if ($null -eq $targetVersion) {
                    throw ("Module '{0}' is not installed under the Windows PowerShell modules path; cannot import module file '{1}'." -f $moduleName, $ModuleFile)
                }

                $packageFolder = Join-Path -Path (Join-Path -Path $modulesBasePath -ChildPath $moduleName) -ChildPath $targetVersion.ToString()
                $importTarget = Join-Path -Path $packageFolder -ChildPath $ModuleFile
                if (-not (Test-Path -Path $importTarget -PathType Leaf)) {
                    throw ("Module file '{0}' was not found in package folder '{1}' for module '{2}'." -f $ModuleFile, $packageFolder, $moduleName)
                }

                $importParams['Name'] = $importTarget
                $importParams.Remove('RequiredVersion')
            }

            $importParams['PassThru'] = $true
            if (-not $importParams.ContainsKey('ErrorAction')) {
                $importParams['ErrorAction'] = 'Continue'
            }
            if (-not $importParams.ContainsKey('DisableNameChecking')) {
                $importParams['DisableNameChecking'] = $true
            }
            # Force the reimport when the caller requested it, or when a bundled dependency must
            # supersede a different same-named module already loaded in the runspace.
            if ($Force -or $forceBundleReimport) {
                $importParams['Force'] = $true
            }

            # A bundled dependency is imported by path, but a same-named module already loaded by
            # name (for example a native SdnDiagnostics) is a distinct module to Import-Module, so
            # even -Force leaves the existing version loaded and both versions coexist. Explicitly
            # remove every already-loaded copy of the module by name first so the bundled version is
            # the only one left in the runspace.
            if ($forceBundleReimport) {
                Trace-Output -Level 'Verbose' -Message ("Removing already-loaded module '{0}' so the bundled version supersedes it." -f $moduleName)
                Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue
            }

            Trace-Output -Level 'Verbose' -Message ("Importing module '{0}'." -f $importParams['Name'])
            # Suppress verbose (4) and warning (3) streams emitted by the imported module on load.
            Import-Module @importParams 4> $null 3> $null
        }
        catch {
            if ($mustSucceed) {
                throw ("Failed to import module '{0}'. {1}" -f $moduleName, $_.Exception.Message)
            }

            $_ | Trace-Exception
            Trace-Output -Level 'Warning' -Message ("Failed to import module '{0}'. {1}" -f $moduleName, $_.Exception.Message)
        }
    }
}

# Ensure the module is always imported from an elevated (administrator) session, regardless of
# whether startup actions are skipped. Confirm-IsAdmin throws a localized error instructing the
# user to re-import as administrator when not elevated.
Confirm-IsAdmin

# Install bundled packages declared in the packages manifest into the Windows PowerShell modules
# path. This runs for all imports, including remote/runspace sessions, so that by-name imports
# below can resolve installed versions from the standard PSModulePath.
try {
    Install-AzsSupportPackage | Out-Null
}
catch {
    $_ | Trace-Exception
    Trace-Output -Level:Warning -Message ("Failed to install one or more bundled packages. {0}" -f $_.Exception.Message)
}

# Import the module dependencies declared in the modules manifest (always, regardless of startup
# actions) so dependent modules such as Microsoft.AzureStack.Lcm.PowerShell are available in both
# interactive and remote sessions. A failed import aborts module loading when that dependency sets
# ErrorAction = Stop in ModuleManifest.psd1; otherwise it is skipped with a warning.
Import-AzsSupportModule | Out-Null

# Determine whether the Azure Local solution is installed by probing for the solution-provided
# cmdlets. On plain HCI OS these are absent, and the module operates in a reduced HCI_OS mode. The
# result is cached on the global so Test-AzsSupportSolutionInstalled and other modules can read it.
$solutionInstalled = $true
foreach ($solutionCommand in @('Get-StampInformation', 'Get-SolutionUpdateEnvironment')) {
    if (-not (Get-Command -Name $solutionCommand -ErrorAction Ignore)) {
        $solutionInstalled = $false
        break
    }
}
$Global:CSSTools_AzsSupport.EnvironmentInfo.SolutionInstalled = $solutionInstalled
$Global:CSSTools_AzsSupport.EnvironmentInfo.Platform = if ($solutionInstalled) { 'HCI_OS_AZLOCAL_SOLUTION' } else { 'HCI_OS' }

# The solution update environment cmdlet is only available when the Azure Local solution is
# installed; query it only in that mode to avoid a CommandNotFoundException on HCI OS.
if ($solutionInstalled) {
    $currentVersion = (Get-SolutionUpdateEnvironment -ErrorAction Ignore).CurrentVersion
    if ($currentVersion) {
        $Global:CSSTools_AzsSupport.EnvironmentInfo.SolutionVersion = $currentVersion.ToString()
    }
}

#################################################################
# #
# ENUMS AND CLASSES #
# #
#################################################################

enum Component {
    OS
    AzureArcResourceBridge
}

#################################################################
# #
# FUNCTIONS #
# #
#################################################################

function Get-AzsSupportOSStampInformation {
    <#
    .SYNOPSIS
        Builds a stamp-information view for systems without the Azure Local solution.
    .DESCRIPTION
        On plain HCI OS the solution-provided Get-StampInformation cmdlet is unavailable. This helper
        synthesizes an ordered set of stamp-style properties from operating-system, computer, domain,
        cluster, and time-zone data. Properties that are only meaningful when the Azure Local solution
        is installed are returned as $null so consumers can render them as not-applicable.
    .OUTPUTS
        System.Collections.Specialized.OrderedDictionary. The synthesized stamp information.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param ()

    $environmentInfo = $Global:CSSTools_AzsSupport.EnvironmentInfo

    $computerSystem = $null
    try {
        $computerSystem = Get-CimInstance -ClassName 'Win32_ComputerSystem' -ErrorAction Stop
    }
    catch {
        $_ | Trace-Exception
    }

    $domainFqdn = $null
    if ($null -ne $computerSystem -and $computerSystem.PartOfDomain) {
        $domainFqdn = $computerSystem.Domain
    }

    # Determine the node count from the failover cluster when the feature is present; default to the
    # single local node otherwise.
    $numberOfNodes = 1
    if (Get-Command -Name 'Get-ClusterNode' -ErrorAction Ignore) {
        try {
            $clusterNodes = @(Get-ClusterNode -ErrorAction Stop)
            if ($clusterNodes.Count -gt 0) {
                $numberOfNodes = $clusterNodes.Count
            }
        }
        catch {
            $_ | Trace-Exception
        }
    }

    $timeZoneId = $null
    try {
        $timeZoneId = (Get-TimeZone -ErrorAction Stop).Id
    }
    catch {
        $_ | Trace-Exception
    }

    return [Ordered]@{
        Platform               = $environmentInfo.Platform
        WindowsProductName     = $environmentInfo.WindowsProductName
        OSDisplayVersion       = $environmentInfo.OSDisplayVersion
        ComputerName           = $env:COMPUTERNAME
        DomainFQDN             = $domainFqdn
        DomainNetBIOSName      = $env:USERDOMAIN
        NumberOfNodes          = $numberOfNodes
        TimeZone               = $timeZoneId
        RegionName             = $environmentInfo.Region
        DeploymentID           = $null
        OemVersion             = $null
        StampVersion           = $null
        ServicesVersion        = $null
        PlatformVersion        = $null
        InitialDeployedVersion = $null
        NetworkSchemaVersion   = $null
        CloudID                = $null
    }
}

function Get-AzsSupportStampInformation {
    <#
    .SYNOPSIS
        Gets common stamp information
    .DESCRIPTION
        Queries for common stamp information properties such as DeploymentID, OEMVersion and CloudID.
        On plain HCI OS, where the Azure Local solution is not installed, an equivalent view is
        synthesized from operating-system, computer, and cluster data instead.
    .EXAMPLE
        PS> Get-AzsSupportStampInformation
    .OUTPUTS
        Outputs the Stamp Information
    #>


    [CmdletBinding()]
    param()

    # On plain HCI OS the Azure Local solution - and its Get-StampInformation cmdlet - is not present.
    # Synthesize an equivalent view so callers and the entry banner still receive a populated object.
    if (-not (Test-AzsSupportSolutionInstalled)) {
        return Get-AzsSupportOSStampInformation
    }

    try {
        $stampInfo = Get-StampInformation -ErrorAction Stop
        $stampInformation = [Ordered]@{
            DeploymentID           = $stampInfo.DeploymentID
            OemVersion             = $stampInfo.OemVersion
            StampVersion           = $stampInfo.StampVersion
            ServicesVersion        = $stampInfo.ServicesVersion
            PlatformVersion        = $stampInfo.PlatformVersion
            InitialDeployedVersion = $stampInfo.InitialDeployedVersion
            NetworkSchemaVersion   = $stampInfo.NetworkSchemaVersion
            Prefix                 = $stampInfo.Prefix
            CompanyName            = $stampInfo.CompanyName
            ServerSku              = $stampInfo.ServerSku
            Topology               = $stampInfo.Topology
            TimeZone               = $stampInfo.TimeZone
            HardwareOEM            = $stampInfo.HardwareOEM
            RegionName             = $stampInfo.RegionName
            DomainNetBIOSName      = $stampInfo.DomainNetBIOSName
            DomainFQDN             = $stampInfo.DomainFQDN
            NumberOfNodes          = $stampInfo.NumberOfNodes
            CloudID                = $stampInfo.CloudID
            RingName               = $stampInfo.RingName
            InstallationMethod     = $stampInfo.InstallationMethod
            HardwareClass          = $stampInfo.HardwareClass
        }
    }
    catch {
        $_ | Write-Error
    }

    return $stampInformation
}

function Get-ModuleVersion {
    $manifest = Test-ModuleManifest -Path "$PSScriptRoot\Microsoft.AzLocal.CSSTools.psd1"
    $Global:CSSTools_AzsSupport.ModuleVersion = $manifest.Version.ToString()
    return $manifest.Version.ToString()
}

function Get-ModuleUpdateStatus {
    try {
        $galleryModule = Find-Module -Name 'Microsoft.AzLocal.CSSTools' -Repository 'PSGallery' -ErrorAction Stop
        $localVersion = [version]$Global:CSSTools_AzsSupport.ModuleVersion
        $galleryVersion = [version]$galleryModule.Version

        if ([version]$galleryVersion -gt [version]$localVersion) {
            return @{
                UpdateAvailable = $true
                CurrentVersion  = $localVersion.ToString()
                LatestVersion   = $galleryVersion.ToString()
            }
        }
    }
    catch {
        Trace-Output -Level:Warning -Message "Unable to check for module updates: $_"
    }

    return @{ UpdateAvailable = $false }
}

function Test-AzsSupportSolutionInstalled {
    <#
    .SYNOPSIS
        Determines whether the Azure Local solution is present on the current system.
    .DESCRIPTION
        Returns the cached platform state recorded on the CSSTools_AzsSupport global during module
        import: $false when running on plain HCI OS, $true when the Azure Local solution is also
        installed. When the global has not yet been populated - for example when this function is
        called in isolation before the main module has finished loading - it falls back to a live
        probe for the solution-provided cmdlets so the result is still correct.
    .PARAMETER Terminating
        If specified, the function will throw a terminating error if the solution is not installed.
    .OUTPUTS
        System.Boolean. $true when the Azure Local solution is installed; otherwise $false.
    .EXAMPLE
        PS> Test-AzsSupportSolutionInstalled
        True
    #>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory=$false)]
        [switch]$Terminating
    )

    $environmentInfo = $Global:CSSTools_AzsSupport.EnvironmentInfo
    if ($null -ne $environmentInfo -and $environmentInfo.ContainsKey('SolutionInstalled')) {
        if (-not $environmentInfo.SolutionInstalled -and $Terminating) {
            throw New-Object System.NotSupportedException($msg.errSolutionNotInstalled)
        }

        return [System.Boolean]$environmentInfo.SolutionInstalled
    }

    # The global state has not been populated yet; probe for the solution-provided cmdlets directly.
    $solutionInstalled = $true
    foreach ($command in @('Get-StampInformation', 'Get-SolutionUpdateEnvironment')) {
        if (-not (Get-Command -Name $command -ErrorAction Ignore)) {
            $solutionInstalled = $false
            break
        }
    }

    if (-not $solutionInstalled -and $Terminating) {
        throw New-Object System.NotSupportedException($msg.errSolutionNotInstalled)
    }

    return $solutionInstalled
}

function Get-EntryText {
@'
 
#################################################################################
                _ _ _
               / \ _____ _ _ __ ___ | | ___ ___ __ _| |
              / _ \ |_ / | | | '__/ _ \ | | / _ \ / __/ _` | |
             / ___ \ / /| |_| | | | __/ | |__| (_) | (_| (_| | |
            /_/ \_\/___|\__,_|_| \___| |_____\___/ \___\__,_|_|
 
#################################################################################
 
Provide feedback or suggestions to azlocaldiagfeedback@microsoft.com.
 
Tool Tips:
    List CSSTools commands: Get-Command -Module Microsoft.AzLocal.CSSTools
    Get help with examples: Get-Help 'Verb-Command' -Full
    Check for common issues: Invoke-AzsSupportInsight
    Check for new updates: Find-Module -Name Microsoft.AzLocal.CSSTools
    Install latest version: Update-Module -Name Microsoft.AzLocal.CSSTools -Force
 
Clean up the working directory after you are done to reclaim disk space:
    Clear-AzsSupportDirectory
 
'@
 | Write-Host -ForegroundColor:Green

    # display primary stamp info properties
    $stampInfo = Get-AzsSupportStampInformation
    if ($stampInfo) {
        $stampInfo += @{
            CSSToolsVersion = $Global:CSSTools_AzsSupport.ModuleVersion
            Region          = $Global:CSSTools_AzsSupport.EnvironmentInfo.Region
            DisconnectedOps = $Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps
        }

        $maxKeyLength = ($stampInfo.Keys | Measure-Object -Property Length -Maximum).Maximum
        $stampInfo.Keys | ForEach-Object {
            $key = $_.PadRight($maxKeyLength)
            $value = "N/A"
            if (![string]::IsNullOrEmpty($stampInfo[$_])) {
                $value = $stampInfo[$_]
            }

            # Print aligned output
            "{0} --> {1}" -f $key, $value | Write-Host -ForegroundColor:Gray
        }
    }
    else {
        $msg.stampInfoNotAvailable | Write-Warning
    }
    "" | Write-Host

    # display remediation history if available
    if ($Global:AzStack_Insights.RemediationHistory.Count -gt 0) {
        "Remediation History Location: $($Global:AzStack_Insights.RemediationHistoryFolder)" | Write-Host -ForegroundColor:Cyan
        "Remediation History:" | Write-Host -ForegroundColor:Cyan
        $Global:AzStack_Insights.RemediationHistory | Format-Table -Property RemediationScript, Name, TimeStamp, Status, ChangesApplied -AutoSize | Out-String | Write-Host -ForegroundColor:Gray
    }
}

function New-AzsSupportDataBundle {
    <#
        .SYNOPSIS
        Creates a support data bundle for Azure Stack HCI.
        .DESCRIPTION
        This function collects various types of diagnostic data from an Azure Stack HCI cluster and compiles it into a support data bundle. The data collected can include cluster commands, node commands, events, registry information, and specific folders. The function supports both automatic data collection based on predefined components and manual data collection based on user-specified parameters.
        .PARAMETER Component
        Specifies the Azure Stack HCI component for which to automatically collect data. Valid values are defined in the Component enum.
        .PARAMETER ClusterCommands
        An array of cluster-level commands to run and include in the data bundle.
        .PARAMETER NodeCommands
        An array of node-level commands to run on each cluster node and include in the data bundle.
        .PARAMETER NodeEvents
        An array of event log queries to run on each cluster node and include in the data bundle.
        .PARAMETER NodeRegistry
        An array of registry paths to query on each cluster node and include in the data bundle.
        .PARAMETER NodeFolders
        An array of folder paths to collect from each cluster node and include in the data bundle.
        .PARAMETER ComputerName
        An array of computer names (cluster nodes) on which to run the specified commands and collect data. If not specified, defaults to all cluster nodes.
        .EXAMPLE
        PS C:\> New-AzsSupportDataBundle -Component "OS"
        Automatically collects a predefined set of diagnostic data related to the operating system component of Azure Stack HCI and compiles it into a support data bundle.
        .EXAMPLE
        PS C:\> New-AzsSupportDataBundle -NodeCommands @("Get-Process", "Get-Service") -ComputerName @("Node01", "Node02")
        Collects the output of "Get-Process" and "Get-Service" commands from Node01 and Node02 and includes it in the support data bundle.
        .NOTES
        This function requires administrative privileges to run. The collected data may contain sensitive information, so ensure that the support data bundle is handled securely and shared only with authorized personnel.
    #>


    [CmdletBinding()]
    param (
        # AUTOMATIC DATA COLLECTION SPECS
        [Parameter(ParameterSetName = 'DataCollectAuto')]
        [Component] $Component,

        # MANUAL DATA COLLECTION SPECS
        [Parameter(ParameterSetName = 'DataCollectManual')]
        [array] $ClusterCommands,
        [Parameter(ParameterSetName = 'DataCollectManual')]
        [array] $NodeCommands,
        [Parameter(ParameterSetName = 'DataCollectManual')]
        [array] $NodeEvents,
        [Parameter(ParameterSetName = 'DataCollectManual')]
        [array] $NodeRegistry,
        [Parameter(ParameterSetName = 'DataCollectManual')]
        [array] $NodeFolders,
        [Parameter(ParameterSetName = 'DataCollectManual')]
        [array] $ComputerName
    )

    Trace-Output -Level:Information -Message $msg.startingNewDataBundle
    $runtime = Register-CommandRuntime

    switch ($PSCmdlet.ParameterSetName.ToLower()) {
        "datacollectmanual" {

            if(($NodeCommands -or $NodeEvents -or $NodeRegistry -or $NodeFolders) -and (-Not $ComputerName)) {
                Trace-Output -Level:Error -Message $msg.errComputerNameNotSupplied
            } else {
                Invoke-DataCollection `
                    -runtime            $runtime `
                    -clusterCommands    $ClusterCommands `
                    -nodeCommands       $NodeCommands `
                    -nodeEvents         $NodeEvents `
                    -nodeRegistry       $NodeRegistry `
                    -nodeFolders        $NodeFolders `
                    -ComputerName       $ComputerName
            }

        }

        "datacollectauto" {
            Trace-Output -Level:Information -Message $msg.startingAutomaticCollection
            Invoke-AutoDataCollection -runtime $runtime -Component $Component
        }

        Default {}
    }

}

function Invoke-DataCollection() {
    param(
        [string] $runtime,
        [array] $ClusterCommands,
        [array] $NodeCommands,
        [array] $NodeEvents,
        [array] $NodeRegistry,
        [array] $NodeFolders,
        [array] $ComputerName
    )

    Trace-Output -Level:Information -Message $msg.startingDataCollection
    $name = "SupportDataBundle"

    # important default information we want to always collect.
    $NodeCommands = $NodeCommands + @("Get-ChildItem env:*", "gpresult /h STORAGE_DEST/gpresultoutput.html")

    # manually collecting data
    Collect-SupportData `
        -runtime            $runtime `
        -clusterCommands    $ClusterCommands `
        -nodeCommands       $NodeCommands `
        -nodeEvents         $NodeEvents `
        -nodeRegistry       $NodeRegistry `
        -nodeFolders        $NodeFolders `
        -ComputerName       $ComputerName `
        -customName         $name
}

function Invoke-AutoDataCollection() {
    param(
        [string] $runtime,
        [Component] $Component
    )
    $storage = Get-WorkingDirectory
    $storageTemp = ('{0}\temp' -f $storage)

    if((Test-Path -path $storageTemp) -eq $false) {
        New-Item -Type:Directory -Path $storageTemp | Out-Null
    }

    Trace-Output -Level:Verbose -Message $Component

    switch ($Component) {
        ([Component]::OS) {
            # Automatic OS log collection
            $cmd = Get-Command -Name "Send-DiagnosticData" -ErrorAction Ignore

            if($cmd) {
                Collect-SupportData `
                    -nodeCommands @('Send-DiagnosticData -SaveToPath STORAGE_DEST -FromDate ((Get-Date).AddDays(-1)) -ToDate (Get-Date) -CollectSddc:$true') `
                    -ComputerName (Get-ClusterNode)
            } else {
                Trace-Output -Level:Error -Message $msg.errSendDiagDataNotAvailable
            }
        }
        ([Component]::AzureArcResourceBridge) {
            Trace-Output -Level:Verbose -Message "Starting Resource Bridge Data Collection"
            Trace-Output -Level:Information -Message $msg.arcApplianceLogStart
            $destPath = ("{0}\SupportDataBundle-{1}" -f $storage, (Get-Date -Format "HH-mm_dd-MM-yyyy"))

            $azTenant = Read-Host -Prompt $msg.questionTenantLogin

            Trace-Output -Level:Information -Message $msg.arcApplianceLogConfig
            # get environment information
            $azStackInfo = Get-AzureStackHCI
            $azStackUri = $azStackInfo.AzureResourceUri.split("/")
            $azSub = $azStackUri[2] # xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx
            $azReg = $azStackUri[4] # sample-rg

            $arcHciConfigObject = Get-ArcHciConfig
            $arcHciConfigPath = ("{0}/hci-resource.yaml" -f $arcHciConfigObject.workingDir)
            $arcHciConfig = Get-Content -Path $arcHciConfigPath
            $arcHciConfig = $arcHciConfig.trim()
            $arcHciConfigApplianceName = ""


            foreach($arcHciConfigEntry in $arcHciConfig) {
                if($arcHciConfigEntry.indexOf("name:") -ne -1){
                    $arcHciConfigApplianceName = $arcHciConfigEntry.split(":")[1].trim()
                }
            }

            Trace-Output -Level:Information -Message $msg.arcApplianceLogLogin
            az login --tenant $azTenant --use-device-code
            az account set --subscription $azSub

            Trace-Output -Level:Information -Message $msg.arcApplianceLogCollectionStart
            az arcappliance get-credentials -g $azReg -n $arcHciConfigApplianceName --overwrite-existing --credentials-dir $storageTemp

            Get-ChildItem -path $storageTemp | ForEach-Object {
                Icacls $storageTemp /c /t /Inheritance:d                | Out-Null
                Icacls $storageTemp /c /t /Grant ${env:UserName}:F      | Out-Null
                TakeOwn /F $storageTemp                                 | Out-Null
                Icacls $storageTemp /c /t /Grant:r ${env:UserName}:F    | Out-Null
                Icacls $storageTemp /c /t /Remove:g Administrator "Authenticated Users" BUILTIN\Administrators BUILTIN Everyone System Users | Out-Null
            } | Out-Null

            az arcappliance logs hci `
            --cloudagent $arcHciConfigObject.cloudFqdn `
            --credentials-dir $storageTemp `
            --ip $arcHciConfigObject.controlPlaneIP `
            --kubeconfig ("{0}\kubeconfig" -f $arcHciConfigObject.workingDir) `
            --loginconfigfile ("{0}\kvatoken.tok" -f $arcHciConfigObject.workingDir) `
            --out-dir $destPath

            if($destPath) {
                $destZipPath = ("{0}.zip" -f $destPath)
                Compress-Archive -Path $destPath -DestinationPath ("{0}.zip" -f $destZipPath)
                Remove-Item -Path $destPath -Recurse -Force
                Trace-Output -Level:Success -Message ($msg.CollectionDataEnd -f $destZipPath)
            } else {
                Trace-Output -Level:Error -Message $msg.arcApplianceLogCollectionError
            }

            Remove-Item -Path $storageTemp -Recurse -Force
        }
        Default {
            Trace-Output -Level:Error -Message $msg.errComponentNotFound
        }
    }
}

function Confirm-AzsSupportOSVersion {
    <#
    .SYNOPSIS
        Validates the OS version against a specified version or minimum version.
    .DESCRIPTION
        This function checks the current OS version against a specified version or minimum version.
        It throws an error if the current OS version does not match the specified version or is below the minimum version.
    .PARAMETER Version
        The exact OS version to confirm against the current OS version.
    .PARAMETER MinimumVersion
        The minimum OS version that the current OS version must meet or exceed.
    .EXAMPLE
        PS> Confirm-AzsSupportOSVersion -Version "23H2"
    .OUTPUTS
        System.Void
 
        This cmdlet does not return objects. It throws an error if the OS version check fails.
    #>

    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ConfirmOSVersion')]
        [ValidateSet("22H2", "23H2", "24H2", "25H2")]
        [string]$Version,

        [Parameter(Mandatory = $true, ParameterSetName = 'ConfirmMinimumOSVersion')]
        [ValidateSet("22H2", "23H2", "24H2", "25H2")]
        [string]$MinimumVersion
    )

    $osOrder = @("22H2", "23H2", "24H2", "25H2")
    $currentVersion = $Global:CSSTools_AzsSupport.EnvironmentInfo.OSDisplayVersion

    switch ($PSCmdlet.ParameterSetName) {
        'ConfirmOSVersion' {
            if ($currentVersion -ne $Version) {
                throw ($msg.osNotSupported -f $Version)
            }
        }
        'ConfirmMinimumOSVersion' {
            $minIndex = $osOrder.IndexOf($MinimumVersion)
            $curIndex = $osOrder.IndexOf($currentVersion)
            if ($curIndex -lt 0) {
                throw ($msg.osNotSupportedGeneric)
            }
            if ($curIndex -lt $minIndex) {
                throw ($msg.osNotSupported -f $MinimumVersion)
            }
        }
    }
}

function Install-AzsSupportModule {
    <#
    .SYNOPSIS
        Install the AzsSupport Module to remote computers if not installed or version mismatch.
    .DESCRIPTION
        This function checks if the Microsoft.AzLocal.CSSTools module is installed on the specified remote computers and if the version matches the local version. If the module is not installed or there is a version mismatch, it copies the module from the local computer to the remote computer. It also has an option to force the installation, which will copy the module regardless of the current state on the remote computer.
    .PARAMETER ComputerName
        Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers.
    .PARAMETER Credential
        Specifies a user account that has permission to perform this action. The default is the current user.
        Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password.
    .PARAMETER Force
        Forces a cleanup and re-install of the module on the remote computer.
    .EXAMPLE
        PS> Install-AzsSupportModule -ComputerName @('Node01', 'Node02')
 
        Checks each remote computer for the installed module version and copies the local module if missing or outdated.
 
    .EXAMPLE
        PS> $cred = Get-Credential
        PS> Install-AzsSupportModule -ComputerName 'Node01' -Credential $cred -Force
 
        Forces a reinstall of the module on the remote computer by copying the local module regardless of remote version.
 
    .OUTPUTS
        System.Void
 
        This cmdlet does not return objects. It writes progress and status/error messages.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String[]]$ComputerName,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    begin {
        $moduleName = 'Microsoft.AzLocal.CSSTools'
        $moduleDirpath = (Split-Path -Path (Get-Module -Name $moduleName).Path -Parent)

        # if we have multiple modules installed on the current workstation,
        # abort the operation because side by side modules can cause some interop issues to the remote nodes
        $localModule = Get-Module -Name $moduleName
        if ($localModule.Count -gt 1) {
            throw ($msg.installSupportModuleMultipleVersions -f $moduleName)
        }

        $getModuleVersionSB = {
            param ([string]$arg0)
            try {
                # Get the latest version of Microsoft.AzLocal.CSSTools Module installed
                $version = (Get-Module -Name $arg0 -ListAvailable -ErrorAction Ignore | Sort-Object Version -Descending)[0].Version.ToString()
            }
            catch {
                # in some instances, the module will not be available and as such we want to skip the noise and return
                # a string back to the remote call command which we can do proper comparison against
                $version = '0.0.0.0'
            }
            return $version
        }

        # typically PowerShell modules will be installed in the following directory configuration:
        # $env:ProgramFiles\WindowsPowerShell\Modules\{module}\{version}
        # $env:USERPROFILE\Documents\WindowsPowerShell\Modules\{module}\{version}
        # so we default to Leaf of the path being {module} as PSGet will handle the versioning so we only ever do import in the following format:
        # Import-Module {module} (if using default PowerShell module path)
        # Import-Module C:\{path}\{module} (if using custom PowerShell module path)
        # so we need to ensure that we are copying the module to the correct path on the remote computer
        [System.String]$destinationPathDir = $moduleDirpath
    }
    process {
        $totalComputers = $ComputerName.Count
        $currentComputer = 0
        
        $ComputerName | ForEach-Object {
            $computer = $_
            $currentComputer++
            $percentComplete = [math]::Min([math]::Round(($currentComputer / $totalComputers) * 100, 0), 99)
            
            Write-Progress -Activity "Installing $moduleName" -Status "Processing $computer ($currentComputer of $totalComputers)" -PercentComplete $percentComplete -Id 1
            
            try {
                # check to see if the computer is local, if so, we will skip the operation
                if (Test-ComputerNameIsLocal -ComputerName $computer) {
                    ($msg.installSupportModuleSkipUpdate -f $computer) | Trace-Output
                    return # exit the current iteration of the loop and move to the next computer
                }

                if (!$Force) {
                    Write-Progress -Activity "Installing $moduleName" -Status "Checking version on $computer" -PercentComplete $percentComplete -CurrentOperation "Getting installed version" -Id 1
                    ($msg.installSupportModuleGetVersion -f $computer, $moduleName) | Trace-Output

                    # use Invoke-Command here, as we do not want to create a cached session for the remote computers
                    # as it will impact scenarios where we need to import the module on the remote computer for remote sessions
                    try {
                        $invokeParams = @{
                            ComputerName = $computer
                            ScriptBlock = $getModuleVersionSB
                            ArgumentList = @($moduleName)
                            ErrorAction = 'Stop'
                        }
                        if ($Credential -ne [System.Management.Automation.PSCredential]::Empty -and $null -ne $Credential) {
                            $invokeParams.Add('Credential', $Credential)
                        }
                        $remoteModuleVersion = Invoke-Command @invokeParams
                    }
                    catch {
                        # if we are unable to connect to the remote computer, we will skip the operation
                        $_ | Trace-Exception
                        ($msg.installSupportModuleUnableToConnect -f $computer) | Trace-Output
                        return # exit the current iteration of the loop and move to the next computer
                    }

                    if ($remoteModuleVersion) {
                        # if the remote module version is greater or equal to the local module version, then we do not need to update
                        ($msg.installSupportModuleVersionCheck -f $computer, $remoteModuleVersion, $localModule.Version.ToString()) | Trace-Output

                        if ([version]$remoteModuleVersion -ge [version]$localModule.Version) {
                            return # exit the current iteration of the loop and move to the next computer
                        }
                    }
                }

                Write-Progress -Activity "Installing $moduleName" -Status "Updating $computer" -PercentComplete $percentComplete -CurrentOperation "Copying module files (version $($localModule.Version))" -Id 1
                ($msg.installSupportModuleUpdateStart -f $computer, $moduleName, $localModule.Version.ToString()) | Trace-Output
                Copy-FileToRemoteComputer -Path $localModule.ModuleBase -ComputerName $computer -Destination $destinationPathDir -Credential $Credential -Recurse -Force

                Write-Progress -Activity "Installing $moduleName" -Status "Cleaning up $computer" -PercentComplete $percentComplete -CurrentOperation "Removing PS sessions" -Id 1
                # ensure that we destroy the current pssessions for the computer to prevent any caching issues
                # we will want to remove any existing PSSessions for the remote computers
                Remove-AzsSupportPSSession -ComputerName $computer
            }
            catch {
                $_ | Trace-Exception
                $_ | Write-Error
            }
        }
    }
    end {
        Write-Progress -Activity "Installing $moduleName" -Completed -Id 1
    }
}

function Invoke-AzsSupportScript {
    <#
    .SYNOPSIS
        Invokes an AzsSupport script.
    .DESCRIPTION
        This function is used to execute a script located in the scripts directory of the AzsSupport module.
    .PARAMETER ScriptName
        The name of the script to execute.
    .PARAMETER Parameters
        A hashtable of parameters to pass to the script.
    .EXAMPLE
        Invoke-AzsSupportScript -ScriptName "MyScript" -Parameters @{Param1 = "Value1"; Param2 = "Value2"}
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
            $possibleValues = Get-ChildItem -Path $PSScriptRoot\scripts -Filter "*.ps1" -ErrorAction Stop | Select-Object -ExpandProperty BaseName
            if ([string]::IsNullOrEmpty($wordToComplete)) {
                return $possibleValues | Sort-Object
            }
            return $possibleValues | Where-Object { $_ -like "*$wordToComplete*" } | Sort-Object
        })]
        [string]$ScriptName,

        [Parameter(Mandatory = $false)]
        [hashtable]$Parameters
    )

    $scriptPath = Join-Path -Path $PSScriptRoot -ChildPath "scripts\$ScriptName.ps1"
    if (-not (Test-Path -Path $scriptPath)) {
        throw "Script '$ScriptName' not found at path: $scriptPath"
    }

    try {
        "Executing script: {0}" -f $ScriptName | Trace-Output
        if ($Parameters) {
            "Passing arguments: {0}" -f ($Parameters | Out-String) | Trace-Output
            & $scriptPath @Parameters
        }
        else {
            & $scriptPath
        }
    }
    catch {
        $_ | Trace-Exception
        "Error occurred while executing script: {0}" -f $ScriptName | Trace-Output -Level:Error
        throw $_
    }
}

#################################################################
# #
# SECONDARY STARTUP ACTIONS ON IMPORT #
# #
#################################################################

$null = Get-ModuleVersion

# The Azure Connected Machine (Arc) agent is independent of the Azure Local solution and can be
# present on plain HCI OS as well. Gate the probe on the agent's actual availability - not on
# SolutionInstalled - so the region is resolved on Arc-enabled HCI OS systems too. The catch still
# covers the case where azcmagent is installed but the invocation fails (agent stopped or not yet
# onboarded).
try {
    if (Get-Command -Name 'azcmagent' -ErrorAction Ignore) {
        $cloudData = azcmagent show -j | ConvertFrom-Json
        if ($cloudData) {
            $Global:CSSTools_AzsSupport.EnvironmentInfo.Region = $cloudData.location

            if ($cloudData.location -notin $Global:CSSTools_AzsSupport.Config.AzureRegions) {
                $Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps = $true
            }
        }
    }
}
catch {
    Trace-Output -Level:Warning -Message $msg.azcmagentFailed
}

# print the entry text if we are not skipping the startup actions
if (!$SkipStartupActions) {
    Get-EntryText

    # check for module updates from PSGallery unless skipped or in a remote session
    if (-not $SkipUpdateCheck) {
        $updateStatus = Get-ModuleUpdateStatus
        if ($updateStatus.UpdateAvailable) {
            Write-Host "" # just a spacer line
            Write-Host "A newer version of Microsoft.AzLocal.CSSTools is available: v$($updateStatus.LatestVersion) (installed: v$($updateStatus.CurrentVersion))." -ForegroundColor Yellow
            Write-Host "To update, remove the current module from your session and install the latest version:" -ForegroundColor Yellow
            Write-Host "`tRemove-Module -Name Microsoft.AzLocal.CSSTools -Force" -ForegroundColor Yellow
            Write-Host "`tUpdate-Module -Name Microsoft.AzLocal.CSSTools -Force" -ForegroundColor Yellow
            Write-Host "`tImport-Module -Name Microsoft.AzLocal.CSSTools -Force" -ForegroundColor Yellow
            Write-Host "" # just a spacer line
        }
    }

    Write-Host "" # just a spacer line
}

Set-Location -Path (Get-AzsSupportWorkingDirectory)

# Remove any existing PSSessions that were created by this module
# This is to ensure that we do not have any stale sessions that could cause issues
# when running commands in the module.
Remove-AzsSupportPSSession

# if we are running in disconnected setup, we want to import the AzStack.Disconnected module into the global runspace
# to expose unique functions related to management of disconnected AzLocal systems
if ($Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps) {
    $disconnectedModulePath = Join-Path -Path $PSScriptRoot -ChildPath "Modules\AzStack.Disconnected"
    if (Test-Path -Path $disconnectedModulePath) {
        Import-Module -Name $disconnectedModulePath -Force -ErrorAction Ignore -Scope Global
    }
}

#################################################
# DO NOT EDIT BELOW THIS LINE #
#################################################
# SIG # Begin signature block
# MIInNwYJKoZIhvcNAQcCoIInKDCCJyQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD9VmTk75T3MQtJ
# mvRNDMz9/NBuEdl5Y/4HAxAoFk8TJqCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghnEMIIZwAIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIFuPaW8qGlDe1RVrcm3VUojFCjHY
# 8UhYRWAc0DW1bC12MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv
# AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE
# ggEABm7ePqp+6/fUrwSdojS+c5weexvksWT0QTeK1giUuGjhbBAHqb2RYk2ZKNDC
# DXfJfixz67s6xfJcFn6m+ZfZ/QM0fUL2cjI+hOdELwG2+vo5SxN9zzKdNi5y5m9y
# zP+RwBz9KTB4VikxZBu3IkEPSq9I285dUFRfP8PT9HsVnNPYhgmTgohGNSA6lq7I
# Oa+mF86rtR3hSGf41iNBMvXop9zo28J8uXx0Qqptz4yknhKojm4plNaeItI8eGx0
# 3BKTmPVZzrZP/loSAcUWZsGUHI4SV14PYoLPMy8ftPXmBHes/14w97mDEW+YuS9A
# g2UxLBBh9m7HuxttoB5Cz8ozWqGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCCF3wG
# CSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsqhkiG
# 9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC
# AQUABCDTYRcshsw9cPsOJiS1wRDEUj5Qj0lsto5bTrsIPhB5OgIGal9aBddhGBMy
# MDI2MDcyOTEzMTc1MS4xMDFaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj
# YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzcwMy0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghHq
# MIIHIDCCBQigAwIBAgITMwAAAh86cGnkojAulQABAAACHzANBgkqhkiG9w0BAQsF
# ADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNjAyMTkxOTM5NTFa
# Fw0yNzA1MTcxOTM5NTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScw
# JQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzcwMy0wNUUwLUQ5NDcxJTAjBgNVBAMT
# HE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDLO8XFOcfGqAqgiz0+AmQmFl3dZ0aTG4UFJkqqNdMHy28D
# aheCBs6ONufukye5x42CWkzgRIy9kE2VWwEntZ8ZkgyrykC0bIqsID7+6FxguseT
# Xf1Vwvm1D8104VmetoBJlJ4uGbuyJZUvXDx55nVh50ygLTzZ24WkQsnPpvRZv2kP
# c39f3bhLyHVtnHsa/W/86Vrftd+AfFveA+qN/EY+XGj5c/DPMXCYECb0arYb92dD
# JWtwzpyBrp4gfHlgY1UEpc4l4AGELrf2J4wrxTzTW+SM8XhV1dOOPrYjD080IbZq
# L8B+IF0RCdn269YXrGK6QIHipznKZcCS8jN30YAHnTJVN5Zzs6t/2YsqBGDquvDa
# d7934FFTwzvUcO3VoIyd93XWwvP8/SCFVJh21W8oGQTptGHyly+Fl4henVMVZF1v
# 6osOtirX8GFTiEhnf8nRdOg7yZYAJ0xy9CtDfbXaTn/cf3Lq3N/GCYKFjC+5mUCE
# +AJhmxMuMdvSUGmKiAFdiPAjUTqsWWBBZJm0eCwgeGJFmmQA+V7/98BKcE+gUL7O
# 9eWRDQwKeAcvo6rxNv2Y4jKrHA6Z/wi3a/fKUhLCNZES8qGdrpDAm7qh+6FjYxyt
# AbkiKM6uTNy/ULPlwtlYZoAJDDQP7eYCywwVbNTbHXRBSS+NccC0sSB4W7U67wID
# AQABo4IBSTCCAUUwHQYDVR0OBBYEFNk72sGDlH0r5DwvfGR5XwJI8B7bMB8GA1Ud
# IwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMFRp
# bWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4wXAYI
# KwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMv
# TWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwGA1Ud
# EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeA
# MA0GCSqGSIb3DQEBCwUAA4ICAQBlbu3IoynnPz0K1iPbeNnsej2b15l5sdl2FAFB
# BGT9lRdc2gNV8LAIusPYHHhUvRDcsx4lbMNhVKPGu4TDLaqNt/CI+SFtGuqdRLpV
# P1XE9cCLyKrKPpcJFJCqPpV+efoAtYBmIUQcxxwT7WIQ7gag8+rkKvrMkCoRqKS0
# mKv8J1sKfi85+G2uhZ/1RteSVdYZOZOj+Sb4wzonTCTj7EtgMN/BX35W5dTzd7wJ
# dGepYkVi871dSrC2Tr1ZFzAR7S44drCWZpJ6phJabVNOsNxFJKgSykugOGWzQ318
# Rr3MTPg2s3Bns+pUPVgMijd4bUOH2BlEsLMMwOcolTTZqg1HYrdY1jxpUAI9ipjB
# QRINL/O705Z+/f2LjNmJQooCVJVX24adpZ519SsfazGoqXGt91bmqKo0fI09Il4s
# UHh4ih6rpiQDBlyL7vmvCejwVxYevY4qVwTZ/o3gvl+R0lFxYS9feIM4NeG0+WsD
# Z7jLci5MFeuNwosQY3z26Xg1oj0U9u+ncR9uTU+xBmJ8BtlCdhQ13RNMX5P+krRY
# PB3XCp9Jm6XaO1995q32AIZm1mzBGI6yHlviXaEC5TzGiO1LXuPtXZU2X93oQJbM
# oe3v8+5CPKrQalGWyYuh2a3V1pwbj+W0FEmEFPpu8TI+qYO1IIQWUSRvFjXth5Ob
# 02hMMjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcN
# AQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAw
# BgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEw
# MB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk
# 4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9c
# T8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWG
# UNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6Gnsz
# rYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2
# LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLV
# wIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTd
# EonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0
# gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFph
# AXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJ
# YfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXb
# GjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJ
# KwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnP
# EP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMw
# UQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggr
# BgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYw
# DwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoY
# xDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtp
# L2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYB
# BQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v
# cGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0B
# AQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U5
# 18JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgAD
# sAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo
# 32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZ
# iefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZK
# PmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RI
# LLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgk
# ujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9
# af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzba
# ukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/
# OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNNMIIC
# NQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEnMCUG
# A1UECxMeblNoaWVsZCBUU1MgRVNOOjM3MDMtMDVFMC1EOTQ3MSUwIwYDVQQDExxN
# aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBLIMg1
# P7sNuCXpmbH2IXT2tXeEEKCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD
# QSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7hRjVDAiGA8yMDI2MDcyOTExMzIzNloY
# DzIwMjYwNzMwMTEzMjM2WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDuFGNUAgEA
# MAcCAQACAgOHMAcCAQACAhMZMAoCBQDuFbTUAgEAMDYGCisGAQQBhFkKBAIxKDAm
# MAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcN
# AQELBQADggEBAMviH8Xc+lr0iATL4YakwJC4YHBj8hN4xGVHg5cLPdT/8DwBws6s
# rhkAtvKD6sQM3AaFb0cfBVDqvHme9P6xJHQa4TjcaZmGIfUCZFZcMLGfqYfw4SI8
# B9GQP+7IhuZmvoTGj3We6awNPRmRbmPlSHvVwu8KnFujERMw1nBzjqGT+AQtd+Jc
# w1TcillQnmMgRTPmV8zUmaZUDCgmYyQA5BptVJWEZvLwz/D/B6rTJ4+AFQMCwIs6
# DtgLLhLj9xXJ6YBG/8BPcgQkCXT6tDrO339b8kgNzLS5wg6nv0ukhizayiWVCNgz
# qfmZ+9OA+rooFhVXt/9hT8yV5r1iRFCpZ7cxggQNMIIECQIBATCBkzB8MQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3Nv
# ZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAh86cGnkojAulQABAAACHzANBglg
# hkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqG
# SIb3DQEJBDEiBCBU5O3xTaXbDA+yAfg8Is4GmjpJP/p22iL3plfHe0oBVDCB+gYL
# KoZIhvcNAQkQAi8xgeowgecwgeQwgb0EILAkCt9WkCsMtURkFu6TY0P3UXdRnCiY
# uPZhe3ykLfwUMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC
# EzMAAAIfOnBp5KIwLpUAAQAAAh8wIgQgvjJrIFDtRplTmDzVZOa/1ZuBy+NuRfPA
# cz0oDsSjESMwDQYJKoZIhvcNAQELBQAEggIACMYOXnCcuosEXrRhfQ64/FBXNjIN
# 6g2fc+dGdjnBsZeCzH3jditIXLVqdv6yraHPXw09u8bqg8fUpLDh9ZUyeW/1MM2p
# JrniaCSY+k3BHqywiM2jUARokTyb1dEf2+AaCVrhV/hKeq1eoReNnmKVFEq40oxy
# tka6iJGDJdTQAiF2it6BD9TBSsNoqpY603tHaQJrRedwI1mG+YldTofrfQgiA1Ks
# eMpMTLEj0ubZq8ncFTt1hxiW2QTYNbju92tzCaAFfRH9IIZL1fGCjxBU3sld2dVx
# 1b/c0vkLYu4UXqtT3qpE27AYE5P6LxrJxcTOU1z+cLtj1H6J+0zoDvNwlVJFQQ1w
# ht45SS2P4z7wccH/DDk5IQZDRaEMdlmEY+ttdrw3lx/LsPHjL9Kb2POQ/yFi58yh
# k4OfwcC6+6wYgTGga6Sc+8gXBukFXpsAST3DA5xDdpOjERcf6y67j2tLMEzsA6vu
# D9rlcqj4aIEbHSqo/U9L6HiJ9mGuUNM+ZVej7htJ/YIWj7U5srwNVSj8ceCWzEoQ
# 6jxMkOTPPzP77aK7GcpXy2NyOnhwpsuV1emzjeHPlD6DVDtGHrYc8iP3p8lrTyrs
# qzpGIW1uQ6Yj9LEaaw8GXKhyulzpcOUEvM2qLOssJUo7oNMV+kwa+3RNEaW4F7wV
# 39R20tYirGssDlg=
# SIG # End signature block