Microsoft.AzStack.CSSTools.AppService.psm1

function Get-AzsSupportAppServiceLogs {
    <#
        .SYNOPSIS
            Gather Azure Stack Hub AppService Resource Provider logs and optionally upload them to a SASUri or a remote network share
        .DESCRIPTION
            Gather Azure Stack Hub AppService Resource Provider logs and optionally upload them to a SASUri or a remote network share
            This script should be done from one of the CN-VMs and requires the Worker Admin credentials to be provided
        .PARAMETER WorkerCred
            Credentials to access worker nodes (Only required when worker role logs are collected)
        .PARAMETER FilterByRole
            Collect only specified roles (Default is all roles)
        .PARAMETER FilterByNode
            Collect only specified nodes by IP address (Defaults to all nodes)
        .PARAMETER TimeOutInMinutes
            Each step will only run up to the number of minutes specificed by this parameter
        .PARAMETER OutputSharePath
            Used to define a network share path that you want to send the file(s) to. It will execute New-AzsSupportNetworkShare to create a new mapped network drive
        .PARAMETER OutputSharePathCreds
            Credentials used to access the network share location
        .PARAMETER OutputShareDriveLetter
            The drive letter that you want to use to map the network drive to
        .PARAMETER OutputSasUri
            The SAS URI token for the Azure or Azure Stack blob storage account you want to upload the file(s) to
        .EXAMPLE
            PS> Get-AzsSupportAppServiceLogs -WorkerCred $WorkerCred
 
            # Collect the logs locally by providing the Worker Credentials using a variable, allowing for the 1 hour default timeout time and allowing for the default 14 days worth of logs
        .EXAMPLE
            PS> Get-AzsSupportAppServiceLogs -WorkerCred $WorkerCred TimeOutInMinutes 120 -FromDate (Get-Date).AddMonths(-1)
 
            # Collect the logs locally by providing the Worker Credentials using a variable, specifying a 30 minutes of timeout time and 1 month worth of logs
        .EXAMPLE
            PS> Get-AzsSupportAppServiceLogs -LogBundlePath c:\Temp\AppService-12121212121 -OutputSharePath "\\xx.xx.xx.xx\share" -OutputSharePathCreds (Get-Credential) -OutputShareDriveLetter X
 
            # Send the logs collected to a remote share
        .EXAMPLE
            PS> Get-AzsSupportAppServiceLogs -LogBundlePath c:\Temp\AppService-12121212121 -OutputSasUri "https://azsdiagprdlocalwestus.blob.core.windows.net/a6a797f70d734aldkhfaknoaghngloransfkjuewrl;jsdjkfnb"
 
            # Send the logs collected to a remote SASUri
            # This option requires AzCopy to be available and that can be installed using the -InstallAzCopy parameter set
        .EXAMPLE
            PS> Get-AzsSupportAppServiceLogs -InstallAzCopy
 
            # Install azcopy locally to be used by the -OutputSasUri parameter set
    #>

    [CmdletBinding(DefaultParameterSetName = "CollectLogs")]
    param (
        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [PSCredential]$workerCred,

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                try {
                    $items = @(Get-AppServiceServer -ErrorAction Ignore | Select-Object -ExpandProperty Role | Sort-Object -Unique)
                }
                catch {}

                if (!$items) {
                    return $null
                }

                if ([string]::IsNullOrEmpty($WordToComplete)) {
                    return $items | Sort-Object
                }
                return $items | Where-Object { $_ -like "*$WordToComplete*" } | ForEach-Object {
                    [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
                }
            })]
        [ValidateScript( {
                try {
                    $roleNames = @(Get-AppServiceServer | Select-Object -ExpandProperty Role | Sort-Object -Unique)
                }
                catch {
                    throw "Unable to validate roles provided"
                }
                foreach ($providedRole in $_) {
                    if ($providedRole -inotin $roleNames) {
                        throw "Role '$providedRole' is not valid"
                    }
                }
                return $true
            })]
        [System.String[]]$FilterByRole = @(Get-AppServiceServer | Select-Object -ExpandProperty Role | Sort-Object -Unique),

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [ArgumentCompleter( {
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                try {
                    $items = @(Get-AppServiceServer -ErrorAction Ignore | Select-Object -ExpandProperty Name | Sort-Object -Unique)
                }
                catch {}

                if (!$items) {
                    return $null
                }

                if ([string]::IsNullOrEmpty($WordToComplete)) {
                    return $items | Sort-Object
                }
                return $items | Where-Object { $_ -like "*$WordToComplete*" } | ForEach-Object {
                    [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
                }
            })]
        [ValidateScript( {
                try {
                    $nodeNames = @(Get-AppServiceServer | Select-Object -ExpandProperty Name | Sort-Object -Unique)
                }
                catch {
                    throw "Unable to validate nodes provided"
                }
                foreach ($providedNode in $_) {
                    if ($providedNode -inotin $nodeNames) {
                        throw "Node '$providedNode' is not valid"
                    }
                }
                return $true
            })]
        [System.Net.IPAddress[]]$FilterByNode,

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [System.Int16]$TimeOutInMinutes = 60,

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [System.DateTime]$FromDate = (Get-Date).AddDays(-14),

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [System.DateTime]$ToDate = (Get-Date),

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [System.Int16]$MaxZipSizeInMB = 100,

        [Parameter(
            ParameterSetName = "CollectLogs",
            Mandatory = $false
        )]
        [System.Int16]$MaxParallelJobs = 5,

        [Parameter(
            ParameterSetName = "InstallAzCopy",
            Mandatory = $true
        )]
        [Switch]$InstallAzCopy,

        [Parameter(
            ParameterSetName = "SendLogsNetworkShare",
            Mandatory = $true
        )]
        [Parameter(
            ParameterSetName = "SendLogsSasUri",
            Mandatory = $true
        )]
        [System.IO.FileInfo]$LogBundlePath,

        [Parameter(
            ParameterSetName = "SendLogsNetworkShare",
            Mandatory = $true
        )]
        [System.IO.FileInfo]$OutputSharePath,

        [Parameter(
            ParameterSetName = "SendLogsNetworkShare",
            Mandatory = $true
        )]
        [System.Management.Automation.PSCredential]$OutputSharePathCreds,

        [Parameter(
            ParameterSetName = "SendLogsNetworkShare",
            Mandatory = $false
        )]
        [char]$OutputShareDriveLetter,

        [Parameter(
            ParameterSetName = "SendLogsSasUri",
            Mandatory = $true
        )]
        [System.String]$OutputSasUri
    )

    $scriptVersion = "2022.04.07.1"
    enum Status {
        Undetermined
        OK
        Failed
        TimedOut
        Incomplete
        NotDone
    }

    class MachineStatus {
        [String]$Role
        [String]$Name
        [String]$IPAddress
        [String]$ServerState
        [String]$workerSizeName
        [String]$ComputeMode
        [Boolean]$Reachable
        [Status]$CollectStatus
        [Status]$CopyStatus
        [Status]$CleanupStatus
        MachineStatus() {
            $this.Reachable = $false
            $this.CollectStatus = [Status]::Undetermined
            $this.CopyStatus = [Status]::Undetermined
            $this.CleanupStatus = [Status]::Undetermined
        }
    }

    $StatusColors = @{
        Undetermined = [ConsoleColor]::Cyan
        OK           = [ConsoleColor]::Green
        Failed       = [ConsoleColor]::Red
        TimedOut     = [ConsoleColor]::Red
        Incomplete   = [ConsoleColor]::Yellow
        NotDone      = [ConsoleColor]::Yellow
    }

    function New-AzsSupportNetworkShare {
        <#
            .SYNOPSIS
                Creates a new network share directory for AzS Hub CSS to leverage
            .DESCRIPTION
                Creates a new network share that can be leveraged to copy files in/out of the Azure Stack Hub environment to customer's datacenter
            .PARAMETER DriveLetter
                Specify the drive letter mapping that you want to create
            .PARAMETER RemoteSharePath
                Specify the network share that is accessible from the Privileged Endpoint via TCP Port 445
            .PARAMETER RemoteShareCredentials
                Specify the credentials that are used to access the network share location
            .EXAMPLE
                New-AzsSupportNetworkShare -DriveLetter z -RemoteSharePath "\\192.168.0.100\share" -RemoteShareCredentials (Get-Credential)
        #>

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

            [Parameter()]
            [ValidateNotNullOrEmpty()]
            [string]$RemoteSharePath,

            [Parameter()]
            [ValidateNotNullOrEmpty()]
            [System.Management.Automation.PSCredential]$RemoteShareCredentials
        )

        try {

            $TotalAttempts = 0
            $MaxRetry = 3

            if ([string]::IsNullOrEmpty($DriveLetter)) {
                $DriveLetter = 'z'
                "No DriveLetter was specified. Using {0}" -f $DriveLetter | Write-Host -ForegroundColor Cyan
            }

            do {
                $TotalAttempts++
                "Attempting to map network drive {0} to {1}" -f $DriveLetter, $RemoteSharePath | Write-Host -ForegroundColor Cyan

                Try {
                    New-PSDrive -Name $DriveLetter -Description "Network Drive used by AppServiceLogs" -PSProvider FileSystem -Root $RemoteSharePath -Credential $RemoteShareCredentials -Persist -Scope Global -ErrorAction Stop | Out-Null
                }
                # Catch and looking for exceptions to be able to gracefully prompt user for required information
                Catch [System.Runtime.InteropServices.ExternalException] {
                    Switch -Wildcard ($_.Exception) {
                        '*The user name or password is incorrect*' {
                            "Ensure that the credentials provided to {0} are correct" -f $RemoteSharePath | Write-Host -ForegroundColor Yellow
                            $RemoteShareCredentials = Get-Credential -Message "Credentials used to access the remote share"
                        }
                        '*The specified network password is not correct*' {
                            "Ensure that the credentials provided to {0} are correct" -f $RemoteSharePath | Write-Host -ForegroundColor Yellow
                            $RemoteShareCredentials = Get-Credential  -Message "Credentials used to access the remote share"
                        }
                        '*The network path was not found*' {
                            "Ensure that the network share path is correct and accessible from {0}" -f $env:COMPUTERNAME | Write-Host -ForegroundColor Yellow
                            $userInputValues = Get-UserInputValues -Properties "NetworkSharePath"
                            $RemoteSharePath = $userInputValues.NetworkSharePath
                        }
                        '*The network resource or device is no longer available*' {
                            "Ensure that the network share path is correct and accessible from {0}" -f $env:COMPUTERNAME | Write-Host -ForegroundColor Yellow
                            $userInputValues = Get-UserInputValues -Properties "NetworkSharePath"
                            $RemoteSharePath = $userInputValues.NetworkSharePath
                        }
                        '*The local device name is already in use*' {
                            "{0} drive is already in use. Please confirm if you want to remove existing mapped drive" -f $DriveLetter | Write-Host -ForegroundColor Yellow
                            Get-PSDrive -PSProvider FileSystem | Format-Table Name, DisplayRoot, CurrentLocation, Description
                            try {
                                $choice = Get-UserInput -Message "Do you want to remove the existing network drive? [Y/N]:"
                                switch ($choice) {
                                    Y {
                                        "User has opted to remove existing network drive" | Write-Host -ForegroundColor Cyan
                                        Get-PSDrive -Name $DriveLetter | Remove-PSDrive
                                    }
                                    N {
                                        throw "User has opted to not remove the existing network drive {0}" -f $DriveLetter
                                    }
                                    Default {
                                        throw "Invalid response"
                                    }
                                }
                            }
                            catch {
                                throw $_
                            }
                        }
                        '*The local device name has a remembered connection to another network resource*' {
                            # in this scenario, we have two seperate powershell sessions where there might be a mapped drive to same resource
                            # psdrive functions do not detect these, and instead need to leverage net use
                            "{0} drive is already defined as persistent connection or connected on another session. Please confirm if you want to remove existing mapped connection" -f $DriveLetter | Write-Host -ForegroundColor Yellow
                            net use
                            try {
                                $choice = Get-UserInput -Message "Do you want to remove the existing network drive? [Y/N]:"
                                switch ($choice) {
                                    Y {
                                        "User has opted to remove existing network drive" | Write-Host -ForegroundColor Cyan
                                        net use "${DriveLetter}:" /delete
                                    }
                                    N {
                                        throw "User has opted to not remove the existing network drive {0}" -f $DriveLetter
                                    }
                                    Default {
                                        throw "Invalid response"
                                    }
                                }
                            }
                            catch {
                                throw $_
                            }
                        }
                        default {
                            "Provide network share and credentials again" | Write-Host -ForegroundColor Yellow
                            $userInputValues = Get-UserInputValues -Properties "DriveLetter,NetworkSharePath"
                            $DriveLetter = $userInputValues.DriveLetter
                            $RemoteSharePath = $userInputValues.NetworkSharePath
                            $RemoteShareCredentials = Get-Credential  -Message "Credentials used to access the remote share"
                        }
                    }
                }
                catch {
                    throw $_
                }
            } until((Test-Path -Path "$($DriveLetter):") -or $TotalAttempts -gt $MaxRetry)

            # Inform the operator if we were able to map the network drive
            if (Test-Path "$($DriveLetter):") {
                "Succesfully mapped network drive" | Write-Host -ForegroundColor Green
            }

        }
        catch {
            $_.Exception.Message | Write-Error
        }
    }

    function Get-UserInputValues {
        <#
        .SYNOPSIS
            Used to capture information from user and generate a psobject
        .PARAMETER Properties
            The psobject properties you want to prompt user to provide
        .EXAMPLE
            PS> $results = Get-UserInputValues -Properties "Destination,Port,RetryAttempts"
 
            Destination: microsoft.com
            Port: 80
            RetryAttempts: 3
 
            PS> $results
 
            Destination Port RetryAttempts
            ----------- ---- -------------
            microsoft.com 80 3
        #>

        [CmdletBinding()]
        param (
            [Parameter(Mandatory = $true)]
            [string]$Properties
        )

        try {

            $object = New-Object PSObject
            foreach ($property in ($Properties.Split(','))) {
                $property = $property.Trim()
                $object | Add-Member -MemberType NoteProperty -Name $property -Value (Get-UserInput -Message "$($property): ").Trim()
            }

            return $object
        }
        catch {
            $_.Exception.Message | Write-Error
        }
    }

    function Get-UserInput {
        <#
        .SYNOPSIS
            Used in scenarios where you need to prompt the user for input
        .PARAMETER Message
            The message that you want to display to the user
        .EXAMPLE
            $choice = Get-UserInput -Message "Do you want to proceed with operation? [Y/N]: "
            Switch($choice){
                'Y' {Do action}
                'N' {Do action}
                default {Do action}
            }
        #>


        param
        (
            [Parameter(Position = 0, ValueFromPipeline = $true)]
            [string]$Message,
            [string]$BackgroundColor = "Black",
            [string]$ForegroundColor = "Yellow"
        )

        Write-Host -ForegroundColor:$ForegroundColor -BackgroundColor:$BackgroundColor -NoNewline $Message;
        return Read-Host
    }

    function Confirm-UserInput {
        param(
            [Parameter(Position = 0, ValueFromPipeline = $true)]
            [string]$Message = "Do you want to continue with this operation? [Y/N]: ",
            [string]$BackgroundColor = "Black",
            [string]$ForegroundColor = "Yellow"
        )

        try {

            Write-Host -ForegroundColor:$ForegroundColor -BackgroundColor:$BackgroundColor -NoNewline $Message
            $answer = Read-Host

            return ($answer -ieq 'y')
        }
        catch {
            $_.Exception.Message | Write-Error
        }
    }

    function Test-Admin {
        [CmdletBinding(DefaultParameterSetName = "Default")]
        Param(
        )
        Write-Verbose -Message "Starting '$($MyInvocation.MyCommand)'"
        $CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
        Write-Verbose -Message "Logged on user - $($CurrentUser.Identity.Name) "
        Write-Verbose -Message "Checking for elevation ... "
        if (($CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) -eq $false) {
            Write-Verbose -Message "In NON admin mode."
            return $false
        }
        else {
            Write-Verbose -Message "In admin mode."
            return $true
        }
    }

    function Get-JobsExecution {
        param(
            $jobPrefix,
            $roleServers,
            [System.Collections.ArrayList]$serversStatus,
            [SYstem.Int16]$jobsCompletedCount,
            [SYstem.Int16]$jobsFailedCount,
            [SYstem.Int16]$MaxParallelJobs,
            $startTime,
            $timer,
            $FilterByRole,
            $Step,
            [Switch]$Wait,
            [Switch]$Cleanup
        )

        while (`
            (!$Cleanup -and ($activeJobs = @(Get-Job | Where-Object Name -Like ("{0}-*" -f $jobPrefix))).Count -ge $MaxParallelJobs -or $Wait) `
                -or ($Cleanup -and ($activeJobs = @(Get-Job | Where-Object Name -Like ("{0}-*" -f $jobPrefix)))) `
                -and ($timer -ge 0)
        ) {
            $activeJobsAtStartCount = $activeJobs.Count
            if ($jobsCompleted = $activeJobs | Where-Object State -ilike "Completed") {
                $jobsCompleted | ForEach-Object {
                    $ipaddress = ($_.Name -split "-")[-1]
                    "[{0}] -`t- Background job for {1} completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ipaddress | Write-Host -ForegroundColor Cyan
                    if ($serverStatus = $serversStatus | Where-Object IPAddress -eq $ipaddress) {
                        $serverStatus.$Step = [Status]::OK
                        $_ | Receive-Job
                        $jobsCompletedCount++
                        $_ | Remove-Job
                        $activeJobsAtStartCount--
                        $Wait = $false
                    }
                }
            }
            elseif ($jobsFailed = $activeJobs | Where-Object State -ilike "Failed" ) {
                $jobsFailed | ForEach-Object {
                    $ipaddress = ($_.Name -split "-")[-1]
                    "[{0}] -`t- Background job for {1} failed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ipaddress | Write-Host -ForegroundColor Red
                    if ($serverStatus = $serversStatus | Where-Object IPAddress -eq $ipaddress) {
                        $serverStatus.$Step = [Status]::Failed
                        $_ | Receive-Job
                        $jobsFailedCount++
                        $_ | Remove-Job
                        $activeJobsAtStartCount--
                        $Wait = $false
                    }
                }
            }
            $message = "[{0}] - `tJobs - Completed {1} - Running {2} - Failed {3} - Pending {4} - Process will timeout in {5} - Elapsed time {6}" -f `
            (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), `
                $jobsCompletedCount, `
                $activeJobsAtStartCount, `
                $jobsFailedCount, `
            ($roleServers.Count - $jobsCompletedCount - $jobsFailedCount - $activeJobsAtStartCount), `
            ((Get-Date).AddSeconds(($timer) * 15) - (Get-Date)).ToString("hh\:mm\:ss"), `
            ((Get-Date) - $startTime).ToString("hh\:mm\:ss")
            if (($activeJobs = @(Get-Job | Where-Object Name -like ("{0}-*" -f $jobPrefix))).Count -gt 0) {
                "{0} - Waiting 15 seconds" -f $message | Write-Host -Foreground Cyan
                Start-Sleep -Seconds 15
                $timer--
            }
            else {
                $message | Write-Host -Foreground Cyan
            }
        }
        if (($activeJobs = @(Get-Job | Where-Object Name -Like ("{0}-*" -f $jobPrefix))) -and $timer -le 0) {
            "[{0}] - `tCleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $activeJobs | Format-Table -AutoSize | Out-String | Write-Verbose
            $activeJobs | ForEach-Object {
                $ipaddress = ($_.Name -split "-")[-1]
                "[{0}] -`t- Cleaning up background job for {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ipaddress | Write-Host -ForegroundColor Cyan
                if ($serverStatus = $serversStatus | Where-Object IPAddress -eq $ipaddress) {
                    $serverStatus.$Step = [Status]::TimedOut
                    $_ | Remove-Job -Force
                }
            }
        }
        return $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount
    }

    $mydocuments = [environment]::getfolderpath("mydocuments")
    $dir = "$mydocuments\AppServiceLogs"
    New-Item -Path $dir -ItemType Directory -Force | Out-Null
    $inst = "azcopy_windows_amd64_10.zip"

    $azcopy = "$azcopypath\AZcopy.exe"

    $Activity = "Azure Stack Hub - App Service Logs - Uploading"
    $Id = 1
    $TotalSteps = 2
    $Step = 1

    switch ($PSCmdlet.ParameterSetName) {
        "CollectLogs" {
            if (!(Test-Admin)) {
                "This command requires an admin elevated session" | Write-Error
                return
            }

            $newFilterByRole = [System.Collections.ArrayList]::new()
            $roleNames = @(Get-AppServiceServer | Select-Object -ExpandProperty Role | Sort-Object -Unique)
            foreach ($providedRole in $FilterByRole) {
                if ($newRoleName = $roleNames -like "*$providedRole*") {
                    $newFilterByRole.add([String]$newRolename) | Out-Null
                }
            }

            $FilterByRole = $newFilterByRole

            if ($FilterByRole -icontains "WebWorker" -and !$workerCred) {
                if (!($workerCred = Get-Credential -Message "Enter credentials for Worker Admin")) {
                    Write-Error "Worker Admin credentials are required"
                    return
                }
            }

            $startTime = Get-Date
            [String]$currentDate = (Get-Date -Date $startTime -Format "yyyyMMdd-HHmmss").ToString()

            $logDirectory = "c:\temp\AppServiceLogs_{0}" -f $currentDate
            $localLogDirectory = "c:\temp\AppServiceLogs_Local"

            do {
                try {
                    $exitStatus = Stop-Transcript -ErrorAction Ignore
                    "Cleaning zombie transcript" | Write-Warning
                }
                catch { $exitStatus = $null }
            } until (!$exitStatus)


            if ($zombieJobs = Get-Job -Name "AppService-*") {
                "Removing zombie jobs" | Write-Warning
                $zombieJobs | Remove-Job -Force
            }

            New-Item -Path $logDirectory -ItemType Directory -Force -ErrorAction Ignore | Out-Null

            $transcriptFileName = "{0}-CompleteAppRPLogCollection_{1}.txt" -f $env:COMPUTERNAME, $currentDate
            Start-Transcript -Path (Join-Path -Path $logDirectory -ChildPath $transcriptFileName) | Write-Host -ForegroundColor Yellow
            $collectionScript = {
                param(
                    [Parameter(Mandatory = $true)]
                    $NodeName,

                    [Parameter(Mandatory = $true)]
                    $NodeIPAddress,

                    [Parameter(Mandatory = $true)]
                    $localLogDirectory,

                    [Parameter(Mandatory = $true)]
                    $MaxZipSizeInMB,

                    [Parameter(Mandatory = $true)]
                    $FromDate,

                    [Parameter(Mandatory = $true)]
                    $ToDate,

                    [Parameter(Mandatory = $true)]
                    $currentDate
                )
                function Compress-MultipleArchivesBasedOnMaxSize {
                    [CmdletBinding()]
                    param(
                        [Parameter(Mandatory = $true)]
                        [String]$NodeName,

                        [Parameter(Mandatory = $true)]
                        [String]$NodeIPAddress,

                        [Parameter(Mandatory = $true)]
                        [String]$ZipOutputPath,

                        [Parameter(Mandatory = $true)]
                        [Object[]]$FilesToAdd,

                        [Parameter(Mandatory = $false)]
                        [Object[]]$ZipBasepath,

                        [Parameter(Mandatory = $false)]
                        [Decimal]$MaxZipSizeInMB = 100
                    )

                    $zipTimeStamp = Get-Date -Format "yyyyMMddHHmmss"
                    $zipIndex = 0
                    Add-Type -Assembly System.IO.Compression.FileSystem
                    [Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null

                    "[{0}|{1}|{2}] - `tMax zip file {3}MB" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $MaxZipSizeInMB | Write-Host -ForegroundColor Cyan

                    $zipFileName = [System.IO.Path]::Combine($ZipOutputPath, ("{0}-{1}-{2}-{3}.zip" -f $NodeName, $NodeIPAddress, $zipTimeStamp, $zipIndex))
                    "[{0}|{1}|{2}] - `tCreating new zip file '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $zipFileName | Write-Host -ForegroundColor Cyan
                    $zipObj = [System.IO.Compression.ZipFile]::Open($zipFileName, "Create")
                    foreach ($fileName in ($NewFilesToAdd | Where-Object Name -notlike ("^$($NodeName)-$($NodeIPAddress)-\d{14}-\d+\.zip$"))) {
                        if ((($zipFileNameInfo = [System.IO.FileInfo]$zipFileName).Length) -and $zipFileNameInfo.Length -gt ($MaxZipSizeInMB * 1MB)) {
                            $zipObj.Dispose()
                            $zipIndex++
                            $zipFileName = [System.IO.Path]::Combine($ZipOutputPath, ("{0}-{1}-{2}-{3}.zip" -f $NodeName, $NodeIPAddress, $zipTimeStamp, $zipIndex))
                            "[{0}|{1}|{2}] - `tCreating new zip file '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $zipFileName | Write-Host -ForegroundColor Cyan
                            $zipObj = [System.IO.Compression.ZipFile]::Open($zipFileName, "Create")
                        }
                        if ($ZipBasepath) {
                            $entryName = $fileName.FullName -replace ("^{0}\\" -f [Regex]::Escape($ZipBasepath))
                        }
                        else {
                            $entryName = $fileName.FullName -replace "^.:\\"
                        }
                        if (Test-Path -Path $FileName.FullName -PathType Leaf) {
                            "[{0}|{1}|{2}] - `tAdding entry {3} for file '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $entryName, $fileName.FullName | Write-Verbose
                            if ($fileName.FullName -like "*.zip") {
                                $compressionLevel = [System.IO.Compression.CompressionLevel]::NoCompression
                            }
                            else {
                                $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
                            }
                            [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipObj, $fileName.FullName, $entryName, $compressionLevel) | Out-Null
                        }
                        else {
                            "[{0}|{1}|{2}] - `tSkipping. Does not exist. {3} for file '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $entryName, $fileName | Write-Verbose
                        }
                    }
                    $zipObj.Dispose()
                }

                function Copy-FilesToArchive {
                    [CmdletBinding()]
                    param(
                        [Parameter(Mandatory = $true)]
                        [String]$NodeName,

                        [Parameter(Mandatory = $true)]
                        [String]$NodeIPAddress,

                        [Parameter(Mandatory = $true)]
                        [String]$SourceDirectory,

                        [Parameter(Mandatory = $true)]
                        [String]$SourceDirectoryDescription,

                        [Parameter(Mandatory = $true)]
                        [String]$DestinationDirectory,

                        [Parameter(Mandatory = $false)]
                        [System.DateTime]$FromDate = (Get-Date).AddDays(-14),

                        [Parameter(Mandatory = $false)]
                        [System.DateTime]$ToDate = (Get-Date)
                    )

                    Set-StrictMode -Version 1.0
                    "[{0}|{1}|{2}] - Collect '{3}' directory '{4}' started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $SourceDirectoryDescription, $SourceDirectory | Write-Host -ForegroundColor Cyan
                    "[{0}|{1}|{2}] - Considering files between {3} and {4}" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $FromDate, $ToDate | Write-Host -ForegroundColor Cyan
                    if (Test-Path -Path $SourceDirectory) {
                        Get-ChildItem -Path $SourceDirectory -Recurse -File | Where-Object { $_.LastWriteTime -ge $FromDate -and $_.LastWriteTime -le $ToDate } | ForEach-Object {
                            $destinationFile = [System.IO.Path]::Combine($DestinationDirectory, ([System.IO.DirectoryInfo]$SourceDirectory).Name, ($_.FullName -replace ("^{0}" -f [Regex]::Escape($SourceDirectory))))
                            $destinationDirectoryTree = Split-Path -Path $destinationFile -Parent
                            if (!(Test-Path -Path $destinationDirectoryTree -PathType Container)) {
                                "[{0}|{1}|{2}] -- Creating destination directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $destinationDirectoryTree | Write-Verbose
                                New-Item -Path $destinationDirectoryTree -ItemType Directory | Out-Null
                            }
                            "[{0}|{1}|{2}] -- Copying '{3}' to '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_.FullName, $destinationFile | Write-Verbose
                            Copy-Item -Path $_.FullName -Destination $destinationFile -Force
                        }
                        "[{0}|{1}|{2}] - Collect '{3}' directory '{4}' Completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $SourceDirectoryDescription, $SourceDirectory | Write-Host -ForegroundColor Green
                    }
                    else {
                        "[{0}|{1}|{2}] - Collect '{3}' directory '{4}' does not exist" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $SourceDirectoryDescription, $SourceDirectory | Write-Host -ForegroundColor Yellow
                    }

                }

                $VerbosePreference = $using:VerbosePreference

                $dataCollectionDir = [System.IO.Path]::Combine($localLogDirectory, "FilesCollected")

                try {
                    #Remove AppServiceLogs_Local directory if already exists
                    if (Test-Path -Path $localLogDirectory -PathType Container) {
                        "[{0}|{1}|{2}] - Removing local log directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $localLogDirectory | Write-Host -ForegroundColor Cyan
                        Remove-Item -Path $localLogDirectory -Recurse -Force -Confirm:$false | Out-Null
                    }

                    #Create AppServiceLogs_Local directory
                    "[{0}|{1}|{2}] - Creating local log directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $dataCollectionDir | Write-Host -ForegroundColor Cyan
                    New-Item -Path $dataCollectionDir -ItemType Directory -Force | Out-Null
                    $transcriptFileName = [System.IO.Path]::Combine($localLogDirectory, ("{0}-{1}-AppRPLogCollection-{2}.txt" -f $NodeName, $NodeIPAddress, $currentDate))
                    "[{0}|{1}|{2}] - Starting transcript to '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $transcriptFileName | Write-Host -ForegroundColor Cyan
                    Start-Transcript -Path $transcriptFileName | Write-Host -ForegroundColor Cyan
                    "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                    "[{0}|{1}|{2}] - Starting log collection" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                    "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White

                    $sharename = "CSS"

                    #Logs to collect
                    $collectGuestLogsPath = "C:\WindowsAzure\GuestAgent*\"
                    $collectGuestLogsPath2 = "C:\WindowsAzure\Packages\"
                    $httplogdirectory = "C:\DWASFiles\Log\"
                    $ftpLogDirectory = "C:\inetpub\logs\LogFiles\"
                    $websitesInstalldir = "C:\WebsitesInstall\"
                    $windowsEventLogdir = "C:\Windows\System32\winevt\Logs"
                    $webPILogdir = "C:\Program Files\IIS\Microsoft Web farm framework\roles\resources\antareslogs"
                    $packagesdir = "C:\Packages"

                    #Create CSS share
                    New-SmbShare -Name $sharename -Path $dataCollectionDir -FullAccess "$env:UserDomain\$env:UserName" -ErrorAction Ignore | Out-Null

                    #Starting CollectGuestLogs
                    "[{0}|{1}|{2}] - Collect Guest Logs (CollectGuestLogs.exe) started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                    if (Test-Path -Path $collectGuestLogsPath -PathType Any) {
                        Start-Process "$collectGuestLogsPath\CollectGuestLogs.exe" -Verb runAs -WorkingDirectory $collectGuestLogsPath -Wait ;
                        Move-Item $collectGuestLogsPath\*.zip -Destination $dataCollectionDir\ -Force
                        Move-Item $collectGuestLogsPath\*.zip.json -Destination $dataCollectionDir\ -Force
                        Get-Item $dataCollectionDir\*.zip.json | Rename-Item -NewName { $_.name -replace ".zip.json", ".json" }
                    }
                    elseif (Test-Path -Path $collectGuestLogsPath2 -PathType Any) {
                        Start-Process "$collectGuestLogsPath2\CollectGuestLogs.exe" -Verb runAs -WorkingDirectory $collectGuestLogsPath2 -Wait ;
                        Move-Item $collectGuestLogsPath2\*.zip -Destination $dataCollectionDir\ -Force
                        Move-Item $collectGuestLogsPath2\*.zip.json -Destination $dataCollectionDir\ -Force
                        Get-Item $dataCollectionDir\*.zip.json | Rename-Item -NewName { $_.name -replace ".zip.json", ".json" }
                    }
                    "[{0}|{1}|{2}] - Collect Guest Logs completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

                    if ($NodeName -notlike "CN*") {
                        #Collecting IIS logs
                        Copy-FilesToArchive -NodeName $NodeName -NodeIPAddress $NodeIPAddress -SourceDirectory $httplogdirectory -DestinationDirectory ([System.IO.Path]::Combine($dataCollectionDir, "HTTPLogs")) -SourceDirectoryDescription "IIS logs" -FromDate $FromDate -ToDate $ToDate
                        #Collecting WFF logs
                        Copy-FilesToArchive -NodeName $NodeName -NodeIPAddress $NodeIPAddress -SourceDirectory $webPILogdir -DestinationDirectory $dataCollectionDir -SourceDirectoryDescription "WFF logs" -FromDate $FromDate -ToDate $ToDate
                    }

                    #Collect FTP logs on Publisher servers
                    if ($NodeName -like "FTP*") {
                        #Collecting FTP logs
                        Copy-FilesToArchive -NodeName $NodeName -NodeIPAddress $NodeIPAddress -SourceDirectory $ftpLogDirectory -DestinationDirectory ([System.IO.Path]::Combine($dataCollectionDir, "FTPLogs")) -SourceDirectoryDescription "FTP logs" -FromDate $FromDate -ToDate $ToDate
                    }


                    #Collecting Event logs
                    "[{0}|{1}|{2}] - Collect Event logs (WebSites logs) started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                    Copy-Item $windowsEventLogdir\Microsoft-Windows-WebSites%4Administrative.evtx -Destination $dataCollectionDir\ -Force
                    Copy-Item $windowsEventLogdir\Microsoft-Windows-WebSites%4Operational.evtx -Destination $dataCollectionDir\ -Force
                    Copy-Item $windowsEventLogdir\Microsoft-Windows-WebSites%4Verbose.evtx -Destination $dataCollectionDir\  -Force
                    "[{0}|{1}|{2}] - Collect Event logs completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

                    #Collecting WebsitesInstall logs
                    "[{0}|{1}|{2}] - Collect WebsitesInstall logs '{3}' started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $websitesInstalldir | Write-Host -ForegroundColor Cyan
                    # WORK AROUND TO ADDRESS THE FACT THAT WINDOWS PATCHES PACKAGES (WITH ABOUT 4GB IN SIZE) WERE ALSO BEING COLLECTED
                    $dataCollectionDir = Get-Item -Path $dataCollectionDir
                    "[{0}|{1}|{2}] -- excluding files with extension like .msi or .msu" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                    Get-ChildItem -Path $websitesInstalldir -Recurse | Where-Object { $_.psIsContainer -eq $false -and $_.Extension -notin (".msi", ".msu", ".exe") } | ForEach-Object {
                        $destinationFile = Join-Path -Path $dataCollectionDir -ChildPath (Join-Path -Path ([System.IO.DirectoryInfo]$websitesInstalldir).Name -ChildPath ($_.FullName -replace ("^{0}" -f [Regex]::Escape($websitesInstalldir))))
                        $destinationDirectory = Split-Path -Path $destinationFile -Parent
                        if (!(Test-Path -Path $destinationDirectory -PathType Container)) {
                            "[{0}|{1}|{2}] -- Creating destination directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $destinationDirectory | Write-Verbose
                            New-Item -Path $destinationDirectory -ItemType Directory | Out-Null
                        }
                        "[{0}|{1}|{2}] -- Copying '{3}' to '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_.FullName, $destinationFile | Write-Verbose
                        Copy-Item -Path $_.FullName -Destination $destinationFile -Force
                    }
                    "[{0}|{1}|{2}] - Collect WebsitesInstall logs completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

                    #Collecting C:\Packages
                    "[{0}|{1}|{2}] - Collect C:\Packages started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                    Copy-Item $packagesdir\ -Recurse -Destination $dataCollectionDir\ -Force
                    "[{0}|{1}|{2}] - Collect C:\Packages completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

                    "[{0}|{1}|{2}] - Finding files to compress started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                    $NewFilesToAdd = Get-ChildItem $dataCollectionDir -Recurse -File
                    "[{0}|{1}|{2}] - Finding files to compress completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
                    "[{0}|{1}|{2}] - Compressing Files started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                    Compress-MultipleArchivesBasedOnMaxSize  -NodeName $NodeName -NodeIPAddress $NodeIPAddress -ZipOutputPath $localLogDirectory -FilesToAdd $NewFilesToAdd -ZipBasepath $dataCollectionDir -MaxZipSizeInMB $MaxZipSizeInMB
                    "[{0}|{1}|{2}] - Compressing Files completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
                    "[{0}|{1}|{2}] - Cleaning up Files on directory {3} started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $dataCollectionDir | Write-Host -ForegroundColor Cyan
                    Remove-Item -Path $dataCollectionDir -Recurse -Force -Confirm:$false -ErrorAction Ignore
                    "[{0}|{1}|{2}] - Cleaning up Files on directory {3} completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $dataCollectionDir | Write-Host -ForegroundColor Green
                    Stop-Transcript | Write-Host -ForegroundColor Green
                }
                catch {
                    "[{0}|{1}|{2}] - Failure found '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $PSItem.Exception.Message | Write-Host -ForegroundColor Red
                    throw $PSItem
                }
                "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - Ending log collection" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
            }

            $cleanupScript = {
                param(
                    [Parameter(Mandatory = $true)]
                    $NodeName,

                    [Parameter(Mandatory = $true)]
                    $NodeIPAddress,

                    [Parameter(Mandatory = $true)]
                    $localLogDirectory
                )

                "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - Starting cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - Starting log cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                try {
                    $sharename = "CSS"

                    #Remove CSS share
                    Get-SmbShare -Name $sharename | Remove-SmbShare -Confirm:$false -Force -ErrorAction Ignore | Out-Null

                    #Remove AppServiceLogs_Local directory if already exists
                    if (Test-Path -Path $localLogDirectory -PathType Any) {
                        Remove-Item -Path $localLogDirectory -Recurse -Force -Confirm:$false -ErrorAction Ignore | Out-Null
                    }
                }
                catch {
                    "[{0}|{1}|{2}] - Failure found '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $PSItem.Exception.Message | Write-Host -ForegroundColor Red
                    throw $PSItem
                }
                "[{0}|{1}|{2}] - Completed log cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
                "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - Ending cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
                "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White
            }

            if (!$PSBoundParameters.ContainsKey("ToDate")) {
                "[{0}] - No ToDate parameter specified. Increasing the default value {1} by ading the TimeOutInMinutes {2}. Final value {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ToDate.ToString(), $TimeOutInMinutes, $ToDate.AddMinutes($TimeOutInMinutes) | Write-Host -ForegroundColor Cyan
                $ToDate = $ToDate.AddMinutes($TimeOutInMinutes)
            }

            "[{0}] - Script Version {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $scriptVersion | Write-Host -ForegroundColor Cyan
            "[{0}] - Parameters:" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            "[{0}] - `tFilterByRole {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByRole -join ",") | Write-Host -ForegroundColor Cyan
            "[{0}] - `tFilterByNode {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByNode -join ",") | Write-Host -ForegroundColor Cyan
            "[{0}] - `tTimeout in {1} minutes" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $TimeOutInMinutes | Write-Host -ForegroundColor Cyan
            "[{0}] - `tMaxParallelJobs {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $MaxParallelJobs | Write-Host -ForegroundColor Cyan
            "[{0}] - `tMaxZipSizeInMB {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $MaxZipSizeInMB | Write-Host -ForegroundColor Cyan
            "[{0}] - `tFromDate {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $FromDate | Write-Host -ForegroundColor Cyan
            "[{0}] - `tNumToDate {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ToDate | Write-Host -ForegroundColor Cyan
            "[{0}] - `tTimeframe {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($ToDate - $FromDate) | Write-Host -ForegroundColor Cyan

            $timer = ($TimeOutInMinutes * 60) / 15
            $originalTimer = $timer
            "[{0}] - Collecting logs for roles {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByRole -join ",") | Write-Host -ForegroundColor Cyan
            $roleServers = Get-AppServiceServer | Where-Object Role -In $FilterByRole | Sort-Object @{E = { [Version]$_.Name } }
            if ($PSBoundParameters.ContainsKey("FilterByNode")) {
                "[{0}] - Filtering by node(s) '{1}'" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByNode -join "','") | Write-Host -ForegroundColor Yellow
                $roleServers = $roleServers | Where-Object Name -in $FilterByNode
            }
            "[{0}] - Starting role server information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            Get-AppServiceServer | Select-Object Name, Status, Role, CpuPercentage, MemoryPercentage, ServerState, PlatformVersion | Format-Table | Out-File (Join-Path -Path $logDirectory -ChildPath "Get-AppServiceServer.txt")
            Get-AppServiceServer | ConvertTo-Json | Out-File $logDirectory"\AppServiceServer.json"
            "[{0}] - Completed role server information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
            "[{0}] - Starting Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $appServiceConfigGlobal = Get-AppServiceConfig -Type Global
            "[{0}] - Redacting secrets on Config Global information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $appServiceConfigGlobal.BitbucketClientId = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketClientSecret = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketNextClientId = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketNextClientSecret = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketProdClientId = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketProdClientSecret = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketStageClientId = "[REDACTED]"
            $appServiceConfigGlobal.BitbucketStageClientSecret = "[REDACTED]"
            $appServiceConfigGlobal.CertificatePassword = "[REDACTED]"
            $appServiceConfigGlobal.GitHubClientId = "[REDACTED]"
            $appServiceConfigGlobal.GitHubClientSecret = "[REDACTED]"
            $appServiceConfigGlobal.InfrastructureClientCertificatePassword = "[REDACTED]"
            $appServiceConfigGlobal.InfrastructureClientId = "[REDACTED]"
            $appServiceConfigGlobal.ManagementServerCertificatePassword = "[REDACTED]"
            $appServiceConfigGlobal.PublisherServerCertificatePassword = "[REDACTED]"
            $appServiceConfigGlobal.TokenRequestCertificatePassword = "[REDACTED]"
            $appServiceConfigGlobal.UsageStorageAccountConnString = "[REDACTED]"
            $appServiceConfigGlobal | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceConfigGlobal.json")
            $appServiceConfigGlobal | Format-List | Out-String | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceConfigGlobal.txt")
            "[{0}] - Completed Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
            "[{0}] - Starting app server event collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            Get-AppServiceEvent -StartTime $FromDate -EndTime $ToDate | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceEvent.json")
            "[{0}] - Completed app server event collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
            "[{0}] - Starting ActiveController operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $appServiceOperationActiveController = Get-AppServiceOperation -OperatorName ActiveController
            $appServiceOperationActiveController | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationActiveController.json")
            $appServiceOperationActiveController | Format-List | Out-String | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationActiveController.txt")
            "[{0}] - Completed ActiveController operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
            "[{0}] - Starting WFF operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $appServiceOperationWFF = Get-AppServiceOperation -OperatorName WFF
            $appServiceOperationWFF | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationWFF.json")
            $appServiceOperationWFF | Format-List | Out-String | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationWFF.txt")
            "[{0}] - Completed WFF operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

            $serversStatus = [System.Collections.ArrayList]::new()

            #Checking reachability
            "[{0}] - Reachability test started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $currentProgressPreference = $ProgressPreference
            $ProgressPreference = "SilentlyContinue"
            foreach ($server in $roleServers) {
                $serverStatus = New-Object MachineStatus
                $serverStatus.Role = $server.Role
                $serverStatus.IPAddress = $server.Name
                $serverStatus.ServerState = $server.ServerState
                $serverStatus.workerSizeName = $server.workerSizeName
                $serverStatus.ComputeMode = $server.ComputeMode
                "[{0}] - `tTesting reachability for {1} - {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                if (Test-NetConnection $server.name -Port 5985) {
                    if ($server.role -eq "WebWorker") {
                        try {
                            $serverStatus.Name = Invoke-Command -ComputerName $server.Name -ScriptBlock { $env:Computername } -Credential $workerCred
                            $serverStatus.Reachable = $true
                        }
                        catch {
                            $serverStatus.Reachable = $false
                        }
                    }
                    else {
                        try {
                            $serverStatus.Name = Invoke-Command -ComputerName $server.Name -ScriptBlock { $env:Computername }
                            $serverStatus.Reachable = $true
                        }
                        catch {
                            $serverStatus.Reachable = $false
                        }
                    }
                }
                else {
                    $serverStatus.Reachable = $false
                    $serverStatus.Name = "Unknown"
                }
                $serversStatus.Add($serverStatus) | Out-Null
            }
            $ProgressPreference = $currentProgressPreference
            "[{0}] - Reachability test completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

            "[{0}] - Setup Log collection jobs started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString()
            $jobsConfig = [System.Collections.ArrayList]::new()
            foreach ($_serverStatus in $serversStatus) {
                if ($_serverStatus.Reachable) {
                    "[{0}] - `tSetting up data collection on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_serverStatus.Name, $_serverStatus.IPAddress, $_serverStatus.Role | Write-Host -ForegroundColor Cyan
                    if ($_serverStatus.role -eq "WebWorker") {
                        $jobsConfig.Add([PSCustomObject]@{
                                ServerName      = $_serverStatus.Name
                                ServerIPAddress = $_serverStatus.IPAddress
                                ServerRole      = $_serverStatus.Role
                                JobName         = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress)
                                Started         = $false
                                JobParams       = @{
                                    ComputerName = $_serverStatus.IPAddress
                                    ScriptBlock  = $collectionScript
                                    Credential   = $workerCred
                                    AsJob        = $true
                                    JobName      = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress)
                                    Verbose      = $Verbose
                                    ArgumentList = ($_serverStatus.Name, $_serverStatus.IPAddress, $localLogDirectory, $MaxZipSizeInMB, $FromDate, $ToDate, $currentDate)
                                }
                            }) | Out-Null
                    }
                    else {
                        $jobsConfig.Add([PSCustomObject]@{
                                ServerName      = $_serverStatus.Name
                                ServerIPAddress = $_serverStatus.IPAddress
                                ServerRole      = $_serverStatus.Role
                                JobName         = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress)
                                Started         = $false
                                JobParams       = @{
                                    ComputerName = $_serverStatus.IPAddress
                                    ScriptBlock  = $collectionScript
                                    AsJob        = $true
                                    JobName      = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress)
                                    Verbose      = $Verbose
                                    ArgumentList = ($_serverStatus.Name, $_serverStatus.IPAddress, $localLogDirectory, $MaxZipSizeInMB, $FromDate, $ToDate, $currentDate)
                                }
                            }) | Out-Null
                    }
                }
                else {
                    "[{0}] - `tSkipped data collection on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_serverStatus.Name, $_serverStatus.IPAddress, $_serverStatus.Role | Write-Host -ForegroundColor Cyan
                    $serverStatus.CollectStatus = [Status]::NotDone
                }
            }
            "[{0}] - Setup Log collection jobs Completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan

            #Log collection
            "[{0}] - Log collection started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString()
            $jobsCompletedCount = 0
            $jobsFailedCount = 0
            while ($jobsToRun = $jobsConfig | Where-Object Started -eq $false) {
                foreach ($_jobToRun in $jobsToRun) {
                    $IPAddressesRunning = Get-Job | Where-Object Name -like "$jobPrefix-*" | ForEach-Object { $_.Name.Split("-")[-1] }
                    $rolesRunning = ($serversStatus | Where-Object IPAddress -in $IPAddressesRunning).Role | Sort-Object -Unique
                    if ($VerbosePreference -eq "Continue") {
                        "RolesRunning" | Write-Host -ForegroundColor Magenta
                        $rolesRunning | Write-Host -ForegroundColor Magenta
                    }
                    if ($_jobToRun.ServerRole -notin $rolesRunning) {
                        $jobParams = $_jobToRun.JobParams
                        try {
                            "[{0}] - `tStarting data collection on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_jobToRun.ServerName, $_jobToRun.ServerIPAddress, $_jobToRun.ServerRole | Write-Host -ForegroundColor Cyan
                            Invoke-Command @JobParams | Out-Null
                        }
                        catch {}
                        foreach ($_jobConfig in $jobsConfig) {
                            if ($_jobConfig.JobName -eq $_jobToRun.JobName) {
                                $_jobConfig.Started = $true
                                break
                            }
                        }
                        $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                            -JobPrefix $jobPrefix `
                            -roleServers $roleServers `
                            -serversStatus $serversStatus `
                            -jobsCompletedCount $jobsCompletedCount `
                            -jobsFailedCount $jobsFailedCount `
                            -MaxParallelJobs $MaxParallelJobs `
                            -startTime $startTime `
                            -timer $timer `
                            -Step "CollectStatus"
                        if ($timer -le 0) {
                            break
                        }
                    }
                }
                $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                    -JobPrefix $jobPrefix `
                    -roleServers $roleServers `
                    -serversStatus $serversStatus `
                    -jobsCompletedCount $jobsCompletedCount `
                    -jobsFailedCount $jobsFailedCount `
                    -MaxParallelJobs $MaxParallelJobs `
                    -startTime $startTime `
                    -timer $timer `
                    -Step "CollectStatus" `
                    -Wait
                if ($timer -le 0) {
                    break
                }
            }
            $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                -JobPrefix $jobPrefix `
                -roleServers $roleServers `
                -serversStatus $serversStatus `
                -jobsCompletedCount $jobsCompletedCount `
                -jobsFailedCount $jobsFailedCount `
                -MaxParallelJobs $MaxParallelJobs `
                -startTime $startTime `
                -timer $timer `
                -Step "CollectStatus" `
                -FilterByRole $FilterByRole `
                -Cleanup

            if ($remaniningJobs = Get-Job -Name ("{0}-*" -f $jobPrefix)) {
                "[{0}] - Cleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                $remaniningJobs | Remove-Job -Force
            }
            "[{0}] - Log collection completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

            #Log copying
            "[{0}] - Log copying started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString()
            $jobsCompletedCount = 0
            $jobsFailedCount = 0
            $timer = $originalTimer
            foreach ($serverStatus in $serversStatus) {
                if ($serverStatus.CollectStatus -eq [Status]::OK) {
                    if ($serverStatus.Role -eq "WebWorker") {
                        "[{0}] - `tStarting data copying on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                        Start-Job {
                            $localLogDirectory = $using:localLogDirectory
                            if ($workerSession = New-PSSession -Credential $using:workerCred -ComputerName $($using:serverStatus.IPAddress)) {
                                Invoke-Command -Session $workerSession -ScriptBlock { Get-ChildItem -Path $using:localLogDirectory } | Format-Table LastWriteTime, Length, FullName -AutoSize | Out-String | Write-Host -ForegroundColor Cyan
                                Copy-Item -FromSession $workerSession -Path $localLogDirectory\* -Destination $using:logDirectory
                                Remove-PSSession $workerSession
                            }
                            else {
                                "Unable to create PSSession to {0}" -f $using:serverStatus.IPAddress | Write-Error
                            }
                        } -Name ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) | Out-Null
                    }
                    else {
                        "[{0}] - `tStarting data copying on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                        Start-Job {
                            Get-ChildItem -Path \\$($using:serverStatus.IPAddress)\$($using:localLogDirectory -replace "C:","C$") | Format-Table LastWriteTime, Length, FullName -AutoSize | Out-String | Write-Host -ForegroundColor Cyan
                            Copy-Item \\$($using:serverStatus.IPAddress)\$($using:localLogDirectory -replace "C:","C$")\* -Destination $using:logDirectory
                        } -Name ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) | Out-Null
                    }
                }
                else {
                    "[{0}] - `tSkipped data moving on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                    $serverStatus.CopyStatus = [Status]::NotDone
                }
                $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                    -JobPrefix $jobPrefix `
                    -roleServers $roleServers `
                    -serversStatus $serversStatus `
                    -jobsCompletedCount $jobsCompletedCount `
                    -jobsFailedCount $jobsFailedCount `
                    -MaxParallelJobs $MaxParallelJobs `
                    -startTime $startTime `
                    -timer $timer `
                    -Step "CopyStatus" `
                    -FilterByRole $FilterByRole
            }
            $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                -JobPrefix $jobPrefix `
                -roleServers $roleServers `
                -serversStatus $serversStatus `
                -jobsCompletedCount $jobsCompletedCount `
                -jobsFailedCount $jobsFailedCount `
                -MaxParallelJobs $MaxParallelJobs `
                -startTime $startTime `
                -timer $timer `
                -Step "CopyStatus" `
                -FilterByRole $FilterByRole `
                -Cleanup `

            if ($remaniningJobs = Get-Job -Name ("{0}-*" -f $jobPrefix)) {
                "[{0}] - Cleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                $remaniningJobs | Remove-Job -Force
            }
            "[{0}] - Log copying completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

            #Log cleanup
            "[{0}] - Log cleanup started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
            $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString()
            $jobsCompletedCount = 0
            $jobsFailedCount = 0
            $timer = $originalTimer
            foreach ($serverStatus in $serversStatus) {
                if ($serverStatus.CopyStatus -eq [Status]::OK) {
                    if ($serverStatus.role -eq "WebWorker") {
                        try {
                            "[{0}] - `tStarting data cleanup on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                            Invoke-Command -ComputerName $serverStatus.IPAddress `
                                -ScriptBlock $cleanupScript `
                                -Credential $workerCred `
                                -AsJob `
                                -JobName ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) `
                                -Verbose:$Verbose `
                                -ArgumentList $serverStatus.Name, $serverStatus.IPAddress, $localLogDirectory `
                            | Out-Null
                        }
                        catch {
                            $serverStatus.CleanupStatus = [Status]::Failed
                        }
                    }
                    else {
                        try {
                            "[{0}] - `tStarting data cleanup on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                            Invoke-Command -ComputerName $serverStatus.IPAddress `
                                -ScriptBlock $cleanupScript `
                                -AsJob `
                                -JobName ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) `
                                -Verbose:$Verbose `
                                -ArgumentList $serverStatus.Name, $serverStatus.IPAddress, $localLogDirectory `
                            | Out-Null
                        }
                        catch {
                            $serverStatus.CleanupStatus = [Status]::Failed
                        }
                    }
                }
                else {
                    "[{0}] - `tSkipped data cleanup on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan
                    $serverStatus.CleanupStatus = [Status]::NotDone
                }
                $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                    -JobPrefix $jobPrefix `
                    -roleServers $roleServers `
                    -serversStatus $serversStatus `
                    -jobsCompletedCount $jobsCompletedCount `
                    -jobsFailedCount $jobsFailedCount `
                    -MaxParallelJobs $MaxParallelJobs `
                    -startTime $startTime `
                    -timer $timer `
                    -Step "CleanupStatus" `
                    -FilterByRole $FilterByRole
            }
            $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution `
                -JobPrefix $jobPrefix `
                -roleServers $roleServers `
                -serversStatus $serversStatus `
                -jobsCompletedCount $jobsCompletedCount `
                -jobsFailedCount $jobsFailedCount `
                -MaxParallelJobs $MaxParallelJobs `
                -startTime $startTime `
                -timer $timer `
                -Step "CleanupStatus" `
                -FilterByRole $FilterByRole `
                -Cleanup

            if ($remaniningJobs = Get-Job -Name ("{0}-*" -f $jobPrefix)) {
                "[{0}] - Cleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
                $remaniningJobs | Remove-Job -Force
            }
            "[{0}] - Log cleanup completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

            $endTime = Get-Date
            $actionTree = [PSCustomObject]@{
                ScriptVersion    = $scriptVersion
                LogDirectory     = $logDirectory
                LogFileName      = $transcriptFileName
                StartTime        = $startTime
                EndTime          = $endTime
                DurationObj      = $endTime - $startTime
                Duration         = ($endTime - $startTime).ToString()
                FilterByRole     = $FilterByRole
                FilterByNode     = $FilterByNode
                TimeOutInMinutes = $TimeOutInMinutes
                MaxParallelJobs  = $MaxParallelJobs
                MaxZipSizeInMB   = $MaxZipSizeInMB
                FromDate         = $FromDate
                ToDate           = $ToDate
                TimeFrameObj     = $ToDate - $FromDate
                TimeFrame        = ($ToDate - $FromDate).ToString()
                ServersStatus    = $serversStatus | Select-Object `
                    Role, Name, IPAddress, ServerState, workerSizeName, ComputeMode, Reachable, `
                @{N = "CollectStatus"; E = { [Enum]::GetName([Status], $_.CollectStatus) } }, `
                @{N = "CopyStatus"; E = { [Enum]::GetName([Status], $_.CopyStatus) } }, `
                @{N = "CleanupStatus"; E = { [Enum]::GetName([Status], $_.CleanupStatus) } }
            }
            $actionTree | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $logDirectory -ChildPath "ActionTree.json")
            $actionTree | Select-Object * -ExcludeProperty ServersStatus, TimeFrameObj, DurationObj | Format-List

            # We stop the transcript before printing the colored status table because the output gets all scrambled
            Stop-Transcript | Write-Host -ForegroundColor Yellow

            if ($serversStatus) {
                $header = ($serversStatus | Format-Table | Out-String -Stream)[1]
                if ($thingsFound = [Regex]::Matches($header, "\w+ *")) {
                    ($serversStatus | Format-Table | Out-String -Stream)[0..2] | Write-Host
                    $serversStatus | ForEach-Object {
                        $item = $_
                        $thingsFound | ForEach-Object {
                            $match = $_
                            $property = $_.Value.TrimEnd()
                            switch ($property) {
                                { $_ -like "*Status" } {
                                    $colorParam = @{}
                                    if (($value = [Enum]::GetName([Status], $item.$property)) -and ($StatusColors[$value])) {
                                        $colorParam = @{ ForegroundColor = $StatusColors[$value] }
                                    }
                                    else {
                                        $value = "Undetermined"
                                        $colorParam = @{ ForegroundColor = $StatusColors[[Enum]::GetName([Status], 0)] }
                                    }
                                    $value = ($value).ToString().PadLeft($match.Length - 1, " ")
                                }
                                "Reachable" {
                                    $colorParam = @{}
                                    if ($item.$property -eq $true) {
                                        $colorParam = @{ ForegroundColor = [ConsoleColor]::Green }
                                    }
                                    else {
                                        $colorParam = @{ ForegroundColor = [ConsoleColor]::Red }
                                    }
                                    $value = ($item.$property).ToString().PadRight($match.Length - 1, " ")
                                }
                                Default {
                                    $colorParam = @{}
                                    if ($item.$property) {
                                        $value = ($item.$property).ToString().PadRight($match.Length - 1, " ")
                                    }
                                    else {
                                        $value = "".PadRight($match.Length - 1, " ")
                                    }
                                }
                            }
                            "{0} " -f $value | Write-Host @colorParam -NoNewline
                        }
                        Write-Host -ForegroundColor Yellow
                    }
                }
            }
            else {
                "Nothing collected" | Write-Host -ForegroundColor Yellow -BackgroundColor Red
            }

            Write-Host
            Write-Host
            "If logs are to be copied manually copy the contents of '{0}' from this computer using your prefered method" -f $logDirectory | Write-Host -ForegroundColor Yellow
            Write-Host
            "If logs are to be copied to an external share execute the following command (Requires outbound SMB/CIFS connectivity):" | Write-Host -ForegroundColor Yellow
            "'{0}' -LogBundlePath '{1}' -OutputSharePath '\\xx.xx.xx.xx\share' -OutputSharePathCreds (Get-Credential) -OutputShareDriveLetter X" -f $MyInvocation.MyCommand.Path, $logDirectory | Write-Host -ForegroundColor Yellow
            Write-Host
            "If logs are to be copied directly to a sasuri provided by the support enginner execute the following command (Requires outbound internet connectivity):" | Write-Host -ForegroundColor Yellow
            "'{0}' -LogBundlePath '{1}' OutputSasUri 'SASURI'" -f $MyInvocation.MyCommand.Path, $logDirectory | Write-Host -ForegroundColor Yellow
            Write-Host
        }
        "InstallAzCopy" {
            if ($azcopy = Get-ChildItem -Path "$dir\AzCopy.exe" -Recurse | Select-Object -ExpandProperty FullName) {
                return
            }

            do {
                Write-Host ""
                Write-Host "AzCopy is required. It was not detected in the default location. Would you like to download and install it? [Y/N]: " -f Yellow
                $choice = Read-Host
                Write-Host ""
                $ok = @("Y", "N") -contains $choice
                if ( -not $ok) { Write-Host "Invalid selection" }
            }
            until ( $ok )
            switch ( $choice ) {
                "Y" {
                    Write-Host "You selected Yes. Downloading AzCopy from http://aka.ms/downloadazcopy-v10-windows and installing to $dir"
                    Start-BitsTransfer -Source http://aka.ms/downloadazcopy-v10-windows -Destination $dir\$inst | Wait-Process
                    Import-Module Microsoft.PowerShell.Archive
                    Expand-Archive -Path $dir\$inst -DestinationPath $dir
                    if ($azcopy = Get-ChildItem -Path "$dir\AzCopy.exe" -Recurse | Select-Object -ExpandProperty FullName) {
                        "AzCopy is now available" | Write-Host -ForegroundColor Green
                    }
                    else {
                        "AzCopy is NOT available. Install it manually" | Write-Host -ForegroundColor Red
                    }
                }
                Default {
                    "AzCopy is NOT available. Install it manually" | Write-Host -ForegroundColor Red
                }
            }
        }
        "SendLogsNetworkShare" {
            $TotalSteps = 4
            $itemPath = Get-Item -Path $LogBundlePath
            try {
                $Step = 1
                $StepText = "Calculating size to copy"
                $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"'
                $StatusBlock = [ScriptBlock]::Create($StatusText)
                Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText
                $colItems = (Get-ChildItem $LogBundlePath  -Recurse | Measure-Object -Property length -Sum)
                $size = ("{0:N2}" -f ($colItems.sum / 1MB) + " MB")
                Write-Host ""
                Write-Host "Upload times will vary across environments. The total size of the log bundle is "-ForegroundColor Cyan -NoNewline
                Write-Host "$size" -ForegroundColor Yellow
                $count = $colItems.Count
                Write-Host "The number of files to copy is "-ForegroundColor Cyan -NoNewline
                Write-Host "$count" -ForegroundColor Yellow
                Write-Host ""

                $Step = 2
                $StepText = "Map network share"
                $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"'
                $StatusBlock = [ScriptBlock]::Create($StatusText)
                Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText

                New-AzsSupportNetworkShare -DriveLetter:$OutputShareDriveLetter -RemoteSharePath $OutputSharePath.FullName -RemoteShareCredentials $OutputSharePathCreds

                $Step = 2
                $StepText = "Copying the following items to {0}" -f $OutputSharePath.FullName
                $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"'
                $StatusBlock = [ScriptBlock]::Create($StatusText)
                Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText

                "Copying the following items to {0}`r`n`n{1}" -f $OutputSharePath.FullName, (`
                        $itemPath `
                    | Select-Object @{n = "Name"; e = { "`t$($_.FullName)" } } `
                    | Select-Object -ExpandProperty Name `
                    | Out-String `
                ) | Write-Host -ForegroundColor Cyan
                Copy-Item -Path $itemPath.FullName -Destination $OutputSharePath.FullName -Recurse -Force -Verbose

            }
            catch {
                throw $_
            }

        }
        "SendLogsSasUri" {
            $TotalSteps = 3
            $Step = 1
            $StepText = "Checking for AzCopy"
            $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"'
            $StatusBlock = [ScriptBlock]::Create($StatusText)
            Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText

            if (!($azcopy = Get-ChildItem -Path "$dir\AzCopy.exe" -Recurse | Select-Object -ExpandProperty FullName)) {
                "AzCopy is not installed. Run {0} -InstallAzCopy" -f $PSCmdlet.MyInvocation.MyCommand
                return
            }
            else {
                $Step = 2
                $StepText = "Calculating size upload"
                $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"'
                $StatusBlock = [ScriptBlock]::Create($StatusText)
                Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText


                $colItems = (Get-ChildItem $LogBundlePath  -Recurse | Measure-Object -Property length -Sum)
                $size = ("{0:N2}" -f ($colItems.sum / 1MB) + " MB")
                Write-Host ""
                Write-Host "Upload times will vary across environments. The total size of the log bundle is "-ForegroundColor Cyan -NoNewline
                Write-Host "$size" -ForegroundColor Yellow
                $count = $colItems.Count
                Write-Host "The number of files to upload is "-ForegroundColor Cyan -NoNewline
                Write-Host "$count" -ForegroundColor Yellow
                Write-Host ""
                Write-Host "Starting to upload with Azcopy. Please check log in " -ForegroundColor Cyan -NoNewline
                Write-Host "$($env:HOMEDIR)\.azcopy" -ForegroundColor Yellow -NoNewline
                Write-Host " to see if there are issues uploading" -ForegroundColor Cyan
                Write-Host ""

                $Step = 3
                $StepText = "Uploading"
                $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"'
                $StatusBlock = [ScriptBlock]::Create($StatusText)
                Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText

                Invoke-Command ([ScriptBLock]::Create("$azcopy copy ""$LogBundlePath"" ""$OutputSasUri"" --% --recursive --put-md5"))
                Write-Host "Azcopy run is finished." -ForegroundColor Yellow
            }
        }
    }
}

#region Global Vars
### Load Global vars to be used by functions
"[{0}] - Collecting App Service RP Information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
# Load Microsoft.Web.Hosting assembly
# 'Add-Type' is primarily used for loading .NET types (classes) and compiling C# code on-the-fly. It expects a full assembly name, including the version, culture, and public key token.
# It may also require that the assembly be available in the Global Assembly Cache (GAC) or in a specific directory.
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Hosting") | Out-Null


### Global Vars
$script:appServiceConfigGlobal = Get-AppServiceConfig -Type Global

#Load global:SiteManager
$script:SiteManager = New-Object Microsoft.Web.Hosting.SiteManager
"[{0}] - Completed App Service RP information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green

$startTime = Get-Date
[String]$currentDate = (Get-Date -Date $startTime -Format "yyyyMMdd-HHmmss").ToString()

$script:logDirectory = "c:\temp\Test-AzureStackAppServiceRP"

## Get ContentShare from global:SiteManager
$script:fileshare = $script:SiteManager.HostingConfiguration.ContentShare

## create fileshare var
$script:fileshareip = $script:fileshare -replace "^\\\\|\\.*$", ""

## Get Conn Strings
$script:meteringStr = Get-AppServiceConnectionString -Type:Metering
$script:hostingStr = Get-AppServiceConnectionString -Type:Hosting

## create sql ip var - fixed tcp:
$script:sqlserverip = ($script:hostingStr -replace "Data Source=(.*?);.*", '$1') -replace "^tcp:", ""

#endregion

function Test-AzsSupportAppService {
    <#
    .SYNOPSIS
        This script is used to collect App Service RP Health information and export it to a HTML report.
    .DESCRIPTION
        The script collects App Service RP information, including configuration, events, and certificates. It can export the collected data to a file.
    .PARAMETER ExportEventsFile
        If specified, the script will export last 24h of the App Service events to a file.
    .PARAMETER ExportGlobalConfigFile
        If specified, the script will export the global configuration to a file.
    .PARAMETER WebAppName
        The name of the web app to check. If Web App not found, it will prompt for selection.
    .EXAMPLE
        Test-AzsSupportAppService
        This example runs the script to test the health of the App Service RP (Default usage).
    .EXAMPLE
        Test-AzsSupportAppService -WebAppName "MyWebApp" (When a Web App is impacted)
        This example runs the script to test the health of the App Service RP for the specified web app without exporting events and global configuration.
        It will add the Web App Workers to all the checks performed.
    .EXAMPLE
       Test-AzsSupportAppService -ExportEventsFile -ExportGlobalConfigFile -WebAppName "MyWebApp" (specific scenarios)
        This example runs the script to test the health of the App Service RP, exporting both events and global configuration for the specified web app.
        
#>


    ### Parameters
    [CmdletBinding()]
    param (
        [Parameter()]
        [Switch]$ExportEventsFile = $false,
        [Parameter()]
        [Switch]$ExportGlobalConfigFile = $false,
        [Parameter()]
        [string]$WebAppName = ""
    )

    ###### Formating Functions #############
    function ConvertTo-HtmlTableWithHeaderAndTitle {
        [CmdletBinding()]
        param (
            [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
            [array]$InputObjects,
            [string]$TestGroup
        )

        process {
            $htmlOutput = ""

            foreach ($InputObject in $InputObjects) {
                $title = $InputObject.Title
                $data = $InputObject.Data

                # Convert and format to HTML Table
                $htmlTable = $data | ConvertTo-Html -Fragment 
            
                switch ($TestGroup) {
                    "AppServiceRPOverview" { $htmlTable = ProcessHTMLAppServiceRPOverview }
                    "AppServiceEvents" { $htmlTable = ProcessHTMLAppServiceEvents }
                    "Certificates" { $htmlTable = ProcessHTMLCertificates }
                    "SQLService" { $htmlTable = ProcessHTMLSQLService }
                    "VMGuestOS" { $htmlTable = ProcessHTMLVMGuestOS }
                }
            


                # Concatenate header with the HTML Table
                $htmlTableWithHeader = "<h2>$title</h2>$htmlTable"

                # ConvertTo-Html converts the output into HTML fragments. We need to remove some unnecessary tags and style it.
                $htmlTableWithHeader = $htmlTableWithHeader -replace '<col.*?>' -replace '</col.*?>' -replace '<!DOCTYPE html><html><head></head><body>' -replace '</body></html>' -replace '<tbody>' -replace '</tbody>' 

                $htmlOutput += $htmlTableWithHeader
            }

            Write-Output $htmlOutput
        }
    }
    function ProduceFinalHtml {
        $PageTitle = "<h3 align='center'>Test App Service executed at {0} from {1}</h3>" -f ((Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString()), $env:COMPUTERNAME
        $TestFileName = "{0}-Test-AzureStackAppServiceRP_{1}.html" -f $env:COMPUTERNAME, $currentDate
        $CSSstyle = @"
<style>
    body { font-family: Arial, sans-serif; /* Set default font to Arial */}
    table { border-collapse: collapse; border: 2px solid black; }
    th, td { border-left: 1px solid black; border-bottom: 1px solid black; padding-left: 5px; padding-right: 5px; padding-top: 1px; padding-bottom:1px;}
    th { background-color: lightgray; border: 2px solid black; min-width: 50px;}
    h2 { font-size: 15px; /* Same as h2 */
                font-weight: bold; /* Same as h2 */}
    .Alert { background-color: red !important; } /* Change background color for rows where Status is Stopped */
    .Warning { background-color: yellow !important; }
    .OK { background-color: green !important; } /* Change background color for rows where Status is Running */
    .Relevant { background-color: gray !important; } // used to mark rows with relevant info
                                                                         
    /* Styling the summary element to resemble the H1 tag */
    summary {
        font-size: 17px; /* Size similar to H1 */
        font-weight: bold;
        cursor: pointer;
        padding: 10px;
        border: 1px solid #ccc;
        margin: 10px 0;
        background-color: #f1f1f1;
        list-style: none; /* Remove default arrow */
    }
 
    /* Custom arrow for the summary element */
    summary::marker {
        content: '\25B6 '; /* Arrow for closed state */
    }
 
    /* Change the arrow icon when the collapsible is open */
    details[open] summary::marker {
        content: '\25BC '; /* Arrow for open state */
    }
 
    /* Styling the collapsible content */
    details {
        margin-bottom: 10px;
        border: 1px solid #ccc;
        padding: 10px;
        font-size: 17px;
    }
    /* Styling for the tag when content is "Alert" */
    .regionstatus[data-status="Alert"] {
        color: red; /* Red color for "Alert" */
        display: inline;
        background-color: #b6b6b6;
        padding: 11px;
        vertical-align: bottom;
    }
 
    /* Styling for the tag when content is "OK" */
    .regionstatus[data-status="OK"] {
        color: green; /* Green color for "OK" */
        display: inline;
        background-color: #b6b6b6;
        padding: 11px;
        vertical-align: bottom;
    }
     /* Styling for the tag when content is "Warning" */
    .regionstatus[data-status="Warning"] {
        color: yellow; /* Green color for "Warning" */
        display: inline;
        background-color: #b6b6b6;
        padding: 11px;
        vertical-align: bottom;
    }
    </style>
"@

        $Head = ConvertTo-Html -Title "Test title" -Head $CSSstyle 
        # We need to remove some unnecessary tags added by the convertto-html.
        $Head = $Head -replace '<col.*?>' -replace '</col.*?>' -replace '<!DOCTYPE html><html><head></head><body>' -replace '</body></html>' -replace '<tbody>' -replace '</tbody>' 

        #Get All segments toghether and output the file
        $Head, $PageTitle, $region_AppServiceConfig, $region_AppServiceOverview, $region_AppServiceCriticalEvents, $region_Certificates, $region_TestSQL, $region_TestFileshare, $region_CheckGuestVMs | Out-File -FilePath (Join-Path -Path $script:logDirectory -ChildPath $TestFileName) -Encoding UTF8 -Force
    }

    #### Funtions to Process HTML specific output for regions ######
    function ProcessHTMLAppServiceRPOverview {
        # Identify rows where Status is Stopped and apply CSS class
        $rows = $htmlTable -split '<tr>'
            

        # Identify the header row (5 row) and extract the column headers
        $headers = $rows[3] -split '</th>'

        # Find the index of the desired columns dynamically
        $statusColumnIndex = -1
        $AvailableWorkersColumnIndex = -1
        $RunningModeColumnIndex = -1
        $serverStateColumnIndex = -1
        $PendingOpColumnIndex = -1

        for ($i = 0; $i -lt $headers.Length; $i++) {
            #match column Status
            if ($headers[$i] -match 'Status') {
                $statusColumnIndex = $i
            }
            #match column ServerState
            if ($headers[$i] -match 'ServerState') {
                $serverStateColumnIndex = $i
            }
            #match column CPU
            if ($headers[$i] -match 'AvailableWorkerCount') {
                $AvailableWorkersColumnIndex = $i
            }
            #match column CPU
            if ($headers[$i] -match 'RunningMode') {
                $RunningModeColumnIndex = $i
            }
            if ($headers[$i] -match 'OperationName') {
                $PendingOpColumnIndex = $i
            }
        }

        #### Analyse and apply CSS classes according to rules
        # Iterate over each row (skipping the first element which contains table headers)
        for ($i = 4; $i -lt $rows.Length; $i++) {


            $row = $rows[$i]
            # Split the row into columns
            $columns = $row -split '</td>'
                
            # Check if the "Status" column contains "Stopped"
            if (($columns[$statusColumnIndex] -match 'Stopped') -or ($columns[$statusColumnIndex] -match 'Not ready') -and ($statusColumnIndex -ne -1)) {
                ##cell change
                $columns[$statusColumnIndex] = $columns[$statusColumnIndex] -replace "<td>", "<td class='Alert'>"
                $rows[$i] = $columns -join '</td>'
                # Add the CSS class to the row
                $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
            }

            # Check if the "Status" column contains "Ready"
            if (($columns[$statusColumnIndex] -match 'Ready') -and ($statusColumnIndex -ne -1)) {              
                ##cell change
                $columns[$statusColumnIndex] = $columns[$statusColumnIndex] -replace "<td>", "<td class='OK'>"
                $rows[$i] = $columns -join '</td>'
                # Add the CSS class to the row
                # $rows[$i] = "<tr class='OK'>$($rows[$i])</tr>"
            }

            # Check if the "ServerState" column contains "Offline"
            #if (($columns[$serverStateColumnIndex] -match 'Offline') -or ($columns[$serverStateColumnIndex] -match 'Installing') -and ($serverStateColumnIndex -ne -1)) {
            if (($columns[$serverStateColumnIndex] -match 'Offline') -or ($columns[$serverStateColumnIndex] -match 'NotReady') -or ($columns[$serverStateColumnIndex] -match 'Installing') -and ($serverStateColumnIndex -ne -1)) {
                ##cell change
                $columns[$serverStateColumnIndex] = $columns[$serverStateColumnIndex] -replace "<td>", "<td class='Alert'>"
                $rows[$i] = $columns -join '</td>'
                # Add the CSS class to the row
                $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
            }
                
            # Check if the "AvailableWorkerCount" column
            $AvailableWorkersvalue = $columns[$AvailableWorkersColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td>
            if (($AvailableWorkersvalue -eq 0) -and ($AvailableWorkersColumnIndex -ne -1)) {
                #change cells
                $columns[$AvailableWorkersColumnIndex] = $columns[$AvailableWorkersColumnIndex] -replace "<td>", "<td class='Alert'>"
                $rows[$i] = $columns -join '</td>'  
                # Add the CSS class to the row
                $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
            }
                
            # Check if the "Stopped" column
            if (($columns[$RunningModeColumnIndex] -match "Stopped") -and ($RunningModeColumnIndex -ne -1)) {
                ##cell change
                $columns[$RunningModeColumnIndex] = $columns[$RunningModeColumnIndex] -replace "<td>", "<td class='Alert'>"
                $rows[$i] = $columns -join '</td>'
                # Add the CSS class to the row
                $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
            }
            # Check if the "RunningMode" column
            if (($columns[$RunningModeColumnIndex] -match "Running") -and ($RunningModeColumnIndex -ne -1)) {
                ##cell change
                $columns[$RunningModeColumnIndex] = $columns[$RunningModeColumnIndex] -replace "<td>", "<td class='OK'>"
                $rows[$i] = $columns -join '</td>'
                # Add the CSS class to the row
                #$rows[$i] = "<tr class='RunningModeStopped'>$($rows[$i])</tr>"
            }

            # Check Pending operations
            if (($columns[$PendingOpColumnIndex] -match "Repair") -and ($PendingOpColumnIndex -ne -1)) {
                ##cell change
                $columns[$PendingOpColumnIndex] = $columns[$PendingOpColumnIndex] -replace "<td>", "<td class='Warning'>"
                $rows[$i] = $columns -join '</td>'
                # Add the CSS class to the row
                $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
            }
        }

        # Join the rows back into a single HTML table string
        $htmlTable = ($rows -join '</tr>') + "<br>"
        return $htmlTable
    }
    function ProcessHTMLAppServiceEvents {
        # Identify rows where Status is Stopped and apply CSS class
        $rows = ""
        $rows = $htmlTable -split '<tr>'
            

        # Identify the header row (row 3) and extract the column headers
        $headers = ""
        $headers = $rows[3] -split '</th>'

        # Find the index of the desired columns dynamically
        $LevelColumnIndex = -1


        for ($i = 0; $i -lt $headers.Length; $i++) {
            #match column Status
            if ($headers[$i] -match 'TraceLevel') {
                $LevelColumnIndex = $i
            }
        }

        #### Analyse and apply CSS classes according to rules
        # Iterate over each row (skipping the first element which contains table headers)
        for ($i = 4; $i -lt $rows.Length; $i++) {

            $row = $rows[$i]
            # Split the row into columns
            $columns = $row -split '</td>'

            ### used to use the value inside the cell without <td>
            $LevelStringValue = $columns[$LevelColumnIndex] -replace '<td>', '' 
                
            if ($columns[$LevelColumnIndex]) {
                # Check if the "Level" column
                if ([int]$LevelStringValue -lt 3 -and [int]$LevelStringValue -gt 1 ) {
                    ##cell change
                    $columns[$LevelColumnIndex] = $columns[$LevelColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
                if ([int]$LevelStringValue -lt 2) {
                    ##cell change
                    $columns[$LevelColumnIndex] = $columns[$LevelColumnIndex] -replace "<td>", "<td class='Alert'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }
        }
        # Join the rows back into a single HTML table string
        $htmlTable = ($rows -join '</tr>') + "<br>"
        return $htmlTable
    }
    function ProcessHTMLVMGuestOS {
        # Identify rows where Status is Stopped and apply CSS class
        $rows = ""
        $rows = $htmlTable -split '<tr>'
               
   
        # Identify the header row (row 3) and extract the column headers
        $headers = ""
        $headers = $rows[3] -split '</th>'
   
        # Find the index of the desired columns dynamically
        $FreeSpaceColumnIndex = -1
        $CPUColumnIndex = -1
        $CommitUseColumnIndex = -1
        $ServicesColumnIndex = -1
        $UpdatesColumnIndex = -1

        $FreeSpacevalue = 0
        $CPUvalue = 0
        $CommitUsevalue = 0
        $ServicesStringValue = 0
        $UpdatesStringValue = 0
   
   
        for ($i = 0; $i -lt $headers.Length; $i++) {
            #match column Status
            if ($headers[$i] -match 'Free Space') {
                $FreeSpaceColumnIndex = $i
            }
            if ($headers[$i] -match 'CPU') {
                $CPUColumnIndex = $i
            }
            if ($headers[$i] -match 'CommitUse') {
                $CommitUseColumnIndex = $i
            }
            if ($headers[$i] -match 'Status') {
                $ServicesColumnIndex = $i
            }
            if ($headers[$i] -match 'Result') {
                $UpdatesColumnIndex = $i
            }
        }

        #debug Write-Host $FreeSpaceColumnIndex ' index free space'
   
        #### Analyse and apply CSS classes according to rules
        # Iterate over each row (skipping the first element which contains table headers)
        for ($i = 4; $i -lt $rows.Length; $i++) {
   
            $row = $rows[$i]
            # Split the row into columns
            $columns = $row -split '</td>'
                   
            # Check if the "Free Space (GB)" column contains value below 10GB
            $FreeSpaceStringValue = $columns[$FreeSpaceColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td>
            if ([double]::TryParse($FreeSpaceStringValue, [ref]$FreeSpacevalue)) {
                   
                if ([int]($FreeSpacevalue) -cle 10 -and ($FreeSpaceColumnIndex -ne -1)) {                          
                    ##cell change
                    $columns[$FreeSpaceColumnIndex] = $columns[$FreeSpaceColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }

            # Check if CPU % column contains value above 90
            $CPUStringValue = $columns[$CPUColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td>
            if ([double]::TryParse($CPUStringValue, [ref]$CPUvalue)) {
                #debug Write-Host $FreeSpacevalue ' value free space Successfully parsed'
                if ([int]($CPUvalue) -gt 90 -and ($CPUColumnIndex -ne -1)) {
                    #debug Write-Host $FreeSpacevalue ' value free space int'
                    ##cell change
                    $columns[$CPUColumnIndex] = $columns[$CPUColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }

            # Check if commit in use is above 70%
            $CommitUseStringValue = $columns[$CommitUseColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td>
            if ([double]::TryParse($CommitUseStringValue, [ref]$CommitUsevalue)) {
                     
                if ([int]($CommitUsevalue) -gt 70 -and ($CommitUseColumnIndex -ne -1)) {
                              
                    ##cell change
                    $columns[$CommitUseColumnIndex] = $columns[$CommitUseColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }
       
            # Check Services are Running and style
            $ServicesStringValue = $columns[$ServicesColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td>
            if ($ServicesStringValue) {
                if ($ServicesStringValue -eq "Running" -and $ServicesColumnIndex -ne -1) {
                    ##cell change
                    $columns[$ServicesColumnIndex] = $columns[$ServicesColumnIndex] -replace "<td>", "<td class='OK'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
                if ($ServicesStringValue -ne "Running" -and $ServicesColumnIndex -ne -1) {
                    ##cell change
                    $columns[$ServicesColumnIndex] = $columns[$ServicesColumnIndex] -replace "<td>", "<td class='Alert'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }

            # Check WUpdates failed
            $UpdatesStringValue = $columns[$UpdatesColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td>
            if ($UpdatesStringValue) {
                if ($UpdatesStringValue -eq "Succeeded" -and $UpdatesColumnIndex -ne -1) {
                    ##cell change
                    $columns[$UpdatesColumnIndex] = $columns[$UpdatesColumnIndex] -replace "<td>", "<td class='OK'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
                if ($UpdatesStringValue -eq "Failed" -and $UpdatesColumnIndex -ne -1) {
                    ##cell change
                    $columns[$UpdatesColumnIndex] = $columns[$UpdatesColumnIndex] -replace "<td>", "<td class='Alert'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }
        }
        # Join the rows back into a single HTML table string
        $htmlTable = ($rows -join '</tr>') + "<br>"
        return $htmlTable
    }
    function ProcessHTMLCertificates {
        # Identify rows where Status is Stopped and apply CSS class
        $rows = ""
        $rows = $htmlTable -split '<tr>' 
   
        # Identify the header row (row 3) and extract the column headers
        $headers = ""
        $headers = $rows[3] -split '</th>'
   
        # Find the index of the desired columns dynamically
        $notAfterColumnIndex = -1
        $notAfterStringValue = 0              
        $StatusColumnIndex = -1

        for ($i = 0; $i -lt $headers.Length; $i++) {
            #match column Status
            if ($headers[$i] -imatch 'notAfter') {
                $notAfterColumnIndex = $i
            }
            if ($headers[$i] -imatch 'Status') {
                $StatusColumnIndex = $i
            }
                  
        }
   
        #### Analyse and apply CSS classes according to rules
        # Iterate over each row (skipping the first 4 elements which contains table headers)
        for ($i = 4; $i -lt $rows.Length; $i++) {
   
            $row = $rows[$i]
            # Split the row into columns
            $columns = $row -split '</td>'
               
            ### used to use the value inside the cell without <td>
            $notAfterStringValue = $columns[$notAfterColumnIndex] -replace '<td>', '' 
                    
            # Check if $notAfterStringValue is a valid DateTime
            $nullDate = [datetime]::MinValue
            $validDate = [datetime]::TryParse($notAfterStringValue, [ref]$nullDate)

            if ($validDate) {
                # Parse the date string into a DateTime object in UTC
                $targetDate = [datetime]::Parse($notAfterStringValue).ToUniversalTime()

                # Get the current date and time in UTC
                $currentDate = [datetime]::UtcNow

                # Calculate the difference using New-TimeSpan
                $timeDifference = New-TimeSpan -Start $currentDate -End $targetDate

                # Get the number of days left
                $daysLeft = $timeDifference.Days

                # Check if the date is close to the end of the month (e.g., within the last 30 days)
                if ($daysLeft -le 60) {               
                    $columns[$notAfterColumnIndex] = $columns[$notAfterColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
                if ($daysLeft -le 30) {           
                    $columns[$notAfterColumnIndex] = $columns[$notAfterColumnIndex] -replace "<td>", "<td class='Alert'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
                else {            
                    $columns[$notAfterColumnIndex] = $columns[$notAfterColumnIndex] -replace "<td>", "<td class='OK'>"
                    $columns[$StatusColumnIndex] = $columns[$StatusColumnIndex] -replace "<td>", "<td class='OK'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }           
        }
        # Join the rows back into a single HTML table string
        $htmlTable = ($rows -join '</tr>') + "<br>"
        return $htmlTable
    }
    function ProcessHTMLSQLService {
        # Identify rows where Status is Stopped and apply CSS class
        $rows = ""
        $rows = $htmlTable -split '<tr>' 
   
        # Identify the header row (row 3) and extract the column headers
        $headers = ""
        $headers = $rows[3] -split '</th>'
   
        ## Find the index of the desired columns dynamically
        $StateColumnIndex = -1
        $GrowthColumnIndex = -1
        $StateStringValue = 0
        $GrowthStringValue = 0
        $PercentFromMaxSizeColumnIndex = -1

        for ($i = 0; $i -lt $headers.Length; $i++) {
            #match column Status
            if ($headers[$i] -imatch 'State') {
                $StateColumnIndex = $i
            }
            if ($headers[$i] -imatch 'is_percent_growth') {
                $GrowthColumnIndex = $i
            }
            if ($headers[$i] -imatch 'PercentFromMaxSize') {
                $PercentFromMaxSizeColumnIndex = $i
            }
                  
        }

        #### Analyse and apply CSS classes according to rules
        # Iterate over each row (skipping the first 4 elements which contains table headers)
        for ($i = 4; $i -lt $rows.Length; $i++) {

            $row = $rows[$i]

            # Split the row into columns
            $columns = $row -split '</td>'
  
            ### used to use the value inside the cell without <td>
            $StateStringValue = $columns[$StateColumnIndex] -replace '<td>', '' 

            if ($columns[$StateColumnIndex]) {
                if ($StateStringValue -eq "ONLINE") {

                    $columns[$StateColumnIndex] = $columns[$StateColumnIndex] -replace "<td>", "<td class='OK'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                } 
                elseif ($StateStringValue -ne "ONLINE") {

                    $columns[$StateColumnIndex] = $columns[$StateColumnIndex] -replace "<td>", "<td class='Alert'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                } 
            }
            ### used to use the value inside the cell without <td>
            if ($columns[$GrowthColumnIndex]) {
                $GrowthStringValue = $columns[$GrowthColumnIndex] -replace '<td>', '' 

                if ($GrowthStringValue -eq "False") {
                    $columns[$GrowthColumnIndex] = $columns[$GrowthColumnIndex] -replace "<td>", "<td class='OK'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                } 
                else {
                    $columns[$GrowthColumnIndex] = $columns[$GrowthColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"

                } 
            }        
            ### used to use the value inside the cell without <td>
            if ($columns[$PercentFromMaxSizeColumnIndex]) {
                $PercentFromMaxSizeStringValue = $columns[$PercentFromMaxSizeColumnIndex] -replace '<td>', '' 

                if ($PercentFromMaxSizeStringValue -lt 80) {
                    $columns[$PercentFromMaxSizeColumnIndex] = $columns[$PercentFromMaxSizeColumnIndex] -replace "<td>", "<td class='OK'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                } 
                elseif ($PercentFromMaxSizeStringValue -gt 80 -and $PercentFromMaxSizeStringValue -lt 90) {
                    $columns[$PercentFromMaxSizeColumnIndex] = $columns[$PercentFromMaxSizeColumnIndex] -replace "<td>", "<td class='Warning'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"

                } 
                elseif ($PercentFromMaxSizeStringValue -gt 90 ) {
                    $columns[$PercentFromMaxSizeColumnIndex] = $columns[$PercentFromMaxSizeColumnIndex] -replace "<td>", "<td class='Alert'>"
                    $rows[$i] = $columns -join '</td>'
                    # Add the CSS class to the row
                    #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>"
                }
            }              
        }
        # Join the rows back into a single HTML table string
        $htmlTable = ($rows -join '</tr>') + "<br>"
        return $htmlTable
    }


    ###########
    # Main Logic
    ######## Start transcript Output to TXT #######
    Clear-host
    ##extend the UI size
    $host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(600, 0)

    New-Item -Path $script:logDirectory -ItemType Directory -Force -ErrorAction Ignore | Out-Null

    $transcriptFileName = "{0}-Test-AzureStackAppServiceRP_{1}.txt" -f $env:COMPUTERNAME, $currentDate
    Start-Transcript -Path (Join-Path -Path $script:logDirectory -ChildPath $transcriptFileName) | Write-Host -ForegroundColor Yellow

    #### MAIN #####
    "[{0}] - Collecting Config Global Information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_AppServiceConfig = Get-AzsSupportAppServiceConfigOutput -ExportHTML 
    "[{0}] - Completed Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    if (($PSBoundParameters.ContainsKey("ExportGlobalConfigFile")) -or ($ExportGlobalConfigFile -eq $true)) {
        Get-AzsSupportAppServiceConfigRedactandExport
    }
    else {
        Write-Host "Not Exporting Global Config to file"
    }
    "[{0}] - Checking App Service RP" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_AppServiceOverview = Get-AzsSupportAppServiceRPOverview -ExportHTML
    "[{0}] - Completed Checking App Service RP" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    "[{0}] - Checking App Service Events" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_AppServiceCriticalEvents = Get-AzsSupportAppServiceCriticalEvent -ExportHTML 
    "[{0}] - Completed Checking App Service Events" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    if (($PSBoundParameters.ContainsKey("ExportEventsFile")) -or ($ExportEventsFile -eq $true)) {
        $AppServiceEvents | ConvertTo-Json | Out-File (Join-Path -Path $script:logDirectory -ChildPath "AppServiceEvent.json") 
    }
    if (($PSBoundParameters.ContainsKey("WebAppName")) -and ($WebAppName -ne "")) {
        "[{0}] - Checking Web App $WebAppName Workers" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
        Get-AzsSupportWebAppDetails($WebAppName)
        "[{0}] - Checking Web App Workers" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green 
    }
    "[{0}] - Testing SQL Service" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_TestSQL = Test-AzsSupportAppServiceSQL -ExportHTML
    "[{0}] - Completed Testing SQL Service" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    "[{0}] - Testing FileShare" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_TestFileshare = Test-AzsSupportAppServiceFileShare -ExportHTML
    "[{0}] - Completed Testing FileShare" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    "[{0}] - Testing Certificates" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_Certificates = Test-AzsSupportAppServiceCertificates -ExportHTML
    "[{0}] - Completed Testing Certificates" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    "[{0}] - Checking Guest VMs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    $region_CheckGuestVMs = Test-AzsSupportVMguestOS -ExportHTML
    # making sure that all sessions were closed
    $null = get-pssession | Where-Object Name -like "WinRM*" | remove-pssession
    "[{0}] - Completed Checking Guest VMs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    "[{0}] - Processing HTML export" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    ProduceFinalHtml
    "[{0}] - Completed Processing HTML export" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
    $filePath = Join-Path -Path $script:logDirectory -ChildPath $TestFileName
    Write-Host ""
    Write-Host "Location: $filePath"
    # Remove the global variables
    if ($script:possibleWorkersNames) { Remove-Variable -Name possibleWorkersNames -Scope Global }
    #### Stop output to TXT
    Stop-Transcript | Write-Host -ForegroundColor Green
}

#region Exported Functions
##### Get App Service Config And export to TXT and Json #####
function Get-AzsSupportAppServiceConfigOutput {
    <#
    .SYNOPSIS
        This function retrieves the App Service configuration and exports it to a formatted table.
    .DESCRIPTION
        This function retrieves the App Service configuration and exports it to a formatted table.
    .EXAMPLE
        Get-AzsSupportAppServiceConfigOutput
        This example retrieves the most relevant App Service configuration.
    #>

    [CmdletBinding()]
    param (
        [switch]$ExportHTML
    )

    ## other method to retrieve the the config - it need to be worked the output from db before using it
    if ($script:appServiceConfigGlobal -eq "") {
        Write-host -ForegroundColor Red "Collecting Config Global Failed"
        Write-host -ForegroundColor Yellow "Trying via SQL DB"

        $script:hostingStr = Get-AppServiceConnectionString -Type Hosting
        $sqlQuery = @'
SELECT TOP (10) [ConfigurationKey], [ConfigurationValue], [RowVersion]
FROM [appservice_hosting].[runtime].[HostingConfigurations]
'@

        
        $sqlresult = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQuery

        $script:appServiceConfigGlobal = $sqlresult
    
    }

    $AppServiceConfigList = @{
        CloudId                     = $script:appServiceConfigGlobal.CloudId
        AdminExtensionUri           = $script:appServiceConfigGlobal.AdminExtensionUri
        ApplicationClientId         = $script:appServiceConfigGlobal.ApplicationClientId
        AppServicePortalUri         = $script:appServiceConfigGlobal.AppServicePortalUri
        CurrentControllerVersion    = $script:appServiceConfigGlobal.CurrentControllerVersion
        ControllersSubnetPrefix     = $script:appServiceConfigGlobal.ControllersSubnetPrefix
        DWASFilesFolderQuotaInGb    = $script:appServiceConfigGlobal.DWASFilesFolderQuotaInGb
        InfrastructureResourceGroup = $script:appServiceConfigGlobal.InfrastructureResourceGroup
        LogScavengerParameters      = $script:appServiceConfigGlobal.LogScavengerParameters
        MaxAllowedContentLength     = $script:appServiceConfigGlobal.MaxAllowedContentLength
        MaxAllowedFiles             = $script:appServiceConfigGlobal.MaxAllowedFiles
        MaxAllowedFolders           = $script:appServiceConfigGlobal.MaxAllowedFolders
        MicrosoftUpdateEnabled      = $script:appServiceConfigGlobal.MicrosoftUpdateEnabled
        PlatformVersion             = $script:appServiceConfigGlobal.PlatformVersion
        WorkerSubnetName            = $script:appServiceConfigGlobal.WorkerSubnetName
        RunWindowsUpdate            = $script:appServiceConfigGlobal.RunWindowsUpdate
        DIsableScaleSetSync         = $script:appServiceConfigGlobal.DIsableScaleSetSync
        FileShare                   = $script:fileshare
        SQLIP                       = $script:sqlserverip
    }
    # Create a custom object
    $configObject = New-Object PSObject -Property $AppServiceConfigList

    # Add the object to an array
    $configArray = @($configObject)

    $verticalTable = @()

    foreach ($config in $configArray) {
        foreach ($property in $config.psobject.properties) {
            $verticalTable += [PSCustomObject]@{
                Config = $property.Name
                Value  = $property.Value
            }
        }
    }


    $AppServiceConfigListTable = @{
        Title = ""
        Data  = $verticalTable
    }


    Write-Host "App Service Configuration:" -ForegroundColor Green
    Write-Host " "
    Write-Host "CloudId: "$script:appServiceConfigGlobal.CloudId 
    Write-Host "AdminExtensionUri: "$script:appServiceConfigGlobal.AdminExtensionUri 
    Write-Host "ApplicationClientId: "$script:appServiceConfigGlobal.ApplicationClientId 
    Write-host "AppServicePortalUri: "$script:appServiceConfigGlobal.AppServicePortalUri 
    Write-host "CurrentControllerVersion: "$script:appServiceConfigGlobal.CurrentControllerVersion 
    Write-host "ControllersSubnetPrefix: "$script:appServiceConfigGlobal.ControllersSubnetPrefix 
    Write-host "DWASFilesFolderQuotaInGb: "$script:appServiceConfigGlobal.DWASFilesFolderQuotaInGb 
    Write-host "InfrastructureResourceGroup:"$script:appServiceConfigGlobal.InfrastructureResourceGroup 
    Write-host "LogScavengerParameters: "$script:appServiceConfigGlobal.LogScavengerParameters 
    Write-host "MaxAllowedContentLength: "$script:appServiceConfigGlobal.MaxAllowedContentLength 
    Write-host "MaxAllowedFiles: "$script:appServiceConfigGlobal.MaxAllowedFiles 
    Write-host "MaxAllowedFolders: "$script:appServiceConfigGlobal.MaxAllowedFolders 
    Write-host "MicrosoftUpdateEnabled: "$script:appServiceConfigGlobal.MicrosoftUpdateEnabled 
    Write-host "PlatformVersion: "$script:appServiceConfigGlobal.PlatformVersion 
    Write-host "RunWindowsUpdate: "$script:appServiceConfigGlobal.RunWindowsUpdate 
    Write-host "DIsableScaleSetSync: "$script:appServiceConfigGlobal.DIsableScaleSetSync 
    Write-host "FileShare: "$script:fileshare 
    Write-host "SQL IP: "$script:sqlserverip 
    Write-host "WorkerSubnetName: "$script:appServiceConfigGlobal.WorkerSubnetName 
    Write-Host " "

    ### convert to html all tables
    if ($ExportHTML) {
        $region_AppServiceConfig = "<summary>App Service RP Config Info</summary>" + ($AppServiceConfigListTable | ConvertTo-HtmlTableWithHeaderAndTitle)
        $region_AppServiceConfig
    }
}
function Get-AzsSupportAppServiceConfigRedactandExport {
    <#
    .SYNOPSIS
        This function redacts sensitive information from the App Service configuration and exports it to JSON and TXT files.
    .DESCRIPTION
        This function redacts sensitive information from the App Service configuration and exports it to JSON and TXT files.
    .EXAMPLE
        Get-AzsSupportAppServiceConfigRedactandExport
        This example redacts sensitive information from the App Service configuration and exports it to JSON and TXT files.
    #>

    
    $AppServiceConfigGlobalRedacted = $script:appServiceConfigGlobal

    "[{0}] - Redacting secrets on Config Global information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan
    
    $AppServiceConfigGlobalRedacted.BitbucketClientId = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketClientSecret = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketNextClientId = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketNextClientSecret = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketProdClientId = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketProdClientSecret = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketStageClientId = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.BitbucketStageClientSecret = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.CertificatePassword = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.GitHubClientId = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.GitHubClientSecret = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.InfrastructureClientCertificatePassword = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.InfrastructureClientId = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.ManagementServerCertificatePassword = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.PublisherServerCertificatePassword = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.TokenRequestCertificatePassword = "[REDACTED]"
    $AppServiceConfigGlobalRedacted.UsageStorageAccountConnString = "[REDACTED]"
    $AppServiceConfigGlobalRedacted | ConvertTo-Json | Out-File (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.json")
    Write-host (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.json") -ForegroundColor Yellow
    $AppServiceConfigGlobalRedacted | Format-List | Out-String | Out-File (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.txt")
    Write-host (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.txt") -ForegroundColor Yellow
    "[{0}] - Completed Redacting Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green
}

####### Check App Service Events ###############
function Get-AzsSupportAppServiceCriticalEvent {
    <#
    .SYNOPSIS
        This function retrieves App Service events from the last day and checks for critical errors.
    .DESCRIPTION
        This function retrieves App Service events from the last day and checks for critical errors.
    .EXAMPLE
        Get-AzsSupportAppServiceCriticalEvent
        This example retrieves App Service events from the last day and checks for critical errors.
    #>

    [CmdletBinding()]
    param (
        [switch]$ExportHTML
    )

    ## collect app service evnts last day
    $AppServiceEvents = Get-AppServiceEvent -StartTime (get-date).AddDays(-1) -EndTime (get-date) 
    $AppServiceEventsList = @()
    ### output object if lelvel 2 errors found
    if (($AppServiceEvents | Where-Object { ($_.TraceLevel -cle 2) }).count -eq 0) { 
        write-host "No Errors found from the last day" -ForegroundColor Green 
        $regionstatus = "OK" 
        $AppServiceEventsListTable = @{
            title = "No Errors found from the last day"
        
        }
    } 
    else {
        Write-Warning 'Found App Service Error Events from last day'
        $AppServiceEventsList = $AppServiceEvents | Select-Object TimeStamp, ServerName, TraceLevel, Message  | Where-Object { ($_.TraceLevel -cle 2) } | Select-Object -First 100               
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        # Check if there are any events with TraceLevel equal to 1
        if ($AppServiceEventsList | Where-Object { $_.TraceLevel -eq 1 }) {
            # If found, update regionstatus to ALERT
            $regionstatus = "Alert"
        }
        $AppServiceEventsList | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
        $AppServiceEventsListTable = @{
            title = "App Service Error Events from last day"
            data  = $AppServiceEventsList
        }
    
    } 
    ### convert to html all Tables
    if ($ExportHTML) {
        $region_AppServiceCriticalEvents = "<br><details><summary>App Service Events - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($AppServiceEventsListTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "AppServiceEvents") + "</details>"
        $region_AppServiceCriticalEvents
    }
}

######### Check App Service RP #################
function Get-AzsSupportAppServiceRPOverview {
    <#
    .SYNOPSIS
        This function retrieves an overview of the App Service RP, including web apps, app service plans, SKU worker tiers, worker tiers, and instances.
    .DESCRIPTION
        This function retrieves an overview of the App Service RP, including web apps, app service plans, SKU worker tiers, worker tiers, and instances.
    .EXAMPLE
        Get-AzsSupportAppServiceRPOverview
        This example retrieves an overview of the App Service RP.
    #>

    [CmdletBinding()]
    param (
        [switch]$ExportHTML
    )

    Write-Host "Check Web Apps" -ForegroundColor Green 
    $WebAppList = $script:SiteManager.Sites | Select-Object SiteID, SiteName, ComputeMode, SKU, SubscriptionName, RunningMode, AlwaysOn, Kind, @{Name = "RunningWorkers"; Expression = { $_.RunningWorkers.WorkerName } }, @{Name = "ServerFarm"; Expression = { $_.ServerFarm.ServerFarmName } }, @{Name = "VirtualFarm"; Expression = { $_.VirtualFarm.VirtualFarmName } }, @{Name = "HostNames"; Expression = { $_.HostNames -join ', ' } } | Where-Object { $_.HostNames -notlike 'mawscanary*' } | Sort-Object SubscriptionName 
    Write-Verbose ($WebAppList | Format-Table * | Out-String)
    $WebAppListStopped = @($script:SiteManager.Sites | Select-Object SiteID, SiteName, ComputeMode, SKU, SubscriptionName, RunningMode, AlwaysOn, Kind, @{Name = "RunningWorkers"; Expression = { $_.RunningWorkers.WorkerName } }, @{Name = "ServerFarm"; Expression = { $_.ServerFarm.ServerFarmName } }, @{Name = "VirtualFarm"; Expression = { $_.VirtualFarm.VirtualFarmName } }, @{Name = "HostNames"; Expression = { $_.HostNames -join ', ' } } | Where-Object RunningMode -match "Stopped" | Where-Object { $_.HostNames -notlike 'mawscanary*' } | Sort-Object SubscriptionName )
    if ($WebAppListStopped.count -gt 0) {
        $message = "Found " + $WebAppListStopped.count + " Web App(s) in Stopped State"
        Write-Warning $message
        if ($regionstatus -ne "Alert") { if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } }
        $WebAppListStopped | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow
    }

    $WebAppListTable = @{
        Title = "Web Apps"
        Data  = $WebAppList
    }

    Write-Host "Check AppService Plans" -ForegroundColor Green
    $AppServicePlanList = Get-AppServiceServerFarm -force | Select-Object ServerFarmId, Name, Status, WebSpace, SubscriptionId, SKU, NumberOfWorkers, CurrentNumberOfWorkers, WorkerSize, ComputeMode | Sort-Object SubscriptionId
    Write-Verbose ($AppServicePlanList | Format-Table * | Out-String)
    $AppServicePlanListTable = @{
        Title = "AppService Plans"
        Data  = $AppServicePlanList
    }

    Write-Host "Check SKU Worker Tiers" -ForegroundColor Green
    $SKUList = $script:SiteManager.SkuWorkerTiers | Select-Object SkuId, SkuName, Family, WorkerTierId, WorkerTierName, StockSize, @{Name = "ComputeVMSize"; Expression = { $_.WorkerTier.ComputeVmSize } }, @{Name = "ComputeScaleSetName"; Expression = { $_.WorkerTier.ComputeScaleSetName } }, @{Name = "ComputeMode"; Expression = { $_.WorkerTier.ComputeMode } }
    Write-Verbose ($SKUList | Format-Table * | Out-String)
    $SKUListTable = @{
        Title = "SKU Worker Tiers"
        Data  = $SKUList
    }

    Write-Host "Check Worker Tiers" -ForegroundColor Green
    $WorkerTierList = $script:SiteManager.WorkerTiers | Where-Object ComputeType -eq "Pico" | Select-Object Name, ComputeScaleSetName, ComputeMode, ComputeInstances, UsedWorkerCount, RegisteredWorkerCount, ReadyWorkerCount, AvailableWorkerCount, Enabled, NumberOfCores, MemorySize, ComputeVmSize, ComputeScaleSetProvisioningState, StampCapacityAlias, Id, WorkerSize, ComputeImageReference 
    Write-Verbose ($WorkerTierList | Format-Table * | Out-String)
    $WorkerTierListEmpty = @($script:SiteManager.WorkerTiers | Where-Object ComputeType -eq "Pico" | Where-Object AvailableWorkerCount    -eq 0 | Select-Object Name, ComputeScaleSetName, ComputeMode, ComputeInstances, UsedWorkerCount, RegisteredWorkerCount, ReadyWorkerCount, AvailableWorkerCount, Enabled, NumberOfCores, MemorySize, ComputeVmSize, ComputeScaleSetProvisioningState, StampCapacityAlias, Id, WorkerSize, ComputeImageReference  )
    if ($WorkerTierListEmpty.count -gt 0) {
        $message = "Found " + $WorkerTierListEmpty.count + " Worker Tiers with no available workers"
        Write-Warning $message
        $regionstatus = "Alert"
        $WorkerTierListEmpty | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow
    }

    #Get-AppServiceWorkerTier | select WorkerSize, Name, Description, ComputeScaleSetName, ComputeInstances, ComputeMode, UsedWorkerCount, ReadyWorkerCount, AvailableWorkerCount, Id | FT -Wrap -AutoSize
    $WorkerTierListTable = @{
        Title = "Worker Tiers List"
        Data  = $WorkerTierList
    }

    Write-Host "Check All Instances " -ForegroundColor Green
    $Instances = Get-AppServiceServer
    #$Instances | FT *
    $InstanceDetails = @()

    foreach ($server in $Instances) {
        $InstanceInfo = [PSCustomObject]@{
            Name                    = $server.Name
            Status                  = $server.Status
            Role                    = $server.Role
            ServerState             = $server.ServerState
            StatusMessage           = $server.StatusMessage
            FeedUrl                 = $server.FeedUrl
            ReadyForLoadBalancing   = $server.ReadyForLoadBalancing
            IsDraining              = $server.IsDraining
            LastError               = $server.LastError
            SKU                     = ""
            AvailableSinceTimeStamp = ""
            WebApps                 = ""
            SiteCount               = ""
            IsDedicatedWorker       = ""
            ASP                     = ""
            MachineName             = ""
            LastHeartBeat           = ""
            IsCurrentOwner          = ""
            CurrentOperations       = $server.CurrentOperations
        }
        $InstanceDetails += $InstanceInfo
    }

    $workers = @()
    $workers = $script:SiteManager.Workers | Select-Object InstanceName, MachineName, IsDedicatedWorker, SiteCount, @{Name = "ASP"; Expression = { $_.VirtualFarm.VirtualFarmName } }, @{Name = "ASP-NumberOfWorkers"; Expression = { $_.VirtualFarm.TargetNumberOfWorkers } }, @{Name = "WebApps"; Expression = { $_.RunningSites.SiteName } }, @{Name = "SKU"; Expression = { $_.RunningSites.SKU | Select-Object -Unique } }, AvailableSinceTimeStamp
    foreach ($worker in $workers) {
        foreach ($server in $InstanceDetails) {
            if ($server.Name -eq $worker.InstanceName) {   
                $server.MachineName = $worker.MachineName 
                $server.ASP = $worker.ASP
                $server.IsDedicatedWorker = $worker.IsDedicatedWorker
                $server.SiteCount = $worker.SiteCount
                $server.WebApps = $worker.WebApps
                $server.SKU = $worker.SKU
                $server.AvailableSinceTimeStamp = $worker.AvailableSinceTimeStamp
                
            }
        }
       
    }

    $Controllers = @()
    $Controllers = $script:SiteManager.Controllers | Select-Object MachineName, IsCurrentOwner, LastHeartBeat
    foreach ($Controller in $Controllers) {
        foreach ($server in $InstanceDetails) {
            if ($server.Name -eq $Controller.MachineName) {   
                $server.IsCurrentOwner = $Controller.IsCurrentOwner 
                $server.LastHeartBeat = $Controller.LastHeartBeat
                
            }
        }
       
    }    

    
    ## full Table output
    $InstanceDetailsList = $InstanceDetails | Select-Object Name, MachineName, Status, Role, ServerState, IsCurrentOwner, IsDedicatedWorker, SiteCount, ReadyForLoadBalancing, IsDraining, LastHeartBeat, AvailableSinceTimeStamp, CurrentOperations, SKU, WebApps, ASP, @{Name = "LastMessages"; Expression = { $_.StatusMessage } }, LastError, FeedUrl | Sort-Object Role
    Write-Verbose ($InstanceDetailsList | Format-Table * | Out-String)
    $InstanceDetailsListTable = @{
        Title = "All Instances List"
        Data  = $InstanceDetailsList
    }
    
    ## Not Ready Table output
    $InstanceDetailsNotReady = @($InstanceDetails | Where-Object ServerState -NE "Ready" | Select-Object Name, MachineName, Status, Role, ServerState, IsCurrentOwner, IsDraining, CurrentOperations, SKU, WebApps, ASP | Sort-Object Role)
    if ($InstanceDetailsNotReady.count -gt 0) {
        Write-Host "Found " $InstanceDetailsNotReady.count " Instances(s) in Not Ready State" -ForegroundColor red
        $regionstatus = "Alert"       
        $InstanceDetailsNotReady | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow
    }
    ## Pending Table output
    #Write-Host "Pending Operations on Instances" -ForegroundColor Yellow
    $InstanceDetailsPendingOperations = @($InstanceDetails | Where-Object { -not [string]::IsNullOrEmpty($_.CurrentOperations) } | Select-Object Name, MachineName, Status, Role, ServerState, IsCurrentOwner, IsDraining, CurrentOperations, SKU, WebApps, ASP | Sort-Object Role)
    if ($InstanceDetailsPendingOperations.count -gt 0) {
        Write-Host "Found " $InstanceDetailsPendingOperations.count " Instances(s) with Operations Pending" -ForegroundColor red
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }        
        $InstanceDetailsPendingOperations | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow
    }
    
    ### Operations WFF
    write-host "Checking App Service WebFarm pending operations" -ForegroundColor green
    $AppServiceOperationsWFF = Get-AppServiceOperation -OperatorName WFF
    
    if ($AppServiceOperationsWFF) {
        Write-Host "Active RP Operations on Instances" -ForegroundColor Yellow
        #$AppServiceOperationsWFFlist = $AppServiceOperationsWFF | Select-Object OperationId, OperationName, OperatorName, Parameters, @{Name="ServerName"; Expression={$_.Parameters.ServerName}}, @{Name="WebFarmName"; Expression={$_.Parameters.WebFarmName}}, ConsumedTime
        $AppServiceOperationsWFFlist = $AppServiceOperationsWFF | Select-Object OperationId, OperationName, OperatorName, @{Name = "Parameters"; Expression = { $_.Parameters } }, ConsumedTime
        if ($regionstatus -ne "Alert") { if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } }
        $AppServiceOperationsWFFlist | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow
        $AppServiceOperationsWFFTable = @{
            Title = "Pending WebFarm Operations"
            Data  = $AppServiceOperationsWFFlist
        }
    
    }
    else {
        Write-Host "No App Service WebFarm Pending Operations found" -ForegroundColor Green
        $AppServiceOperationsWFFTable = @{
            Title = "No Pending WebFarm Operations"
            Data  = $AppServiceOperationsWFFlist
        }
    }

    ### Operations ActiveController
    write-host "Checking App Service Controller pending operations" -ForegroundColor green
    $AppServiceOperationsActiveController = Get-AppServiceOperation -OperatorName ActiveController
    
    if ($AppServiceOperationsActiveController) {
        Write-Host "Active RP Operations on Instances" -ForegroundColor Yellow
        #$AppServiceOperationsActiveControllerlist = $AppServiceOperationsActiveController | Select-Object OperationId, OperationName, OperatorName, Parameters, @{Name="ServerName"; Expression={$_.Parameters.ServerName}}, @{Name="WebFarmName"; Expression={$_.Parameters.WebFarmName}}, ConsumedTime
        $AppServiceOperationsActiveControllerlist = $AppServiceOperationsActiveController | Select-Object OperationId, OperationName, OperatorName, @{Name = "Parameters"; Expression = { $_.Parameters } }, ConsumedTime
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        $AppServiceOperationsActiveControllerlist | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow
        $AppServiceOperationsActiveControllerTable = @{
            Title = "Pending Controller Operations"
            Data  = $AppServiceOperationsActiveControllerlist
        }
    
    }
    else {
        Write-Host "No App Service Controller Pending Operations found" -ForegroundColor Green
        $AppServiceOperationsActiveControllerTable = @{
            Title = "No Pending Controller Operations"
            Data  = $AppServiceOperationsActiveControllerlist
        }
    }
    
    ### convert to html all Table'
    if ($ExportHTML) {
        $region_AppServiceOverview = "<br><details><summary>App Service Overview - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($WebAppListTable, $AppServicePlanListTable, $SKUListTable, $WorkerTierListTable, $InstanceDetailsListTable, $AppServiceOperationsWFFTable, $AppServiceOperationsActiveControllerTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "AppServiceRPOverview") + "</details>"
        $region_AppServiceOverview
    }
}

######### Web App View #################
function Select-WebApp {
    <#
    .SYNOPSIS
        This is a helper function that allows the user to select a web app from a paginated list.
    .DESCRIPTION
        This function allows the user to select a web app from a paginated list. It displays the web apps in pages of 100 and allows navigation through the pages.
    .EXAMPLE
        Select-WebApp -WebAppNameList $WebAppNameList
        This example displays the web apps in pages of 100 and allows the user to select a web app or navigate through the pages.
    .PARAMETER WebAppNameList
        An array of web app names to display in the paginated list.
    #>

    param (
        [Parameter(Mandatory = $true)]
        [Array[]]$WebAppNameList
    )

    # Calculate total number of pages
    $totalPages = [Math]::Ceiling($WebAppNameList.Count / 100)
    $currentPage = 1
    
    # Pagination loop
    while ($true) {
        # Display web apps for the current page
        $startIndex = ($currentPage - 1) * 100
        $endIndex = [Math]::Min($startIndex + 99, $WebAppNameList.Count - 1)
        
        Write-Host "Page $currentPage / $totalPages"
        Write-Host "Select a Web App:"
        
        for ($i = $startIndex; $i -le $endIndex; $i++) {
            $index = $i + 1
            Write-Host "$index. $($WebAppNameList[$i])"
        }
        
        # Prompt user to navigate or select a web app
        $selection = Read-Host "Enter the number of the Web App to select, or type 'n' for next page, 'p' for previous page, or 'q' to quit"
        if ($selection -eq 'q') {
            Write-Host "Exiting..."
            break
        }
        elseif ($selection -eq 'n' -and $currentPage -lt $totalPages) {
            $currentPage++
            Write-Host "`nPress any key to continue..."
            $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
            Write-Host "`n"
        }
        elseif ($selection -eq 'p' -and $currentPage -gt 1) {
            $currentPage--
            Write-Host "`nPress any key to continue..."
            $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
            Write-Host "`n"
        }
        elseif (-not [int]::TryParse($selection, [ref]$null)) {
            Write-Host "`nnot a valid key pressed..."
        }
        elseif ([int]$selection -ge 1 -and [int]$selection -le $WebAppNameList.Count) {
            $selectedWebAppName = $WebAppNameList[$selection - 1]
            Write-Host "You have selected: $selectedWebAppName"
            return $selectedWebAppName
        }
        else {
            Write-Host "Invalid selection. Please enter a valid number or command."
        }
    }
}
function Get-AzsSupportWebAppDetails {
    <#
    .SYNOPSIS
        This function retrieves details of a specific web app, including its workers and command line for log collection.
    .DESCRIPTION
        This function retrieves details of a specific web app, including its workers and command line for log collection.
    .EXAMPLE
        Get-AzsSupportWebAppDetails -WebAppName "MyWebApp"
        This example retrieves details of the specified web app, including its workers and command line for log collection.
    #>

    [CmdletBinding()]
    param (
        [string]$WebAppName = ""
    )

    $WebAppList = Get-AppServiceSite -RawView | Sort-Object SiteName
    $WebAppNameList = $WebAppList.SiteName
    if ([string]::IsNullOrWhiteSpace($WebAppName)) {
        Write-Warning -Message "Couldn't find any sites for the given web app."
        $selectedWebAppName = Select-WebApp -WebAppNameList $WebAppNameList
        Get-AzsSupportWebAppDetails -WebAppName $selectedWebAppName
        return
    }

    $sites = $script:SiteManager.Sites | Where-Object { $_.SiteName -like "$WebAppName" }

    if ($null -eq $sites) {
        Write-Warning -Message "Couldn't find any sites for the given web app '$WebAppName'"
        $selectedWebAppName = Select-WebApp -WebAppNameList $WebAppNameList
        Get-AzsSupportWebAppDetails -WebAppName $selectedWebAppName
        return
    }

    $sites | Select-Object SiteID, SiteName, HostNames, ComputeMode, SKU, RunningMode, AlwaysOn, RootDirectory | Format-Table -AutoSize

    $siteVirtualFarmId = $sites | Select-Object -ExpandProperty VirtualFarmId -Unique
    $siteVirtualFarm = $sites | Select-Object -ExpandProperty VirtualFarm -Unique
    $TargetWorkerSize = $siteVirtualFarm.TargetWorkerSize

    Write-Host "Possible Workers for WebApp $WebAppName" -ForegroundColor Green

    $possibleWorkers = $script:SiteManager.Workers | Where-Object {
        ($_.WorkerSize -like $TargetWorkerSize) -and ($_.VirtualFarmId -eq $siteVirtualFarmId)
    }

    $possibleWorkers | Select-Object Name, MachineName, SiteCount | Format-Table -AutoSize

    $script:possibleWorkersNames = $possibleWorkers | Select-Object -ExpandProperty Name
    $AppServiceLogFilterRoleCommand = "C:\temp\Get-AppServiceLogs.ps1 -FilterByRole WebWorker -workerCred (get-credential) -FilterByNode " + ($possibleWorkersNames -join ',')

    Write-Host "`nCommandline for log collection from Workers (copied to clipboard) --> `"$AppServiceLogFilterRoleCommand`"" -ForegroundColor DarkYellow
    $AppServiceLogFilterRoleCommand | Set-Clipboard
}

######### Helper Function: Evaluate Certificate Status #########
function Get-CertificateStatus {
    [CmdletBinding()]
    param (
        [datetime]$NotAfter,
        [datetime]$CurrentDate,
        [int]$NearExpirationThreshold,
        [int]$WarningThreshold
    )

    $notAfterDate = $NotAfter.ToUniversalTime()
    $daysLeft = (New-TimeSpan -Start $CurrentDate -End $notAfterDate).Days

    if ($notAfterDate -lt $CurrentDate) {
        return "Expired"
    }
    elseif ($daysLeft -le $NearExpirationThreshold) {
        return "Alert: Expires in $daysLeft days. Immediate action required!"
    }
    elseif ($daysLeft -le $WarningThreshold) {
        return "Warning: Expires in $daysLeft days. Please renew soon."
    }
    else {
        return "OK"
    }
}
### Aux Functions ###
function Get-CertificateDetails {
    param (
        [byte[]]$blob,
        [SecureString]$password
    )
    try {
        $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
        $unsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($ptr)

        $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $blob, $unsecurePassword
        $cert
    }
    catch {
        Write-Error "Error retrieving certificate details: $_"
    }
    finally {
        if ($unsecurePassword) {
            [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR(
                [Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
            )
        }
    }
}
function Get-CertificateDetailsFromConfig {
    $script:appServiceConfigGlobal = Get-AppServiceConfig -Type Global
    try {
        # Array of certificate types to query
        $certificateTypes = @{
            CertificateBlob                     = "Web Traffic Default SSL Cert";
            InfrastructureClientCertificateBlob = "SSO";
            ManagementServerCertificateBlob     = "API";
            PublisherServerCertificateBlob      = "FTP";
            TokenRequestCertificateBlob         = "SSO"
        }
        $certificates = @()
        # Loop through each certificate type
        foreach ($certificateType in $certificateTypes.keys) {
            #Write-Host "Querying $certificateType"
            $blob = $script:appServiceConfigGlobal.$certificateType
            $password = $script:appServiceConfigGlobal."$($certificateType.TrimEnd('Blob'))Password"
            $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
            $cert = Get-CertificateDetails -blob $blob -password $securePassword
            $cert | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CertType"  -Value $certificateType
            $cert | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CertAlias"  -Value $certificateTypes.$certificateType
            $certificates += $cert
        }
        $certificates
    }
    catch {
        Write-Error "Error retrieving certificate details from config: $_"
    }
}
function Get-ArmClientCertficate {
    try {
        $script:appServiceConfigGlobal = Get-AppServiceConfig -Type Global
        $adminDiscoverUrl = $script:appServiceConfigGlobal.AdminThumbprintsDiscoveryUri -replace '{ArmEndpoint}', ($script:appServiceConfigGlobal.ArmEndpoint -replace '^https://').TrimEnd('/')
        $tenantDiscoverUrl = $script:appServiceConfigGlobal.TenantThumbprintsDiscoveryUri -replace '{TenantArmEndpoint}', ($script:appServiceConfigGlobal.TenantArmEndpoint -replace '^https://').TrimEnd('/')

        try {
            $adminCertResponse = Invoke-WebRequest $adminDiscoverUrl
            $adminCert = (ConvertFrom-Json $adminCertResponse.Content).clientCertificates
            $adminCert | Add-Member -MemberType NoteProperty -Name "DiscoverUrl" -Value $adminDiscoverUrl
        }
        catch {
            Write-Error $_
        }

        try {
            $tenantCertResponse = Invoke-WebRequest $tenantDiscoverUrl
            $tenantCert = (ConvertFrom-Json $tenantCertResponse.Content).clientCertificates
            $tenantCert | Add-Member -MemberType NoteProperty -Name "DiscoverUrl" -Value $tenantDiscoverUrl
        }
        catch {
            Write-Error $_
        }
        $adminCert
        $tenantCert
    }
    catch {
        Write-Error "Error retrieving ARM certificate : $_"
    }
}

######### Check Certificates #########
function Test-AzsSupportAppServiceCertificates {
    <#
    .SYNOPSIS
        This function checks the status of ARM client certificates and App Service certificates, reporting any that are expired or near expiration.
    .DESCRIPTION
        This function checks the status of ARM client certificates and App Service certificates, reporting any that are expired or near expiration.
    .EXAMPLE
        Test-AzsSupportAppServiceCertificates -verbose
        This example checks the status of ARM client certificates and App Service certificates, providing certificates output.
    #>

    [CmdletBinding()]
    param (
        [switch]$ExportHTML
    )

    # Thresholds
    $nearExpirationThreshold = 30
    $warningThreshold = 60
    $currentDate = [datetime]::UtcNow
    $regionstatus = "OK"

    ######### ARM Client Certificates #########
    Write-Host "Checking ARM Client Certificates" -ForegroundColor Green
    $ArmClientCertificatesResult = Get-ArmClientCertficate

    $ArmClientCertificatesResult | ForEach-Object {
        $_ | Add-Member -MemberType NoteProperty -Name "Status" -Value (
            Get-CertificateStatus -NotAfter $_.NotAfter -CurrentDate $currentDate -NearExpirationThreshold $nearExpirationThreshold -WarningThreshold $warningThreshold
        )
    }
    ## Output the results even if no alerts
    Write-Verbose ($ArmClientCertificatesResult | Format-Table Thumbprint, NotBefore, NotAfter, DiscoverUrl, Status | Out-String)

    $ArmCertAlerts = $ArmClientCertificatesResult | Where-Object { $_.Status -ne "OK" }

    if ($ArmCertAlerts) {
        Write-Host "The following ARM certificates are expired or near expiration:" -ForegroundColor Yellow
        $ArmCertAlerts | Format-Table Thumbprint, NotBefore, NotAfter, DiscoverUrl, Status | Out-String | Write-Host -ForegroundColor Yellow
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
    }
    else {
        Write-Host "No expired or near-expiring ARM certificates found." -ForegroundColor Green
    }

    $ArmClientCertificatesResultTable = @{
        title = "ARM Client Certificates"
        data  = $ArmClientCertificatesResult | Select-Object Thumbprint, NotBefore, NotAfter, DiscoverUrl, Status
    }

    ######### App Service Certificates #########
    Write-Host "Checking App Service Certificates" -ForegroundColor Green
    $AppServiceCertificatesResult = Get-CertificateDetailsFromConfig

    $AppServiceCertificatesResult | ForEach-Object {
        $_ | Add-Member -MemberType NoteProperty -Name "Status" -Value (
            Get-CertificateStatus -NotAfter $_.NotAfter -CurrentDate $currentDate -NearExpirationThreshold $nearExpirationThreshold -WarningThreshold $warningThreshold
        )
    }
    ## Output the results even if no alerts
    Write-Verbose ($AppServiceCertificatesResult | Format-Table CertType, CertAlias, Thumbprint, NotBefore, NotAfter, Subject, Status | Out-String)

    $AppServiceCertAlerts = $AppServiceCertificatesResult | Where-Object { $_.Status -ne "OK" }

    if ($AppServiceCertAlerts) {
        Write-Host "The following App Service certificates are expired or near expiration:" -ForegroundColor Yellow
        $AppServiceCertAlerts | Format-Table Thumbprint, NotBefore, NotAfter, Subject, Status | Out-String | Write-Host -ForegroundColor Yellow
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
    }
    else {
        Write-Host "No expired or near-expiring App Service certificates found." -ForegroundColor Green
    }

    $AppServiceCertificatesResultTable = @{
        title = "App Service Certificates"
        data  = $AppServiceCertificatesResult | Select-Object CertType, CertAlias, Thumbprint, NotBefore, NotAfter, Subject, Status
    }

    ######### convert to html all Tables #########
    if ($ExportHTML) {
        $region_CheckCertificates = "<br><details><summary>Certificates - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" +
        ($ArmClientCertificatesResultTable, $AppServiceCertificatesResultTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "Certificates") +
        "</details>"
    
        return $region_CheckCertificates
    }
}

######### Check VM guest OS #################
function Test-AzsSupportVMguestOS {
    <#
    .SYNOPSIS
        This function checks the guest OS status of VMs in the App Service.
    .DESCRIPTION
        It verifies the OS version, updates, and other relevant information.
    .EXAMPLE
        Test-AzsSupportVMguestOS
        This example checks the guest OS status of VMs in the App Service.
    #>

    [CmdletBinding()]
    param (
        [switch]$ExportHTML
    )

    $regionstatus = "OK"

    ##################################
    #Checking Roles Disk Space status.
    #Unhealthy: Free space is less then 10GB
    #disk space, see Get-AzsSupportAppServiceRoleFreeSpace
    Write-Host "Check Free Space" -ForegroundColor Green
    $AppServiceRoleFreeSpaceResult = Get-AzsSupportAppServiceRoleFreeSpace  -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
    if ($script:possibleWorkersNames) { $AppServiceRoleFreeSpaceResult += Get-AzsSupportAppServiceRoleFreeSpace -Name $script:possibleWorkersNames }
    
    $FreeSpaceAlert = $AppServiceRoleFreeSpaceResult | Select-Object ComputerName, IPv4, Drive, "Free Space (GB)", "Total Capacity (GB)" | Where-Object { $_."Free Space (GB)" -lt 10 }
    
    if ($FreeSpaceAlert) { 
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        Write-Warning "Detected Low disk space on some of the Instances"
        $AppServiceRoleFreeSpaceResult | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
    }

    $AppServiceRoleFreeSpaceResultTable = @{
        title = "Free Space"
        data  = $AppServiceRoleFreeSpaceResult | Select-Object ComputerName, IPv4, Drive, "Free Space (GB)", "Total Capacity (GB)" | Sort-Object ComputerName
    }

    ################################
    #Checking Roles Hotfixes status.
    #Unhealthy: Installation result is Failed.
    #Hotfixes, see Test-AzsSupportAppServiceRoleUpdateStatus
    Write-Host "Check Windows Updates" -ForegroundColor Green
    $AppServiceRoleUpdateStatusResult = Test-AzsSupportAppServiceRoleUpdateStatus -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
    if ($script:possibleWorkersNames) { $AppServiceRoleUpdateStatusResult += Test-AzsSupportAppServiceRoleUpdateStatus -Name $script:possibleWorkersNames }
    
    # list failed
    $FailedUpdateResults = $AppServiceRoleUpdateStatusResult | Select-Object ComputerName, IPv4, KB, Date, Result, Title | Where-Object { $_.Result -eq "Failed" }

    if ($FailedUpdateResults) {
        # Display the filtered results with failed updates
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        Write-Warning "Detected Windows Updates Failed on the last 60 days on some of the Instances"
        $FailedUpdateResults | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
    }

    $AppServiceRoleUpdateStatusResultTable = @{
        title = "Windows Update in Past 60 Days(Except Security Intelligence Update)"
        data  = $AppServiceRoleUpdateStatusResult | Sort-Object Date -Descending
        #data = $AppServiceRoleUpdateStatusResult | Sort-Object PSComputerName
    }

    ###################################
    #Checking Roles Performance status.
    #Unhealthy: Cpu higher than 90%
    #Unhealthy: Available(GB) < RAM(GB)*0.03
    #Unhealthy: Commit(GB) > RAM(GB)*1.5
    #performance counters, see Test-AzsSupportAppServiceRolePerformance
    Write-Host "Check Role Performance" -ForegroundColor Green
    $AppServiceRolePerformanceResult = Test-AzsSupportAppServiceRolePerformance -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' 
    if ($script:possibleWorkersNames) { $AppServiceRolePerformanceResult += Test-AzsSupportAppServiceRolePerformance -Name $script:possibleWorkersNames }

    #check unhealthy status
    $rolesPerformanceAlert = $AppServiceRolePerformanceResult | Where-Object { $_."CPU(%)" -gt 90 -or $_."Available(GB)" / $_."RAM(GB)" -lt 0.03 -or $_."Commit(GB)" / $_."RAM(GB)" -gt 1.5 }

    if ($rolesPerformanceAlert) {
        # Display the filtered results
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        Write-Warning "Detected CPU usage higher than 90% OR Available(GB) < RAM(GB)*0.03 OR ommit(GB) > RAM(GB)*1.5 on some of the Instances"
        $rolesPerformanceAlert | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
    }

    $AppServiceRolePerformanceResultTable = @{
        title = "Performance"
        data  = $AppServiceRolePerformanceResult | Sort-Object ComputerName
    }

    ################################
    #Checking Roles services status.
    #Unhealthy: Requested services are not running
    #services status. See Test-AzsSupportAppServiceRoleServices
    Write-Host "Check Role Services" -ForegroundColor Green
    $AppServiceRoleServicesResult = Test-AzsSupportAppServiceRoleServices -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' 
    if ($script:possibleWorkersNames) { $AppServiceRoleServicesResult += Test-AzsSupportAppServiceRoleServices -Name $script:possibleWorkersNames }

    $rolesServicesAlert = $AppServiceRoleServicesResult | Where-Object { $_.Status -ne 'Running' }
    if ($rolesServicesAlert) {
        # Display the filtered results which services are not running.
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        Write-Warning "Detected requested services are not running on some of the Instances"
        $rolesServicesAlert | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
    }

    $AppServiceRoleServicesResult = @{
        title = "Services"
        data  = $AppServiceRoleServicesResult | Sort-Object ComputerName
    }

    ################################
    #Checking Roles processes status.
    #Unhealthy: Requested processes are not running
    #Processes status. See Test-AzsSupportAppServiceRoleProcesses
    Write-Host "Check Role Processes" -ForegroundColor Green
    $AppServiceRoleProcessesResult = Test-AzsSupportAppServiceRoleProcesses -Role 'LoadBalancer' 
    #if ($script:possibleWorkersNames) {$AppServiceRoleServicesResult += Test-AzsSupportAppServiceRoleServices -Name $script:possibleWorkersNames}

    $rolesProcessesAlert = $AppServiceRoleProcessesResult | Where-Object { $_.Status -ne 'Running' }
    if ($rolesProcessesAlert) {
        # Display the filtered results which processes are not running.
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        Write-Warning "Detected requested processes are not running on some of the Instances"
        $rolesServicesAlert | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
    }

    $AppServiceRoleProcessesResult = @{
        title = "Processes"
        data  = $AppServiceRoleProcessesResult | Sort-Object ComputerName
    }
    ### convert to html all Tables
    if ($ExportHTML) {
        $region_CheckGuestVMs = "<br><details><summary>VM Guest OS - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($AppServiceRoleFreeSpaceResultTable, $AppServiceRolePerformanceResultTable, $AppServiceRoleServicesResult, $AppServiceRoleProcessesResult, $AppServiceRoleUpdateStatusResultTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "VMGuestOS") + "</details>"
        $region_CheckGuestVMs
    }
}
Function New-AzsSupportAppServicePSsession {
    <#
    .SYNOPSIS
        Creates PowerShell sessions to app service roles computers.
 
    .DESCRIPTION
        This function creates PowerShell sessions to one or more remote computers specified by their name IP addresses.
 
    .PARAMETER ComputerName
        Specifies the name or IP address of the remote computers.
        This parameter supports aliases (-Name).
        Example: "LNV-W10--000001", "10.0.3.5"
 
    .EXAMPLE
        PS> New-AzsSupportAppServicePSsession -ComputerName "10.0.3.5","10.0.4.4","10.0.6.6","LNV-W10--000001"
    .EXAMPLE
        PS> New-AzsSupportAppServicePSsession -Role "ManagementServer", "Publisher", "LoadBalancer"
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")]
        [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')]
        [String[]]$Role,

        [Parameter(Mandatory = $true, ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")]
        [Alias("Name")]
        [string[]]$ComputerName
    )
    try {
        $sessions = @()
        $appServiceServer = Get-AppServiceServer

        $AllCredentials = Get-AppServiceConfig -Type Credential
    
        # Define a function to create a PSCredential object
        function Get-PSCredential {
            param (
                [String]$RoleName
            )
    
            $CredentialRAW = $AllCredentials | Where-Object { $_.CredentialName -eq $RoleName }
            $PSCredential = New-Object System.Management.Automation.PSCredential($CredentialRAW.UserName, (ConvertTo-SecureString -String $CredentialRAW.Password -AsPlainText -Force))
            return $PSCredential
        }
    
        # Build the hash table of Role to PSCredential
        $RoleToPSCredential = @{
            LoadBalancer     = Get-PSCredential -RoleName "FrontEndCredential"
            Publisher        = Get-PSCredential -RoleName "PublisherCredential"
            WebWorker        = Get-PSCredential -RoleName "WorkerCredential"
            ManagementServer = Get-PSCredential -RoleName "ManagementServerCredential"
            Controller       = Get-PSCredential -RoleName "ManagementServerCredential"
        }

        if ($PSCmdlet.ParameterSetName -eq "Role") {
            $ComputerName = $appServiceServer | Where-Object Role -In $Role | Select-Object -ExpandProperty Name
        }

        Foreach ($Machine in $ComputerName) {
            #Filter the IP or resolve the ComputerName to IP.
            if ([bool]($Machine -as [ipaddress] -and ($Machine.ToCharArray() | Where-Object { $_ -eq "." }).count -eq 3)) {
                $Server = $appServiceServer | Where-Object Name -EQ $Machine
            }
            else {
                $Server = $appServiceServer | Where-Object Name -EQ (Resolve-DnsName -Name $Machine | Select-Object -ExpandProperty IPAddress)
            } 
            
            if ($Server) {    
                $ServerName = $Server.Name
                $Credential = $RoleToPSCredential[$Server.Role.ToString()]
                
                $Session = New-PSSession -ComputerName $ServerName -Credential $Credential
                "PowerShell remoting session established to $ServerName." | Out-Null #place holder, trace logs in the future.
                $sessions += $Session
            }
            else {
                "$Machine is an invalid app service server." | write-host
            }
        }
        return $Sessions
    }
    catch {
        Write-Error "Failed to establish PowerShell remoting session to $ComputerName. Error: $_"
    }
}
function Get-AzsSupportAppServiceRoleFreeSpace {
    <#
    .SYNOPSIS
        This function retrieves the free disk space on app service roles.
    .DESCRIPTION
        It checks the free disk space on specified app service roles or computers and returns the results.
    .EXAMPLE
        Get-AzsSupportAppServiceRoleFreeSpace -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
        Get-AzsSupportAppServiceRoleFreeSpace -ComputerName "10.0.3.5","10.0.4.4","10.0.6.6","LNV-W10--000001"
    #>

    [CmdletBinding(DefaultParameterSetName = 'Role')]
    param (
        [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")]
        [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')]
        [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'),

        [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")]
        [Alias("Name")]
        [string[]]$ComputerName
    )
    try {
        $getFreeSpace = {
            $vol = Get-Volume -DriveLetter ($env:SYSTEMDRIVE).trimend(":")
            if ($vol) {
                $total = [math]::Round($vol.Size / 1024 / 1024 / 1024, 2)
                $remaining = [math]::Round($vol.SizeRemaining / 1024 / 1024 / 1024, 2)
        
                $freeSpace = New-Object -TypeName psobject
                $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
                $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "ComputerName"  -Value $env:computername
                $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "IPv4"  -Value $IPv4
                $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Drive" -Value $env:SYSTEMDRIVE
                $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Free Space (GB)"    -Value $remaining
                $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Total Capacity (GB)" -Value $total
                $freeSpace
            }
            else {
                throw "Unable to get Volume of system drive."
            }
        }

        if ($PSCmdlet.ParameterSetName -eq "Role") {
            $session = New-AzsSupportAppServicePSsession -Role $Role
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Name") {
            $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName
        }

        #$role = 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
        #$session = New-AzsSupportAppServicePSsession -role $Role
        $Result = Invoke-Command -Session $session -ScriptBlock $getFreeSpace  | Select-Object -Property *, PSComputerName, RunspaceId -ExcludeProperty PSComputerName, RunspaceId 
        $Result | Sort-Object ComputerName
    }
    catch {
        Write-Error "Failure on $ComputerName. Error: $_"
    }
    finally {
        if ($session) {
            Remove-PSSession -Session $session -Confirm:$false
        }
    } 
}
function Test-AzsSupportAppServiceRolePerformance {
    <#
    .SYNOPSIS
        This function tests the performance of app service roles by collecting various performance counters.
    .DESCRIPTION
        It retrieves performance counters such as CPU usage, memory availability, disk I/O, and network statistics for specified app service roles or computers.
    .PARAMETER Role
        Specifies the roles for which to collect performance data. Valid roles include 'Controller', 'ManagementServer', 'WebWorker', 'Publisher', and 'LoadBalancer'.
        If not specified, it defaults to all roles.
    .EXAMPLE
        Test-AzsSupportAppServiceRolePerformance -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
        Test-AzsSupportAppServiceRolePerformance -ComputerName "10.0.3.5","10.0.4.4","10.0.6.6","LNV-W10--000001"
    #>

    [CmdletBinding(DefaultParameterSetName = 'Role')]
    param (
        [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")]
        [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')]
        [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'),

        [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")]
        [Alias("Name")]
        [string[]]$ComputerName
    )
    try {
        $SampleInterval = 3
        $MaxSamples = 20
        $countersHash = @{"VMCPU(%)" = "\Processor(_total)\% processor time";
            "Available(GB)"          = "\Memory\Available Bytes";
            "Commit(GB)"             = "\Memory\committed bytes";
            "CommitUse(%)"           = "\Memory\% committed bytes in use";
            "NIC(MB/s)"              = "\Network Interface(*)\bytes total/sec";
            "DiskR(GB/s)"            = "\physicaldisk(_total)\disk read bytes/sec";
            "DiskR(IOPS)"            = "\physicaldisk(_total)\disk reads/sec";
            "RLatency(ms)"           = "\physicaldisk(_total)\avg. disk sec/read";
            "RQD"                    = "\physicaldisk(_total)\avg. disk read queue length";
            "DiskW(GB/s)"            = "\physicaldisk(_total)\disk write bytes/sec";
            "DiskW(IOPS)"            = "\physicaldisk(_total)\disk writes/sec";
            "WLatency(ms)"           = "\physicaldisk(_total)\avg. disk sec/write";
            "WQD"                    = "\physicaldisk(_total)\avg. disk write queue length";
            "HostCPU(%)"             = "\Hyper-V Hypervisor Logical Processor(_total)\% Total Run Time"
        }
        $countersList = $countersHash.Values -as [System.Object[]]
        #$session = New-AzsSupportAppServicePSsession -ComputerName $infVM
        #$session = New-AzsSupportAppServicePSsession -Role $Role
        if ($PSCmdlet.ParameterSetName -eq "Role") {
            $session = New-AzsSupportAppServicePSsession -Role $Role
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Name") {
            $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName
        }

        $perf = Invoke-Command -Session $session {
            (Get-Counter  -Counter $using:countersList  -SampleInterval $using:SampleInterval -MaxSamples $using:MaxSamples -ErrorAction SilentlyContinue).CounterSamples 
        }
        function Get-CounterValue {
            param (
                $CounterValues,
                [string]$CounterName
            )
            $ctrName = "*$CounterName*"
        ($CounterValues | Where-Object { $_.Path -like $ctrName }).CookedValue
        }

        function Select-CounterValueByMachine {
            param (
                $CounterValues,
                [string]$ComputerName
            )

            $CounterValues | Where-Object { $_.Path -match $ComputerName }
        }
        $vmSettings = Invoke-Command -Session $session {
            $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
            $vmSettings = @()
            $r = New-Object psobject
            $r | Add-Member -MemberType NoteProperty -name Name -Value $env:computername
            $r | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4
            $r | Add-Member -MemberType NoteProperty -name ProcessorCount -Value (Get-WmiObject -Class Win32_processor).NumberOfCores
            $r | Add-Member -MemberType NoteProperty -name MemoryStartup -Value (Get-WmiObject -Class Win32_ComputerSystem).TotalPhysicalMemory
            $vmSettings += $r   
            $vmSettings        
        } 
        $serverNameList = $vmSettings.Name 
        $counterValues = $perf
        $counterTable = @()
        foreach ($machine in  $serverNameList) {
            #Log-Info -Message "Checking counters on $machine"
            $MachineCounterValues = Select-CounterValueByMachine -CounterValues $counterValues -ComputerName $machine
            $counterDetails = New-Object -TypeName psobject

            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "ComputerName"  -Value $machine
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "IPv4"     -Value (($vmSettings | Where-Object Name -like $machine).IPv4)
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Cores"     -Value (($vmSettings | Where-Object Name -like $machine).ProcessorCount)
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CPU(%)"    -Value ([math]::Round((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("VMCPU(%)") -ComputerName $machine | Microsoft.PowerShell.Utility\Measure-Object -Average).Average, 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "RAM(GB)"   -Value ([math]::Round(($vmSettings | Where-Object Name -like $machine).MemoryStartup / 1GB))

            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Available(GB)"  -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("Available(GB)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average) / 1GB, 2))            
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Commit(GB)"    -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("Commit(GB)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average) / 1GB, 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CommitUse(%)"  -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("CommitUse(%)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "NIC(MB/s)"     -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("NIC(MB/s)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average / 1MB), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskR(GB/s)"   -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskR(GB/s)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average / 1GB), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskR(IOPS)"   -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskR(IOPS)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "RLatency(ms)"  -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("RLatency(ms)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average * 1000), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "RQD"           -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("RQD") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskW(GB/s)"   -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskW(GB/s)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average / 1GB), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskW(IOPS)"   -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskW(IOPS)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "WLatency(ms)"  -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("WLatency(ms)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average * 1000), 2))
            $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "WQD"           -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("WQD") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2))

            $counterTable += $counterDetails
        }

        $counterTable | Sort-Object ComputerName
    }
    catch {
        Write-Error "Failure on $ComputerName. Error: $_"
    } 
    finally {
        if ($session) {
            Remove-PSSession -Session $session -Confirm:$false
        }
    }
}
function Test-AzsSupportAppServiceRoleUpdateStatus {
    <#
    .SYNOPSIS
        This function checks the update status of app service roles.
    .DESCRIPTION
        It retrieves the update history for specified app service roles or computers and returns the results.
    .PARAMETER Role
        Specifies the roles for which to check update status. Valid roles include 'Controller', 'ManagementServer', 'WebWorker', 'Publisher', and 'LoadBalancer'.
        If not specified, it defaults to all roles.
    .PARAMETER ComputerName
        Specifies the name or IP address of the remote computers to check update status.
        This parameter supports aliases (-Name).
        Example: "LNV-W10--000001", "10.0.3.5"
    .EXAMPLE
        Test-AzsSupportAppServiceRoleUpdateStatus -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
        Test-AzsSupportAppServiceRoleUpdateStatus -ComputerName "10.0.3.5", "10.0.4.4", "10.0.6.6", "LNV-W10--000001"
 
    #>

    [CmdletBinding(DefaultParameterSetName = 'Role')]
    param (
        [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")]
        [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')]
        [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'),

        [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")]
        [Alias("Name")]
        [string[]]$ComputerName
    )

    try {
        $hotfixScript = {
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null
            Install-Module -Name PSWindowsUpdate -Force -Confirm:$false | Out-Null
            $WUHistory = Get-WUHistory -MaxDate ((Get-Date).AddDays(-60)) -Confirm:$false | Where-Object { $_.Title -notmatch "Security Intelligence Update for Microsoft Defender Antivirus" }
            $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
            $WUHistory | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4
            Remove-Module -Name PSWindowsUpdate -Force | Out-Null
            $WUHistory
        }

        if ($PSCmdlet.ParameterSetName -eq "Role") {
            $session = New-AzsSupportAppServicePSsession -Role $Role
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Name") {
            $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName
        }

        $data = Invoke-Command -Session $session -ScriptBlock $hotfixScript | Select-Object ComputerName, IPv4, KB, Date, Result, Title 
        $data | Sort-Object ComputerName
        
    }
    catch {
        Write-Error "Failure on $ComputerName. Error: $_"
    }
    finally {
        if ($session) {
            Remove-PSSession -Session $session -Confirm:$false
        }
    }
}
function Test-AzsSupportAppServiceRoleServices {
    <#
      .SYNOPSIS
        This function checks the status of services on app service roles.
    .DESCRIPTION
        It retrieves the status of specified services for app service roles or computers and returns the results.
    .PARAMETER Role
        Specifies the roles for which to check service status. Valid roles include 'Controller', 'ManagementServer', 'WebWorker', 'Publisher', and 'LoadBalancer'.
        If not specified, it defaults to all roles.
    .PARAMETER ComputerName
        Specifies the name or IP address of the remote computers to check service status.
        This parameter supports aliases (-Name).
        Example: "LNV-W10--000001", "10.0.3.5"
    .EXAMPLE
        Test-AzsSupportAppServiceRoleServices -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer'
        Test-AzsSupportAppServiceRoleServices -ComputerName "10.0.3.5", "10.0.4.4", "10.0.6.6", "LNV-W10--000001"
 
    #>

    [CmdletBinding(DefaultParameterSetName = 'Role')]
    param (
        [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")]
        [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')]
        [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'),

        [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")]
        [Alias("Name")]
        [string[]]$ComputerName
    )
    try {
        
        $serviceScript = {
            param(
                $appServiceServer,
                $PlatformVersion
            )

            $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
            $roleName = $appServiceServer | Where-Object { $_.Name -EQ $IPv4 } | Select-Object -ExpandProperty Role
            switch ($roleName) {
                "Controller" {      
                    $serviceList = @(
                        "WebFarmService",
                        "W3SVC",
                        "ResourceMetering"
                    )
                }
                "ManagementServer" {    
                    $serviceList = @(
                        "WebFarmAgentService",
                        "W3SVC",
                        "ImportExportService",
                        "ResourceMetering",
                        "UsageService"
                    )
                }
                "WebWorker" {    
                    $serviceList = @(
                        "WebFarmAgentService",
                        "DWASSvc",
                        "ResourceMetering"
                    )
                }
                "LoadBalancer" {  
                    if ($PlatformVersion -gt '102.0.0.0') {
                        $serviceList = @(
                            "WebFarmAgentService",
                            "esc",
                            "ResourceMetering"
                        )
                    }
                    else {  
                        $serviceList = @(
                            "WebFarmAgentService",
                            "W3SVC",
                            "esc",
                            "ResourceMetering"
                        )
                    }  
                   
                }
                "Publisher" {    
                    $serviceList = @(
                        "WebFarmAgentService",
                        "ResourceMetering",
                        "DWASSvc",
                        "ftpsvc"
                    )
                }
            }
            $services = Get-Service -Name $serviceList -ErrorAction SilentlyContinue
            $services = $services | Sort-Object -Property Name 
            $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
            $services | Add-Member -MemberType NoteProperty -name ComputerName -Value $env:computername
            $services | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4   
            $services | Add-Member -MemberType NoteProperty -name Role -Value $roleName               
            $services                 
        }
    
        if ($PSCmdlet.ParameterSetName -eq "Role") {
            $session = New-AzsSupportAppServicePSsession -Role $Role
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Name") {
            $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName
        }
        $appServiceServer = Get-AppServiceServer
        $data = Invoke-Command -Session $session -ScriptBlock $serviceScript -ArgumentList $appServiceServer, $script:appServiceConfigGlobal.PlatformVersion | Select-Object ComputerName, IPv4, Role, Name, Status, StartType
        $data | Sort-Object ComputerName
        
    }
    catch {
        Write-Error "Failure on $ComputerName. Error: $_"
    }
    finally {
        if ($session) {
            Remove-PSSession -Session $session -Confirm:$false
        }
    }
}
function Test-AzsSupportAppServiceRoleProcesses {
    <#
    .SYNOPSIS
        This function checks the status of processes on app service LoadBalancer role.
    .DESCRIPTION
        It retrieves the status of specified processes for app service LoadBalancer role or computers and returns the results.
    .PARAMETER Role
        Specifies the roles for which to check process status. Valid role is 'LoadBalancer'.
        If not specified, it defaults to 'LoadBalancer'.
    .PARAMETER ComputerName
        Specifies the name or IP address of the remote computers to check process status.
        This parameter supports aliases (-Name).
        Example: "LNV-FE---00001", "10.0.3.5"
    .EXAMPLE
        Test-AzsSupportAppServiceRoleProcesses -Role 'LoadBalancer'
        Test-AzsSupportAppServiceRoleProcesses -ComputerName "10.0.6.7", "LNV-FE---000000"
 
    #>

    [CmdletBinding(DefaultParameterSetName = 'Role')]
    param (
        [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")]
        [ValidateSet('LoadBalancer')]
        [String[]]$Role = @('LoadBalancer'),

        [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")]
        [Alias("Name")]
        [string[]]$ComputerName
    )

    try {
        
        $processScript = {
            param(
                $appServiceServer,
                $PlatformVersion
            )
            
            $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
            $roleName = $appServiceServer | Where-Object { $_.Name -EQ $IPv4 } | Select-Object -ExpandProperty Role
            switch ($roleName) {
                "LoadBalancer" {    
                    if ($PlatformVersion -gt '102.0.0.0') {
                        $processList = @(
                            "krpbfe",
                            "WebFarmAgentService"
                        )
                    }
                    else {
                        $processList = @(
                            "WebFarmAgentService"
                        )
                    }
                }
            }
            $processesOutput = @()
            $processes = Get-Process -Name $processList -ErrorAction SilentlyContinue
            foreach ($process in $processList) {
                if ($processes.Name -contains $process) {
                    $processStatus = "Running"
                    $FileVersion = ($processes | Where-Object { $_.Name -eq $process }).FileVersion
                }
                else {
                    $processStatus = "Stopped"
                    $FileVersion = ""
                }
                $customObject = New-Object PSObject -Property @{
                    'Name'        = $process
                    'Status'      = $processStatus
                    'FileVersion' = $FileVersion
                }
                $processesOutput += $customObject 
            }
            
            $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress
            $processesOutput | Add-Member -MemberType NoteProperty -name ComputerName -Value $env:computername
            $processesOutput | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4   
            $processesOutput | Add-Member -MemberType NoteProperty -name Role -Value $roleName               
            $processesOutput                 
        }
    
        if ($PSCmdlet.ParameterSetName -eq "Role") {
            $session = New-AzsSupportAppServicePSsession -Role $Role
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Name") {
            $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName
        }
        $appServiceServer = Get-AppServiceServer
        $data = Invoke-Command -Session $session -ScriptBlock $processScript -ArgumentList $appServiceServer, $script:appServiceConfigGlobal.PlatformVersion | Select-Object ComputerName, IPv4, Role, Name, Status, FileVersion
        $data | Sort-Object ComputerName
        
    }
    catch {
        Write-Error "Failure on $ComputerName. Error: $_"
    }
    finally {
        if ($session) {
            Remove-PSSession -Session $session -Confirm:$false
        }
    }
}

######### App Service File share / SQL Tests #################
## Define Invoke-SQL funtion to run SQL queries
function Invoke-SQL {
    <#
    .SYNOPSIS
        This function executes a SQL query against a specified SQL Server using the provided connection string.
    .DESCRIPTION
        It connects to the SQL Server using the provided connection string, executes the specified query, and returns the results as a DataTable.
    .PARAMETER ConnectionString
        The connection string to connect to the SQL Server.
    .PARAMETER Query
        The SQL query to be executed against the SQL Server.
    .EXAMPLE
        $connectionString = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password='';"
        $query = "SELECT * FROM myTable"
        $result = Invoke-SQL -ConnectionString $connectionString -Query $query
        This example connects to the SQL Server using the specified connection string and executes the query, returning the results as a DataTable.
    .OUTPUTS
        Returns a DataTable containing the results of the executed SQL query.
    .NOTES
        This function requires the System.Data.SqlClient assembly to be available in the PowerShell environment.
    #>

    [CmdletBinding()]
    param(
        [string]$ConnectionString,
        [string]$Query
    )
    $connection = New-Object System.Data.SqlClient.SqlConnection($ConnectionString)
    $command = New-Object System.Data.SqlClient.SqlCommand($Query, $connection)
    $connection.Open()
    $adapter = New-Object System.Data.SqlClient.SqlDataAdapter $command
    $dataset = New-Object System.Data.DataSet
    $adapter.Fill($dataSet) | Out-Null
    $connection.Close()
    return $dataSet.Tables
}
## check SQL DB
function Test-AzsSupportAppServiceSQL {
    <#
    .SYNOPSIS
        This function checks the SQL Server connection, version, database sizes, and trace messages in the App Service environment.
    .DESCRIPTION
        This function checks the SQL Server connection, version, database sizes, and trace messages in the App Service environment.
    .EXAMPLE
        Test-AzsSupportAppServiceSQL
     
    #>

    [CmdletBinding()]
    param (
        [switch]$ExportHTML
    )

    ## check connectivity
    $tncSQL = Test-NetConnection -Port 1433 $script:sqlserverip
    If ($tncSQL.TcpTestSucceeded -eq "True") {
        Write-Host "TCP connection Succeded" -ForegroundColor Green
        $regionstatus = "OK"
    
    }
    else {
        Write-Host "TCP connection Failed" -ForegroundColor Red
        $regionstatus = "Alert"
    }

    ## General SQL query
    $sqlQueryVersion = @'
SELECT @@VERSION
'@

    $sqlQueryHostingCheckDBSize = @'
SELECT [type]
      ,[type_desc]
      ,[name]
      ,[physical_name]
      ,[state]
      ,[state_desc]
      ,CAST(size AS bigint) * 8 / 1024 AS Size_MB
      ,CAST(max_size AS bigint) * 8 / 1024 AS MaxSize_MB
      ,CAST(growth AS bigint) * 8 / 1024 AS growth_MB
      ,[is_sparse]
      ,[is_percent_growth]
  FROM [appservice_hosting].[sys].[database_files]
'@

    $sqlQueryMeteringCheckDBSize = @'
SELECT [type]
      ,[type_desc]
      ,[name]
      ,[physical_name]
      ,[state]
      ,[state_desc]
      ,CAST(size AS bigint) * 8 / 1024 AS Size_MB
      ,CAST(max_size AS bigint) * 8 / 1024 AS MaxSize_MB
      ,CAST(growth AS bigint) * 8 / 1024 AS growth_MB
      ,[is_sparse]
      ,[is_percent_growth]
  FROM [appservice_metering].[sys].[database_files]
'@

    $sqlQueryTraceMessagesCount = @'
SELECT COUNT (*)
FROM [appservice_hosting].[runtime].[TraceMessages];
'@

    $sqlQueryTraceMessagesbyYear = @'
SELECT YEAR([Timestamp]) AS [Year],
       COUNT(*) AS [MessageCount]
FROM [appservice_hosting].[runtime].[TraceMessages]
GROUP BY YEAR([Timestamp])
ORDER BY [Year];
'@


    try {  
        ## check SQL version
        $SQLVersion = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryVersion
        $SQLVersionList = @{
            Version = $SQLVersion[0].Column1
        }

        # Create a custom object
        $SQLVersionconfigObject = New-Object PSObject -Property $SQLVersionList


        $SQLVersionTable = @{
            title = 'SQL Version and OS'
            data  = $SQLVersionconfigObject
        }
        Write-Host 'SQL Version and OS' -ForegroundColor Green
        Write-Host $SQLVersion[0].Column1


        ## Execute SQL tests
        Write-Host -ForegroundColor Green "Checking DB details"
    
        $HostingDBConnTime = Measure-Command { $sqlHostingDBSize = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryHostingCheckDBSize }
        $MeteringDBConnTime = Measure-Command { $sqlMeteringDBSize = Invoke-SQL -ConnectionString $script:meteringStr -Query $sqlQueryMeteringCheckDBSize }

        $sqlDBSize = @()
        $sqlDBSize += $sqlHostingDBSize 
        $sqlDBSize += $sqlMeteringDBSize
    
        # Initialize an empty array to store custom objects
        $sqlDBDetailsObjects = @()

        # Loop through each element in the array
        foreach ($item in $sqlDBSize) {
            # Calculate the percentage of current size against the max size
            if ($item.MaxSize_MB -gt 0) {
                $percentFromMaxSize = ($item.Size_MB / $item.MaxSize_MB) * 100  
            }
            else {
                $percentFromMaxSize = 0
            }

            if ($percentFromMaxSize -gt 80 -and $percentFromMaxSize -lt 90) { 
                $message = "Warning: Database $($item.name) on $($script:sqlserverip) is using $([math]::Round($percentFromMaxSize, 2))% of its maximum allowed size."
                if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
                Write-Warning $message
            }
            if ($percentFromMaxSize -gt 90 ) {
                $message = "Alert: Database $($item.name) on $($script:sqlserverip) is using $([math]::Round($percentFromMaxSize, 2))% of its maximum allowed size."
                $regionstatus = "Alert"
                Write-Warning $message
            }

            # Create a custom object for each element
            $customObject = New-Object PSObject -Property @{
                'SQL Server'         = $script:sqlserverip
                'Name'               = $item.name
                'DB Size MB'         = $item.Size_MB
                'State'              = $item.state_desc
                'physical_name'      = $item.physical_name
                'max_size MB'        = $item.MaxSize_MB
                'growth MB'          = $item.growth_MB
                'is_sparse'          = $item.is_sparse
                'is_percent_growth'  = $item.is_percent_growth
                'PercentFromMaxSize' = [math]::Round($percentFromMaxSize, 2)  # Rounded to 2 decimal places
            }

            # Add the custom object to the array
            $sqlDBDetailsObjects += $customObject
        }

        $sqlDBDetailsObjects = $sqlDBDetailsObjects | Select-Object "SQL Server", Name, "DB Size MB", State, physical_name, "max_size MB", "growth MB", is_sparse, is_percent_growth, 'PercentFromMaxSize'
        $sqlDBDetailsObjects | Format-Table | Out-String | Write-Host -ForegroundColor White

        $sqlDBDetailsTable = @{
            title = "Database details"
            data  = $sqlDBDetailsObjects
        }
        ## Alert if conn time is more than 2 seconds, expected to be below 1s
        if (($HostingDBConnTime.Seconds -gt 2) -or ($MeteringDBConnTime.Seconds -gt 2)) {
            Write-Warning 'Hosting DB connection Time: '$HostingDBConnTime.Seconds 'seconds' 
            Write-Warning 'Metering DB connection Time: '$MeteringDBConnTime.Seconds 'seconds' 
        }
    
        Write-Host -ForegroundColor Green "Checking Tables"
        $TraceMessagesbyYear = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryTraceMessagesbyYear
        $TraceMessagesCount = [int](Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryTraceMessagesCount)[0].Column1[0]

        $TraceMessagesTable = @{
            title = "TraceMessages Table is below 500k rows"
        }
    
        if ([int]$TraceMessagesCount -gt 500000) {
            Write-Warning "Found Number of Rows on Table TraceMessages > 500k "
            Write-Host $TraceMessagesCount " Messages Found" -ForegroundColor Yellow
            $TraceMessagesbyYear | Format-Table | Out-String | Write-Host -ForegroundColor Yellow
            if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
            $TraceMessagesTable = @{
                title = "TraceMessages Table"
                data  = $TraceMessagesbyYear | Select-Object Year, MessageCount
            }
        }

    }

    catch {
        Write-Error "Error processing SQL server with message: $($_.Exception.Message)"
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
    }
    ### convert to html all Tables
    if ($ExportHTML) {
        $region_TestSQL = "<br><details><summary>SQL Service - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($SQLVersionTable, $sqlDBDetailsTable, $TraceMessagesTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "SQLService") + "</details>"
        $region_TestSQL
    }
}

### Check FileServer performance ###
function Test-AzsSupportAppServiceFileShare {
    <#
    .SYNOPSIS
        This function tests the file share performance by writing and reading a file to/from the file share.
    .DESCRIPTION
        This function tests the file share performance by writing and reading a file to/from the file share.
    .EXAMPLE
        Test-AzsSupportAppServiceFileShare
    .EXAMPLE
        Test-AzsSupportAppServiceFileShare -FileSizeMB 50MB
    #>

    [CmdletBinding()]
    param(
        [switch]$ExportHTML,
        [int32]$FileSizeMB = 50MB
    )

    $LocalFilePath = "C:\temp\File1.txt"
    $RemoteFilePath = "$script:fileshare\File1.txt" 

    ### The values from transfer rates may not be relevant
    ### The intention is to test the User and Owner creds writing and reading from/to fileshare
  
    try {
        ## check connectivity
        $tncSQL = Test-NetConnection -Port 445 $script:fileshareip
        If ($tncSQL.TcpTestSucceeded -eq "True") {
            Write-Host "TCP connection Succeded" -ForegroundColor Green
            $regionstatus = "OK"
    
        }
        else {
            Write-Host "TCP connection Failed" -ForegroundColor Red
            if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        }

        ## get FileshareOwner password from global:SiteManager
        $fileshareOwner = $script:SiteManager.HostingConfiguration.FileShareOwnerCredential.UserName
        $fileshareOwnerpass = $script:SiteManager.HostingConfiguration.FileShareOwnerCredential.Password
        ## get FileshareUser password from global:SiteManager
        $fileshareuser = $script:SiteManager.HostingConfiguration.FileShareUserCredential.UserName
        $fileshareuserpass = $script:SiteManager.HostingConfiguration.FileShareUserCredential.Password 

        #create local file
        $f = new-object System.IO.FileStream "$LocalFilePath", Create, ReadWrite
        $f.SetLength(($FileSizeMB / 1GB) * 1GB)   ### trick to overcome the int64 convertion
        $f.Close()

        ### create SMB connection with Owner
        $timeconnectwrite = Measure-Command -Expression { New-SmbMapping -RemotePath $script:fileshare -Username $fileshareOwner -Password $fileshareOwnerpass }
    
        ## output info for screen
        Write-Host "Test Fileshare" $fileshare -ForegroundColor green
        write-host "Statistics for file" $filename "with size" $FileSizeMB -ForegroundColor green
        Write-Host "Time spent on first connect to the share" $timeconnectwrite -ForegroundColor green

        ## mesure write
        Write-Host "Write Test using user "$fileshareOwner  -ForegroundColor Green
        $timewrite = Measure-Command -Expression { Copy-Item -literalpath "$LocalFilePath" "$RemoteFilePath"  -Force } 

        ## Disconnect from share using owner
        Remove-SmbMapping -RemotePath $script:fileshare -force

        ### create SMB connection Read
        $null = New-SmbMapping -RemotePath $script:fileshare -Username $fileshareuser -Password $fileshareuserpass
        # mesure read
        Write-Host "Read Test using user "$fileshareuser -ForegroundColor Green
        $timeread = Measure-Command -Expression { Get-Item "$RemoteFilePath" } 

        ## Disconnect from share using User
        Remove-SmbMapping -RemotePath "$script:fileshare" -force

        $item = Get-Item "$LocalFilePath"

        $WriteRate = ($item.length / 1024 / 1024) / $timewrite.TotalSeconds
        $ReadRate = ($item.length / 1024 / 1024) / $timeread.TotalSeconds

        $resultwrite = New-Object -TypeName psobject -Property @{
            Operation       = "Write"
            Source          = $item.fullname
            TimeTaken       = [math]::round($timewrite.TotalSeconds, 2)
            TransferRateMBs = [math]::round($WriteRate, 2)
            User            = $fileshareOwner
        }

        $resultread = New-Object -TypeName psobject -Property @{
            Operation       = "Read"
            Source          = $item.fullname
            TimeTaken       = [math]::round($timeread.TotalSeconds, 2)
            TransferRateMBs = [math]::round($ReadRate, 2)
            User            = $fileshareuser
        }

        ###Clean UP files
        $null = New-SmbMapping -RemotePath $script:fileshare -Username $fileshareOwner -Password $fileshareOwnerpass
        Write-Host "Performing cleanup..." -ForegroundColor green
        Remove-Item "$LocalFilePath"
        Remove-Item "$RemoteFilePath"
        ## Disconnect from share using Owner
        Remove-SmbMapping -RemotePath $fileshare -force
    }

    catch {  
        Write-Error "Error processing FileShare with message: $($_.Exception.Message)"
        if ($regionstatus -ne "Alert") { $regionstatus = "Warning" }
        ## Disconnect from share using User in case previous failure
        $null = Remove-SmbMapping -RemotePath $script:fileshare -force
    }

    $FileshareTestResult = @()
    $FileshareTestResult += $resultwrite
    $FileshareTestResult += $resultread

    $FileshareTestResultTable = @{
        title = "FileShare Read Write Tests"
        data  = $FileshareTestResult
    }

    ### convert to html all Tables
    if ($ExportHTML) {
        $region_TestFileshare = "<br><details><summary>Fileshare Service - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($FileshareTestResultTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "Fileshare") + "</details>"
        $region_TestFileshare
    }
}
#endregion
# SIG # Begin signature block
# MIIoOQYJKoZIhvcNAQcCoIIoKjCCKCYCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAcZItVe1xDFOmc
# 81VMAsdq5crgWJGOe4aj8e1QuRhIXqCCDYUwggYDMIID66ADAgECAhMzAAAEA73V
# lV0POxitAAAAAAQDMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTEzWhcNMjUwOTExMjAxMTEzWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCfdGddwIOnbRYUyg03O3iz19XXZPmuhEmW/5uyEN+8mgxl+HJGeLGBR8YButGV
# LVK38RxcVcPYyFGQXcKcxgih4w4y4zJi3GvawLYHlsNExQwz+v0jgY/aejBS2EJY
# oUhLVE+UzRihV8ooxoftsmKLb2xb7BoFS6UAo3Zz4afnOdqI7FGoi7g4vx/0MIdi
# kwTn5N56TdIv3mwfkZCFmrsKpN0zR8HD8WYsvH3xKkG7u/xdqmhPPqMmnI2jOFw/
# /n2aL8W7i1Pasja8PnRXH/QaVH0M1nanL+LI9TsMb/enWfXOW65Gne5cqMN9Uofv
# ENtdwwEmJ3bZrcI9u4LZAkujAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU6m4qAkpz4641iK2irF8eWsSBcBkw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMjkyNjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AFFo/6E4LX51IqFuoKvUsi80QytGI5ASQ9zsPpBa0z78hutiJd6w154JkcIx/f7r
# EBK4NhD4DIFNfRiVdI7EacEs7OAS6QHF7Nt+eFRNOTtgHb9PExRy4EI/jnMwzQJV
# NokTxu2WgHr/fBsWs6G9AcIgvHjWNN3qRSrhsgEdqHc0bRDUf8UILAdEZOMBvKLC
# rmf+kJPEvPldgK7hFO/L9kmcVe67BnKejDKO73Sa56AJOhM7CkeATrJFxO9GLXos
# oKvrwBvynxAg18W+pagTAkJefzneuWSmniTurPCUE2JnvW7DalvONDOtG01sIVAB
# +ahO2wcUPa2Zm9AiDVBWTMz9XUoKMcvngi2oqbsDLhbK+pYrRUgRpNt0y1sxZsXO
# raGRF8lM2cWvtEkV5UL+TQM1ppv5unDHkW8JS+QnfPbB8dZVRyRmMQ4aY/tx5x5+
# sX6semJ//FbiclSMxSI+zINu1jYerdUwuCi+P6p7SmQmClhDM+6Q+btE2FtpsU0W
# +r6RdYFf/P+nK6j2otl9Nvr3tWLu+WXmz8MGM+18ynJ+lYbSmFWcAj7SYziAfT0s
# IwlQRFkyC71tsIZUhBHtxPliGUu362lIO0Lpe0DOrg8lspnEWOkHnCT5JEnWCbzu
# iVt8RX1IV07uIveNZuOBWLVCzWJjEGa+HhaEtavjy6i7MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAQDvdWVXQ87GK0AAAAA
# BAMwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKsK
# FR7FXNBRbrkX2KoKbbSOyyguhZ41FyYb/+DY4hcuMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAnd4+Oydr4UUWZi4lZ2pVYEnuwgJf4VFhFGMl
# yRQBbavw2U2Z6za6UWFP2VZwRvHYcu8PRpjGAc6+amStGJxJNZlwMCRNmXSZBWBw
# l7pHCGf1sS99BXQ3bAYdCvJCyhYNqjIKyXwHnnjgtk7+lTjCNOkj9Mpf5wuZ2fr6
# /zKts3iI3uDKCV3JC8U1wvAChWnzA09sehAehrK4ViJ7E/EORLnOAey2OZwBTvsy
# lYkv9QQfJ9DQna5arKvAw9c5bRRFDOV4Mgsaof8Hx097Lb/xkJHt68WCeXlsgX8m
# UdkrLxt4bSmcX+N4ac/Wok38IQi7vjG7xEnkCu10SGENZJJ2paGCF5QwgheQBgor
# BgEEAYI3AwMBMYIXgDCCF3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCAF5lP5VHbUDHD5e+Yyw7qerNZgGACjO9T+
# eWalFuorBAIGaEtWkPcGGBMyMDI1MDYyNzE2NTgxNi44NTJaMASAAgH0oIHRpIHO
# MIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxk
# IFRTUyBFU046MzMwMy0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFNlcnZpY2WgghHqMIIHIDCCBQigAwIBAgITMwAAAg9XmkcUQOZG5gAB
# AAACDzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MDAeFw0yNTAxMzAxOTQzMDRaFw0yNjA0MjIxOTQzMDRaMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzMwMy0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Uw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCl6DTurxf66o73G0A2yKo1
# /nYvITBQsd50F52SQzo2cSrt+EDEFCDlSxZzWJD7ujQ1Z1dMbMT6YhK7JUvwxQ+L
# kQXv2k/3v3xw8xJ2mhXuwbT+s1WOL0+9g9AOEAAM6WGjCzI/LZq3/tzHr56in/Z+
# +o/2soGhyGhKMDwWl4J4L1Fn8ndtoM1SBibPdqmwmPXpB9QtaP+TCOC1vAaGQOds
# qXQ8AdlK6Vuk9yW9ty7S0kRP1nXkFseM33NzBu//ubaoJHb1ceYPZ4U4EOXBHi/2
# g09WRL9QWItHjPGJYjuJ0ckyrOG1ksfAZWP+Bu8PXAq4s1Ba/h/nXhXAwuxThpva
# Fb4T0bOjYO/h2LPRbdDMcMfS9Zbhq10hXP6ZFHR0RRJ+rr5A8ID9l0UgoUu/gNvC
# qHCMowz97udo7eWODA7LaVv81FHHYw3X5DSTUqJ6pwP+/0lxatxajbSGsm267zqV
# NsuzUoF2FzPM+YUIwiOpgQvvjYIBkB+KUwZf2vRIPWmhAEzWZAGTox/0vj4eHgxw
# ER9fpThcsbZGSxx0nL54Hz+L36KJyEVio+oJVvUxm75YEESaTh1RnL0Dls91sBw6
# mvKrO2O+NCbUtfx+cQXYS0JcWZef810BW9Bn/eIvow3Kcx0dVuqDfIWfW7imeTLA
# K9QAEk+oZCJzUUTvhh2hYQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFJnUMQ2OtyAh
# LR/MD2qtJ9lKRP9ZMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8G
# A1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBs
# BggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy
# MDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH
# AwgwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBTowbo1bUE7fXT
# y+uW9m58qGEXRBGVMEQiFEfSui1fhN7jS+kSiN0SR5Kl3AuV49xOxgHo9+GIne5M
# pg5n4NS5PW8nWIWGj/8jkE3pdJZSvAZarXD4l43iMNxDhdBZqVCkAYcdFVZnxdy+
# 25MRY6RfaGwkinjnYNFA6DYL/1cxw6Ya4sXyV7FgPdMmxVpffnPEDFv4mcVx3jvP
# Zod7gqiDcUHbyV1gaND3PejyJ1MGfBYbAQxsynLX1FUsWLwKsNPRJjynwlzBT/OQ
# bxnzkjLibi4h4dOwcN+H4myDtUSnYq9Xf4YvFlZ+mJs5Ytx4U9JVCyW/WERtIEie
# TvTRgvAYj/4Mh1F2Elf8cdILgzi9ezqYefxdsBD8Vix35yMC5LTnDUoyVVulUeeD
# AJY8+6YBbtXIty4phIkihiIHsyWVxW2YGG6A6UWenuwY6z9oBONvMHlqtD37ZyLn
# 0h1kCkkp5kcIIhMtpzEcPkfqlkbDVogMoWy80xulxt64P4+1YIzkRht3zTO+jLON
# u1pmBt+8EUh7DVct/33tuW5NOSx56jXQ1TdOdFBpgcW8HvJii8smQ1TQP42HNIKI
# JY5aiMkK9M2HoxYrQy2MoHNOPySsOzr3le/4SDdX67uobGkUNerlJKzKpTR5ZU0S
# eNAu5oCyDb6gdtTiaN50lCC6m44sXjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKb
# SZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmlj
# YXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIy
# NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXI
# yjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjo
# YH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1y
# aa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v
# 3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pG
# ve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viS
# kR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYr
# bqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlM
# jgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSL
# W6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AF
# emzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIu
# rQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIE
# FgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWn
# G1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEW
# M2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5
# Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi
# AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV
# 9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3Js
# Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAx
# MC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2
# LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv
# 6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZn
# OlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1
# bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4
# rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU
# 6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDF
# NLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/
# HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdU
# CbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKi
# excdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTm
# dHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZq
# ELQdVTNYs6FwZvKhggNNMIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJp
# Y2EgT3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjMzMDMtMDVF
# MC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMK
# AQEwBwYFKw4DAhoDFQBetIzj2C/MkdiI03EyNsCtSOMdWqCBgzCBgKR+MHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7AjxfjAi
# GA8yMDI1MDYyNzEwMzEyNloYDzIwMjUwNjI4MTAzMTI2WjB0MDoGCisGAQQBhFkK
# BAExLDAqMAoCBQDsCPF+AgEAMAcCAQACAim2MAcCAQACAhPGMAoCBQDsCkL+AgEA
# MDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAI
# AgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBAOFK/2jIPrzX1jwrzRBCs+k4O+BL
# GGM9w7cuytRDVBZM8LSN/zMGLS+yz+8IoifD9SMTxhAiGY070LPmbE9f6x3uLhH5
# zLpliW2WzuwdpemZ3KDJp4zGoicbPP6iqnaZvdrwQ8+vbODZSsMxmxe4fkDJs7pS
# sdXA+5r9SPUavzBW+WvHGhfl99htUj6JRG2wfn2yOYSzqKDg7nW+s2vSjj6pEfpH
# leWhKe4WESVEVh6UwwqjH6kDqInbs9Oon3yf/uxJXsEQ3AWoUDpxduj/mh75VvMD
# yVps6tw2sGXl2WYhxphJMS2Abj/ZF5E7H81PuR0hGqjAotdL4+Fcl57EIaUxggQN
# MIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAg9X
# mkcUQOZG5gABAAACDzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0G
# CyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCBd8LbN5NROBHR3diFLIb8O99Pu
# 1v5h8FIaQ8+P7XLU1zCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIN1Hd5Um
# Knm7FW7xP3niGsfHJt4xR8Xu+MxgXXc0iqn4MIGYMIGApH4wfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAIPV5pHFEDmRuYAAQAAAg8wIgQgv4CYMLP4
# ZXsKD/8L0tMxoMEinmsX3Zd/mZ6wZo5moHYwDQYJKoZIhvcNAQELBQAEggIAgi90
# JVW3Ll4ADUCRQR5pe9MxQ0Blolz+Y4wk8MW5WIWHXn7ka0Y396Ia/kS/zoxfxfXp
# +kZRfl525be4vbRBejrvMboyvEMeevLd2lwK65PBpI7fyMCu6BshgzrfC4DP1jGv
# jU5eHYwj+K1TV7Xy7QuKPsuVM0mUtiRYQc8oGvl318sUdh2taOkX6X59CB2DccO3
# YtizdULzjSjAJIcOJXPdKhDBP2LO4ce+BbZm3SttutEMl4Prl8caElXONZrrDbF/
# VAdO51+9A0Q4hfh6kbWD443WNqfeUhbqLkN/wBJ7/OaDZYg/QU+vqgw0ruxlE1gb
# KRo1d6kpQTYHodhMNGN/AVrf+aiJO9XIJrp8WrEnaLaSMrIeOOz6Ct1qScEq/ai9
# 1l2bQEUyN1jP/gET4hAJwFQx7SCzWhfSNusdEmblpddPlKuFUR/N1L3eicHPwG17
# ISWrwDnmUw53dNFY1DI2uStzzfkcyl+nDkoyM/WS9WABaDFqNT1ZI9PFzHTD04Js
# nSAaprfaN9FgvQYFl4qrI01oqUbJvJM24y9q+qgAuZT+/TOcXksOkTgv24TfIKL2
# kYyyeVYjSm2mpt8gOsvo+LxnY9yJDQPbuiWFlmqoZVdJo/lfT9JD91gyZOUuHOz3
# nIEh0XNKswO386jp/Gt57OFCKPjn4h1gPtX9VgY=
# SIG # End signature block