roe.Misc.psm1

function Build-PowerShellModule {

    <#
        .SYNOPSIS
        Builds PowerShell module from functions found in given path

        .DESCRIPTION
        Takes function from seperate files in FunctionsPath and builds a PowerShell module to ModuleDir.
        Existing module files in ModuleDir will be overwritten.

        FunctionsPath must include one separate file for each function to include.
        The full content of the file will be included as a function. If a Function declaration is not found as the first line, it will be automatically created.

        Build version for module is incrementeted by 1 on each build, relative to version found in ManifestFile.
        

        .PARAMETER FunctionsPath
        Path to individual functions files

        .PARAMETER ModuleDir
        Directory to export finished module to

        .PARAMETER ModuleName
        Name of module. If omitted name will be taken from Manifest. If no manifest can be found, name will be autogenerated as Module-yyyyMMdd

        .PARAMETER ManifestFile
        Path to .psd1 file to use as template. If omitted the first .psd1 file found in ModuleDir will be used. If no file can be found a default will be created

        .PARAMETER Description
        Description of module

        .PARAMETER RunScriptAtImport
        Script to run when module is imported. Will be added to .psm1 as-is

    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({Test-Path -path $_})]
        [String]$FunctionsPath,
        [Parameter(Mandatory = $true)]
        [string]$ModuleDir,
        [Parameter(Mandatory = $false)]
        [string]$ModuleName,
        [Parameter(Mandatory = $false)]
        [String]$ManifestFile,
        [Parameter(Mandatory = $false)]
        [String]$Description,
        [Parameter(Mandatory = $false)]
        [String]$RunScriptAtImport,
        [Parameter(Mandatory = $false)]
        [switch]$RetainVersion
    )

    $ErrorActionPreference = 'Stop'

    switch ($ModuleDir) {
        {-not (Test-Path -Path $_ -ErrorAction SilentlyContinue)}                       {Write-Verbose "Creating $_" ; $null = New-Item -Path $_ -ItemType Directory ; break}
        {-not (Get-Item -Path $ModuleDir -ErrorAction SilentlyContinue).PSIsContainer}  {Throw "$ModuleDir is not a valid directory path"}
        default                                                                         {}
    }

    # Make sure we have a valid Manifest file
    if ([string]::isnullorwhitespace($ManifestFile)) {
        Write-Verbose "Getting first manifest file from $ModuleDir"
        $ManifestFile = (Get-Item -Path (Join-Path -Path $ModuleDir -ChildPath "*.psd1") | Select-Object -First 1).FullName
    }

    if ([string]::isnullorwhitespace($ManifestFile)) {
        if ([string]::IsNullOrWhiteSpace($ModuleName)) {
            Write-Verbose "Generating Modulename"
            $ModuleName = "Module-$(Get-Date -format yyyyMMdd)"
        }
        Write-Verbose "Creating default manifestfile"
        $ManifestFile = Join-Path -Path $ModuleDir -ChildPath "$ModuleName.psd1"
        New-ModuleManifest -Path $ManifestFile -ModuleVersion 0.0.0
    }
    
    # Get content of manifest
    $ManifestHash = [ScriptBlock]::Create((Get-Content -Path $ManifestFile -Raw)).InvokeReturnAsIs()

    # Make sure destination files will end up in the ModuleDir
    if ([string]::isnullorwhitespace($ModuleName)) {
        Write-Verbose "Naming module after $Manifestfile"
        $ModuleName = split-path -path $ManifestFile -LeafBase    
    }

    $ManifestFile   = Join-Path -path $moduleDir -childPath "$ModuleName.psd1" 
    $moduleFile     = Join-Path -Path $ModuleDir -ChildPath "$ModuleName.psm1"

    $ManifestHash["RootModule"] = "$ModuleName.psm1"
    if (-not [String]::isnullorwhitespace($Description)) {
        Write-Verbose "Adding description to manifest"
        $ManifestHash["Description"] = $Description
    }

    [Version]$Version = $manifestHash["ModuleVersion"]
    if (-not $RetainVersion) {
        # Increment version number
        Write-Verbose "Updating version: $($Version)"
        $VersionString = $Version.ToString().Split(".")
        $VersionString[-1] = [int]$VersionString[-1] + 1
        $Version =[version]($VersionString -join ".")
        Write-Verbose "New version: $($Version)"

        # Not really sure why I wanted to change major/minor version after .9
        # if ($Version.Minor -eq 9 -and $Version.Build -eq 9) {
        # Write-Verbose "Incrementing major version"
        # [version]$Version = "$($version.Major + 1).0.0"
        # }
        # elseif ($Version.Build -lt 9) {
        # Write-Verbose "Incrementing build version"
        # [version]$Version = "$($Version.Major).$($Version.Minor).$($Version.Build + 1)"
        # }
        # elseif ($Version.Minor -lt 9 -and $version.build -eq 9){
        # Write-Verbose "Incrementing minor version"
        # [Version]$Version = "$($Version.Major).$($Version.Minor + 1).0"
        # }
        # else {
        # Write-Warning "WTF?"
        # }
    }
    else {
        Write-Verbose "Retaining version: $($Version)"
    }
    # Get content of PS1 files in FunctionsPath
    $moduleContent = @(
        Get-Item -ErrorAction SilentlyContinue -Path (Join-Path -Path $FunctionsPath -childPath "*.ps1") | Sort-Object -Property BaseName | ForEach-Object -Process {
            Write-Verbose "Processing functionfile: $_"
            $FunctionsToExport += @($_.BaseName)
            $FunctionContent = Get-Content -Path $_ | Where-Object {-not [String]::isnullorwhitespace($_)}
            if ($FunctionContent[0].trim() -match "^Function") {
                Write-Verbose "Detected function"
                Get-Content -Path $_ -Raw
            }
            else {
                Write-Verbose "Adding function declaration"
                $Return = @()
                $Return += "Function $($_.BaseName) {"
                $Return += Get-Content -Path $_ -Raw
                $Return += "}"
                $Return
            }
        }
    )

  
    if (-not [String]::IsNullOrWhiteSpace($RunScriptAtImport)) {
        Write-Verbose "Adding RunScriptAtImport"
        $moduleContent += $RunScriptAtImport
    }
    
    Write-Verbose "Writing $Modulefile"
    Set-Content -Path $moduleFile -Value $moduleContent


    # Update manifest
    $ManifestHash["Path"]   = $manifestFile
    $ManifestHash["ModuleVersion"]        = $version
    
    $ManifestHash['FunctionsToExport']    = $functionsToExport 
    $ManifestHash['Copyright'] = "(c) Robert Eriksen. Almost all rights reserved :) $(Get-Date -format 'yyyy-MM-dd HH:mm:ss') $((Get-TimeZone).Id)"
    Write-Verbose "Updating manifest"
    Update-ModuleManifest @ManifestHash

    $props = [ordered]@{"ModuleName" = $ModuleName
                        "Version" = $Version
                        "Manifest" = (Get-Item -Path $manifestFile).FullName
                        "Module" = (Get-Item -Path $moduleFile).FullName
                        }
    return New-Object -TypeName PSObject -Property $props 

}
Function Clear-UserVariables {
    #Clear any variable not defined in the $SysVars variable
    $IgnoreVariables = @("PSCmdlet", "IgnoreVariables")
    if (Get-Variable -Name SysVars -ErrorAction SilentlyContinue) {
        $UserVars = get-childitem variable: | Where-Object {$SysVars -notcontains $_.Name} 
        ForEach ($var in ($UserVars | Where-Object {$_.Name -notin $IgnoreVariables})) {
            Write-Host ("Clearing $" + $var.name)
            Remove-Variable $var.name -Scope 'Global'
        }    
    }
    else {
        Write-Warning "SysVars variable not set"
        break
    }
}
Function Connect-EXOPartner {
    param(
        [parameter(Mandatory = $false)]
        [System.Management.Automation.CredentialAttribute()] 
        $Credential, 
        [parameter(Mandatory = $false)]
        [string] 
        $TenantDomain 
    )
    if (-not $TenantDomain) {
        $TenantDomain = Read-Host -Prompt "Input tenant domain, e.g. hosters.com"
    }
    if (-not $Credential) {
        $Credential = Get-Credential -Message "Credentials for CSP delegated admin, e.g. ""bm@klestrup.dk""/""password"""
    }
    $ExSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://ps.outlook.com/powershell-liveid?DelegatedOrg=$TenantDomain" -Credential $Credential -Authentication Basic -AllowRedirection
    if ($ExSession) {Import-PSSession $ExSession}
}

Function ConvertTo-HashTable {
    <#
        .SYNOPSIS
        Converts PS Object, or JSON string, to Hashtable
    #>

    # https://stackoverflow.com/questions/3740128/pscustomobject-to-hashtable
    param (
        [Parameter(ValueFromPipeline)]
        $PSObject
    )
  
    process {
        if ($null -eq $PSObject -and $null -eq $JSON ) { 
            return $null 
        }
          
        if ($PSObject -is [string]) {
            try {
                $PSObject = $PSObject | ConvertFrom-Json
            }
            catch {
                [string]$PSObject = $PSObject #Uncast string parameter is both type [psobject] and [string]
            }
        }
  
        if ($PSObject -is [Hashtable] -or $PSObject -is [System.Collections.Specialized.OrderedDictionary]) {
            return $PSObject
        }

        if ($PSObject -is [System.Collections.IEnumerable] -and $PSObject -isnot [string]) {
            $collection = @(
                foreach ($object in $PSObject) { ConvertTo-HashTable $object }
            )
            Write-Output -NoEnumerate $collection
        }
        elseif ($PSObject -is [psobject]) {
            $hash = [ordered]@{}
            foreach ($property in $PSObject.PSObject.Properties) {
                $hash[$property.Name] = ConvertTo-HashTable $property.Value
            }
            return $hash
        }
        else {
            return $PSObject
        }
    }
} 

function Find-NugetPackage {
    <#
        .SYNOPSIS
        Finds URL for NuGet package in PSGallery

        .DESCRIPTION
        Finds URL for, and optionally downloads, NuGet packages in PSGallery.
        Can be used as substitute for Install-Module in circumstances where it is not possible to install the required PackageProviders.

        Function returns PSCustomObject containing these properties:
        Name: Name of module
        Author: Author of module
        Version: Version of module
        URI: Direct link to NuGet package on PowerShell Gallery
        Description: Description of module
        Properties: Properties of NuGet Packaget
        NuGet: Search result from PowerShellGallery
        Dependencies: Name and version of dependencies
        FilePath: Path to module .psd1 file if module has been downloaded
        ImportOrder: Order of which modules should be imported (dependencies first)

        .Example

        PS> $Modules = Find-NugetPackage -Name Az.AKS -IncludeDependencies -Download -DownloadPath C:\temp\Modules
        PS> $Modules | Sort-Object -Property ImportOrder | ForEach-Object {Import-Module $_.FilePath}

        This will find the Az.AKS module, and any dependencies. Download them to C:\Temp\Modules, and then import them in the correct order.
        Az.AKS is dependent on Az.Accounts. Az.Accounts will get ImportOrder -1 and Az.AKS will get ImportOrder 0


        .PARAMETER Name
        Name of module to find

        .Parameter All
        [Experimental] Get all versions of module

        .PARAMETER Version
        Get specific version of module

        .Parameter IncludeDependencies
        Get info for all dependecies for module

        .Parameter KeepDuplicateDependencies
        Don't clean up output, if several versions of the same module is named as dependency. If omitted only the newest version of depedency modules will be returned.

        .Parameter Download
        Download found modules

        .Parameter DownloadPath
        Used with Download switch.
        If no DownloadPath is specified users module directory will be used. If DownloadPath does not exist, it will be created
    #>

    # Moddified from https://www.powershellgallery.com/packages/BuildHelpers/2.0.16/Content/Public%5CFind-NugetPackage.ps1
    
    [CMDLetbinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String]$Name,
        [Parameter(Mandatory = $false)]
        [switch]$All,
        [Parameter(Mandatory = $false)]
        [string]$Version,
        [Parameter(Mandatory = $false)]
        [switch]$IncludeDependencies,
        [Parameter(Mandatory = $false)]
        [switch]$KeepDuplicateDependencies,
        [Parameter(Mandatory = $false)]
        [Switch]$Download,
        [PArameter(Mandatory = $false)]
        [String]$DownloadPath
    )

    try {
    # Return stuff in this
    $Result = @()

    $PackageSourceUrl = "https://www.powershellgallery.com/api/v2/"

    #Figure out which version to find
    if ($PSBoundParameters.ContainsKey("Version")) {
        Write-Verbose "Searching for version [$version] of [$name]"
        $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$name' and Version eq '$Version'"
    }
    elseif ($PSBoundParameters.ContainsKey("All")) {
        Write-Verbose "Searching for all versions of [$name] module"
        $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$name'"
    }
    else {
        Write-Verbose "Searching for latest [$name] module"
        $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$name' and IsLatestVersion"
    }



    $NUPKG = @(Invoke-RestMethod $URI -Verbose:$false) 
    if ($Null -eq $NUPKG) {
        Write-Warning "No result for module $Name"
    }
    foreach ($pkg in $NUPKG) {
        $PkgDependencies = $pkg.properties.Dependencies.split("|")
        $Dependencies = @()
        foreach ($Dependency in ($PkgDependencies | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) {
            # Dependency will be formatted similar to:
            # Az.Accounts:[2.7.1, ):
            # (the lonely '[' is not a typo)
            try {
                $DepName = $Dependency.split(":")[0]
                $DepVersion = (($Dependency.split(":")[1].split(",")[0] | Select-String -Pattern "\d|\." -AllMatches).Matches | ForEach-Object { $_.Value }) -join ""
                $Dependencies += New-Object -TypeName PSObject -property ([ordered]@{"Module" = $DepName; "Version" = $DepVersion })
            }
            catch {
                Write-Warning $_.Exception.Message
                throw $_ 
            }
        }

        # Not sure in which cases NormalizedVersion and Version will differ, but the original author made a distinction, so so do I
        # And in case of -preview version numbers or similar, move the -preview part to the module name, so we can treat the rest of the version as a [version]. Not perfect but best we can do.
        [string]$PkgVersion =   if ($pkg.properties.NormalizedVersion) {
                                    $pkg.properties.NormalizedVersion
                                }
                                else {
                                    $pkg.properties.Version
                                }
        if ($pkgVersion -match "-") {
            [version]$PkgVersion = ($pkg.properties.NormalizedVersion | Select-String -AllMatches -Pattern "\d|\." | ForEach-Object { $_.Matches.value }) -join ""
            $pkgName = "$($pkg.title.('#text'))-$($pkg.properties.NormalizedVersion.split("-",2)[-1])"
        }
        else {
            [version]$PkgVersion = $PkgVersion
            $pkgName = $pkg.title.('#text')
        }

        try {
            $ImportOrder = Get-Variable -Name ImportOrder -ValueOnly -Scope 1 -ErrorAction Stop
            $ImportOrder = $ImportOrder - 1
        }
        catch {
            [int]$ImportOrder = 0
        }

        $Props = [ordered]@{"Name"          = $PkgName
                            "Author"        = $pkg.author.name
                            "Version"       = $pkgVersion
                            "URI"           = $pkg.content.src
                            "Description"   = $pkg.properties.Description
                            "Properties"    = $pkg.properties
                            "NuGet"         = $pkg
                            "Dependencies"  = $Dependencies
                            "FilePath"      = $false # Will point to local destination if -DownloadTo is used
                            "ImportOrder"   = $ImportOrder
                        }
        Write-Verbose "Finished collecting for $($pkgName) $($pkgVersion)"
        $Result += New-Object -TypeName PSObject -Property $Props

    }

    if ($IncludeDependencies) {
        # When function is called by itself/self-referenced, get a list of already retrieved dependencies, so we don't waste time getting info for the same module multiple times unless there is a requirement for a newer version
        $CurrentDependencies = [ordered]@{}
        foreach ($dep in $Dependencies ) {
            $CurrentDependencies.add($dep.Module, $dep.Version)
        }

        try {
            # If we can retrieve $KnownDependencies from parent scope this is most likely a nested run. Maybe use $Script scope... need testing
            try {
                $KnownDependencies = Get-Variable -Name KnownDependencies -ValueOnly -Scope 1 -ErrorAction Stop
            }
            catch {
            }
            $GetDependencies = @{}
            foreach ($key in $CurrentDependencies.Keys) {
                if ($KnownDependencies[$key] -lt $CurrentDependencies[$key]) {
                    $KnownDependencies[$key]    = $CurrentDependencies[$key]
                    $GetDependencies[$key]      = $CurrentDependencies[$key]
                }
            }
            # Now we have updated $KnownDependencies with any new versions, so return it to parent for the next modules dependency check
            Set-Variable -Name KnownDependencies -Value $KnownDependencies -Scope 1
        }
        catch {
            # If we cant retrieve $KnownDependencies from parent scope, this is most likely first iteration of function
            # Save a list of dependencymodules we already know we need to get.
            $KnownDependencies  = $CurrentDependencies
            $GetDependencies    = $KnownDependencies
        }

        # For some reason a "Collection was modified" error is thrown if referencing the keys directly.
        $GetModules = $GetDependencies.keys | ForEach-Object {$_.ToString()}
        foreach ($module in $GetModules) {
            if ($Module -eq "Az.Accounts") {
            Write-Verbose "Finding dependency for $($pkgName) $($Pkgversion): $($module) $($GetDependencies[$module])"
            }

            $Result += Find-Nugetpackage -Name $module -Version $GetDependencies[$module] -IncludeDependencies -KeepDuplicateDependencies:$PSBoundParameters.ContainsKey("KeepDuplicateDependencies")
        }
    }
    if (-not $KeepDuplicateDependencies) {
        # Only keep latest version of each dependecy
        $DuplicateModules = $Result | Group-Object -Property Name | Where-Object {$_.Count -gt 1} | Select-object -ExpandProperty Name 
        Write-Verbose "$($DuplicateModules.count) duplicate dependency-module versions found"
        $RemoveVersions = @()
        foreach ($Module in $DuplicateModules) {
            $RemoveVersions += $Result | Where-Object {$_.Name -eq $Module} | Sort-Object -Property Version -Descending | Select-object -Skip 1
            Write-Verbose "Removing duplicates of $Module"
        }
        $Result = @($Result | Where-Object {$_ -notin $RemoveVersions})
    }

    if ($Download) {
        if ([string]::IsNullOrWhiteSpace($DownloadPath)) {
            $DownloadPath =  @(($env:PSModulePath).split(";") | Where-Object {$_ -match [regex]::escape($env:userprofile)})[0]
            if ($null -eq $DownloadPath) {
                throw "No PS Modules Path found under current userprofile: $($env:PSModulePath)"
            }
        }
        if (-not (Test-Path -Path $DownloadPath -PathType Container -ErrorAction SilentlyContinue)) {
            $null = New-Item -Path $DownloadPath -Force -ItemType Directory -ErrorAction Stop
            if (-not (Test-Path -Path $DownloadPath -PathType Container -ErrorAction SilentlyContinue)) {
                throw "Unable to create directory: $DownloadPath"
            }
        }
        Write-Verbose "Downloading to: $DownloadPath"
        $DownloadCounter = 1
        foreach ($Module in $Result) {
            #$ZipFile = Join-Path $DownloadTo -ChildPath $($Module.Name) -AdditionalChildPath "$($Module.version).zip"
            $ZipFile = Join-Path $DownloadPath -ChildPath $($Module.Name)
            $ZipFile = Join-Path $ZipFile -ChildPath "$($Module.Version).zip"
            Write-Verbose "Downloading to Zipfile: $ZipFile"
            $ExtractDir = $ZipFile.replace(".zip", "")
            if (-not (Test-Path -Path $ExtractDir -ErrorAction SilentlyContinue)) {
                $null = New-Item -Path $ExtractDir -ItemType Directory -Force
            }
            Write-Verbose "Extracting to $ExtractDir"
            Write-Verbose "$($DownloadCounter)/$($Result.count) : Downloading $($Module.Name) $($Module.Version) to $ZipFile"
            Invoke-WebRequest -Uri $Module.Uri -OutFile $ZipFile
            Write-Verbose "Download complete"
            Write-Verbose "Extracting archive"
            Expand-Archive -Path $ZipFile -DestinationPath $ExtractDir -Force
            Write-Verbose "Extraction done"
            Write-Verbose "Deleting $ZipFile"
            Remove-Item -Path $ZipFile
            Write-Verbose "File deleted"
            $Module.FilePath = "$ExtractDir\$($Module.Name).psd1"
            $DownloadCounter++
        }
    }
    return $Result
    }
    catch {
        throw $_ 
    }
}

function Find-NugetPackage2 {
    <#
        .SYNOPSIS
        Finds URL for NuGet package in PSGallery, and optionally downloads.

        .DESCRIPTION
        Finds URL for, and optionally downloads, NuGet packages in PSGallery.
        Can be used as substitute for Install-Module in circumstances where it is not possible to install the required PackageProviders.

        Function returns PSCustomObject containing these properties:
        Name: Name of module
        Author: Author of module
        Version: Version of module
        URI: Direct link to NuGet package on PowerShell Gallery
        Description: Description of module
        Properties: Properties of NuGet Packaget
        NuGet: Search result from PowerShellGallery
        Dependencies: Name and version of dependencies
        FilePath: Path to module .psd1 file if module has been downloaded
        ImportOrder: Order of which modules should be imported (dependencies first)

        .Example
        PS> $Modules = Find-NugetPackage -Name Az.AKS -IncludeDependencies -Download -DownloadPath C:\temp\Modules
        PS> $Modules | Sort-Object -Property ImportOrder | ForEach-Object {Import-Module $_.FilePath}

        This will find the Az.AKS module, and any dependencies. Download them to C:\Temp\Modules, and then import them in the correct order.
        Az.AKS is dependent on Az.Accounts. Az.Accounts will get ImportOrder -1 and Az.AKS will get ImportOrder 0


        .PARAMETER Name
        Name of module to find

        .Parameter All
        [Experimental] Get all versions of module

        .PARAMETER Version
        Get specific version of module

        .Parameter IncludeDependencies
        Get info for all dependecies for module

        .Parameter KeepDuplicateDependencies
        Don't clean up output, if several versions of the same module is named as dependency. If omitted only the newest version of dependency modules will be returned.

        .Parameter Download
        Download found modules

        .Parameter DownloadPath
        Used with Download switch.
        If no DownloadPath is specified users module directory will be used. If DownloadPath does not exist, it will be created
    #>

    # Moddified from https://www.powershellgallery.com/packages/BuildHelpers/2.0.16/Content/Public%5CFind-NugetPackage.ps1
    
    [CMDLetbinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [String[]]$Name,
        [Parameter(Mandatory = $false)]
        [switch]$All,
        [Parameter(Mandatory = $false)]
        [string]$Version,
        [Parameter(Mandatory = $false)]
        [switch]$IncludeDependencies,
        [Parameter(Mandatory = $false)]
        [Switch]$Download,
        [PArameter(Mandatory = $false)]
        [String]$DownloadPath
    )

    try {
    # Return stuff in this
    $Result = @()
    
    # Use this to check for parent scope at end of function. If not parent scope can be detected prepare to filter, optionally download, and return data.
    $FindNugetPackageChild = $true 

    $PackageSourceUrl = "https://www.powershellgallery.com/api/v2/"

    # Get searchresults for all requested modules
    Write-Verbose "Starting processing $($Name.count) modules: $($Name)"
    $local:NUPKG = @()
    foreach ($module in $name) {
        #Figure out which version to find
        if ($PSBoundParameters.ContainsKey("Version")) {
            Write-Verbose "Searching for version [$version] of [$module]"
            $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$module' and Version eq '$Version'"
        }
        elseif ($PSBoundParameters.ContainsKey("All")) {
            Write-Verbose "Searching for all versions of [$module] module"
            $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$module'"
        }
        else {
            Write-Verbose "Searching for latest [$module] module"
            $URI = "${PackageSourceUrl}Packages?`$filter=Id eq '$module' and IsLatestVersion"
        }


        
        $NUPKG += @(Invoke-RestMethod $URI -Verbose:$false) 
        if ($Null -eq $NUPKG) {
            Write-Warning "No result for module $Name"
        }
    }

    $local:Dependencies = @()
    foreach ($pkg in $NUPKG) {
        Write-Verbose ""
        Write-Verbose "Getting details for $($pkg.properties.Id) $($pkg.properties.Version)"
        $PkgDependencies = $pkg.properties.Dependencies.split("|")
        $PkgDependencies = $PkgDependencies | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        Write-Verbose "$($PkgDependencies.count) dependencies found"
        foreach ($Dependency in $PkgDependencies) {
            # Dependency will be formatted similar to:
            # Az.Accounts:[2.7.1, ):
            # (the lonely '[' is not a typo)
            try {
                $DepName = $Dependency.split(":")[0]
                $DepVersion = (($Dependency.split(":")[1].split(",")[0] | Select-String -Pattern "\d|\." -AllMatches).Matches | ForEach-Object { $_.Value }) -join ""
                Write-Verbose "Registering dependency: $($DepName) $($DepVersion)"
                $Dependencies += New-Object -TypeName PSObject -property ([ordered]@{"Module" = $DepName; "Version" = $DepVersion })
            }
            catch {
                Write-Warning $_.Exception.Message
                throw $_ 
            }
        }

        # Not sure in which cases NormalizedVersion and Version will differ, but the original author made a distinction, so so do I
        # And in case of -preview version numbers or similar, move the -preview part to the module name, so we can treat the rest of the version as a [version]. Not perfect but best we can do.
        [string]$PkgVersion =   if ($pkg.properties.NormalizedVersion) {
                                    $pkg.properties.NormalizedVersion
                                }
                                else {
                                    $pkg.properties.Version
                                }
        if ($pkgVersion -match "-") {
            [version]$PkgVersion = ($pkg.properties.NormalizedVersion | Select-String -AllMatches -Pattern "\d|\." | ForEach-Object { $_.Matches.value }) -join ""
            $pkgName = "$($pkg.title.('#text'))-$($pkg.properties.NormalizedVersion.split("-",2)[-1])"
        }
        else {
            [version]$PkgVersion = $PkgVersion
            $pkgName = $pkg.title.('#text')
        }

        # Try to guess the order modules must be imported. If this is nested call, its because this is a dependency module so set import order lower than parent module
        try {
            # Scope 1 is parent scope. If the variable can be found there, this is a nested call
            $ImportOrder = Get-Variable -Name ImportOrder -ValueOnly -Scope 1 -ErrorAction Stop
            $ImportOrder = $ImportOrder - 1
        }
        catch {
            # If not, this is the original module. Leave an import order for any dependencies to refer to
            [int]$ImportOrder = 0
        }
        Write-Verbose "Setting ImportOrder to $ImportOrder"
        $Props = [ordered]@{"Name"          = $PkgName
                            "Author"        = $pkg.author.name
                            "Version"       = $pkgVersion
                            "URI"           = $pkg.content.src
                            "Description"   = $pkg.properties.Description
                            "Properties"    = $pkg.properties
                            "NuGet"         = $pkg
                            "Dependencies"  = $Dependencies
                            "FilePath"      = $false # Will point to local destination if -DownloadTo is used
                            "ImportOrder"   = $ImportOrder
                        }
        Write-Verbose "Finished collecting details for $($pkgName) $($pkgVersion)"
        $Result += New-Object -TypeName PSObject -Property $Props
        Write-Verbose ""
    }

    if ($IncludeDependencies) {
        # Get latest version of the dependencies we know of so far
        $DependencyNames = ($Dependencies | Group-Object -Property Module).Name
        Write-Verbose "Preparing collection of details for $($DependencyNames.count) unique dependencies for $($Name.count) modules"
        $CurrentDependencies = @{}
        foreach ($depname in $DependencyNames) {  
            $CurrentDependencies.Add($depname,($Dependencies | Where-Object {$_.Module -eq $depname} | Sort-Object -Property version -Descending | Select-Object -First 1 -ExpandProperty Version))
        } 

        try {
            # See if we can retrieve $KnownDependencies from parent scope, in case this is a nested call.
            $KnownDependencies = Get-Variable -Name KnownDependencies -ValueOnly -Scope 1 -ErrorAction Stop

            $GetDependencies = @{}
            # If we already know of a newer version of a dependency theres no need to get an older version.
            foreach ($key in $CurrentDependencies.Keys) {
                if ($KnownDependencies[$key] -lt $CurrentDependencies[$key]) {
                    Write-Verbose "Adding dependency $($key) version $($CurrentDependencies[$key]) to list of dependencies to collect details for"
                    $KnownDependencies[$key]    = $CurrentDependencies[$key]
                    $GetDependencies[$key]      = $CurrentDependencies[$key]
                }
                else {
                    Write-Verbose "Dependency $($key) already added version $($KnownDependencies[$key])"
                }
            }
            # Now we have updated $KnownDependencies with any new versions, so return it to parent for the next modules dependency check
            Set-Variable -Name KnownDependencies -Value $KnownDependencies -Scope 1
        }
        catch {
            # If we cant retrieve $KnownDependencies from parent scope, this is most likely first iteration of function
            # Save a list of dependencymodules we already know we need to get.
            $KnownDependencies  = $CurrentDependencies
            $GetDependencies    = $KnownDependencies
        }

        # For some reason a "Collection was modified" error is thrown if referencing the keys directly.
        $GetModules =@($GetDependencies.keys)# | ForEach-Object {$_.ToString()}
        Write-Verbose "Calling nested call for $($GetDependencies.Keys.count) dependency modules for $($name)"
        foreach ($module in $GetModules) {
            # Should probably just ignore versions all together, and go for the latest version every time
            $Result += Find-Nugetpackage2 -Name $module -Version $GetDependencies[$module] -IncludeDependencies -Verbose:$PSBoundParameters.ContainsKey("Verbose")
        }
        Write-Verbose "Finished getting $($GetDependencies.Keys.count) dependency modules for $($name)"
    }


    try {
        $null = Get-Variable -Name FindNugetPackageChild -ValueOnly -Scope 1 -ErrorAction Stop
        Write-Verbose "Returning to parent"
        # Do nothing. This is a nested call, so we're still collecting info
    }
    catch {
        Write-Verbose "Filtering dependencies for latest version"
        # No FindNugetPackageChild variable. We must be the top-most parent
        # Only keep latest version of each dependecy. We do sort of a filtering when collecting info, but if lower version is observed before a higher version both will be initially collected.
        $ResultNames = ($Result | Group-Object -Property name).Name
        $Result = foreach ($name in $ResultNames) {  $Result | Where-Object {$_.Name -eq $name} | Sort-Object -Property version -Descending | Select-Object -First 1} 

        if ($Download) {
            Write-Verbose "Starting download of $($Result.count) modules: $Result"
            if ([string]::IsNullOrWhiteSpace($DownloadPath)) {
                $DownloadPath =  @(($env:PSModulePath).split(";") | Where-Object {$_ -match [regex]::escape($env:userprofile)})[0]
                if ($null -eq $DownloadPath) {
                    throw "No PS Modules Path found under current userprofile: $($env:PSModulePath)"
                }
            }
            if (-not (Test-Path -Path $DownloadPath -PathType Container -ErrorAction SilentlyContinue)) {
                Write-Verbose "Creating download path: $DownloadPath"
                $null = New-Item -Path $DownloadPath -Force -ItemType Directory -ErrorAction Stop
                if (-not (Test-Path -Path $DownloadPath -PathType Container -ErrorAction SilentlyContinue)) {
                    throw "Unable to create directory: $DownloadPath"
                }
            }
            Write-Verbose "Downloading to: $DownloadPath"
            $DownloadCounter = 1
            foreach ($Module in $Result) {
                #$ZipFile = Join-Path $DownloadTo -ChildPath $($Module.Name) -AdditionalChildPath "$($Module.version).zip"
                $ZipFile = Join-Path $DownloadPath -ChildPath $($Module.Name)
                $ZipFile = Join-Path $ZipFile -ChildPath "$($Module.Version).zip"
                Write-Verbose "Downloading to Zipfile: $ZipFile"
                $ExtractDir = $ZipFile.replace(".zip", "")
                if (-not (Test-Path -Path $ExtractDir -ErrorAction SilentlyContinue)) {
                    $null = New-Item -Path $ExtractDir -ItemType Directory -Force
                }
                Write-Verbose "Extracting to $ExtractDir"
                Write-Verbose "$($DownloadCounter)/$($Result.count) : Downloading $($Module.Name) $($Module.Version) to $ZipFile"
                Invoke-WebRequest -Uri $Module.Uri -OutFile $ZipFile
                Write-Verbose "Download complete"
                Write-Verbose "Extracting archive"
                Expand-Archive -Path $ZipFile -DestinationPath $ExtractDir -Force
                Write-Verbose "Extraction done"
                Write-Verbose "Deleting $ZipFile"
                Remove-Item -Path $ZipFile
                Write-Verbose "File deleted"
                $Module.FilePath = "$ExtractDir\$($Module.Name).psd1"
                $DownloadCounter++
            }
        }
        
    }
    return $Result
    }
    catch {
        throw $_ 
    }
}
Function Get-AzRunbookLog {
    <#
        .Parameter Stream
        Streams to read from runbook log. If all individual streams are specified this is converted to 'Any'

        .PARAMETER IgnoreStream
        Streams to ignore

        .PARAMETER ImportModule
        Include verbose messages related to the Import-Module command (will be filtered out by default). Only affects LogMessages key.

        .Parameter CopyToClipboard
        Copy Any-key to Clipboard
        
        .PARAMETER Job
        Object containing Azure Automation Job to read logs from

        .EXAMPLE
        $job | Get-AzRunbookLog -Stream Output
        This will get the Output stream from the Automation job log, excluding Import-Module messages.

        .EXAMPLE
        $job | Get-AzRunbookLog -Stream Any -IgnoreStream Verbose,Progress
        This will get any stream, except Verbose and Progress streams, excluding Import-Module messages.

        .EXAMPLE
        $job | Get-AzRunbooklog -stream output,verbose -ImportModule -CopyToClipboard
        This will get the Output and Verbose stream and include any entries related to the Import-Module command and copy the Any-key to clipboard

    #>


    [CMDLetbinding()]
    param (

        [ValidateSet("Any", "Progress", "Output", "Warning", "Error", "Verbose")]
        [String[]]$Stream = "Any",
    
        [ValidateSet("Progress", "Output", "Warning", "Error", "Verbose")]
        [String[]]$IgnoreStream,

        [Switch]$ImportModule,

        [Switch]$CopyToClipboard,

        [Parameter(Mandatory = $true, ValuefromPipeline = $True)]
        [Microsoft.Azure.Commands.Automation.Model.Job]$Job
    )

    # RegEx pattern to match Import-Module verbose logging:
    # [Verbose] is added by script during filtering
    # Sample output:
    # Loading module from path 'C:\Modules\User\TMS.PSExtensions.Shared\TMS.PSExtensions.Shared.psm1'.
    # Exporting function 'Copy-FileFromImageBlob'.
    # Importing function 'Copy-FileFromImageBlob'.
    # Loading module from path 'C:\Program Files\WindowsPowerShell\Modules\VMware.PowerCLI\11.5.0.14912921\VMware.PowerCLI.ps1'.
    # Loading 'Assembly' from path 'C:\Program Files\WindowsPowerShell\Modules\VMware.VimAutomation.Sdk\11.5.0.14898111\net45\Microsoft.Practices.EnterpriseLibrary.Common.dll'.
    $ModuleMatchPattern = @(
        "^\[Verbose\] (Importing|Exporting)\sfunction\s'.+'\.$"
        "^\[Verbose\] (Importing|Exporting)\scmdlet\s'.+'\.$"
        "^\[Verbose\] (Importing|Exporting)\salias\s'.+'\.$"
        "^\[Verbose\] Loading\s(.+)\sfrom\spath\s'.+'\.$"
    ) -join "|"

    if ($null -ne $IgnoreStream) {
        Write-Verbose "Filtering out ignored streams: $IgnoreStream"
        if ($Stream -contains "Any") {
            $Stream = @("Progress", "Output", "Warning", "Error", "Verbose")
        }
        $Stream = $Stream | Where-Object { $_ -notin $IgnoreStream }
    }

    if ( ( @("Progress", "Output", "Warning", "Error", "Verbose") | Where-Object { $Stream -contains $_ }).count -eq 5) {
        Write-Verbose "All streams specified - Changing to 'Any'" # Faster to get all streams at once, than each one individually
        $Stream = @("Any")
    }

    $Stream = $Stream | Sort-Object

    Write-Verbose "Record streamstype(s) to retrieve: $Stream"
    if ($Stream -contains "Any" -or $Stream -contains "Verbose") {
        Write-Warning "Due to some weird bug in Get-AzAutomationJobOutputRecord first line of verbose output might be omitted from result"
    }

    Write-Verbose "Getting joboutputrecords"
    # Get all log records of the selected types
    $x = 0
    $AllStreamRecords = @()
    Foreach ($type in $Stream) {
        $x++
        Write-Verbose "$($x)/$($Stream.count): Getting Stream: $type"
        $Time = Measure-Command -Expression {
            $AllStreamRecords += @($job | Get-AzAutomationJobOutput -Stream $type | Get-AzAutomationJobOutputRecord)
        }
        Write-Verbose "Read in $([math]::round($time.TotalSeconds,2)) seconds"
    }

    # Get text values from different records, and split to separate keys if 'Any' was specified
    Write-Verbose "Filtering $($AllStreamRecords.count) records"
    $JobRecords = [ordered]@{"Any" = @() } # Store text-version of log records here
    foreach ($record in ($AllStreamRecords | Sort-Object -Property Time)) {
        $LogMessage = switch ($record.type) {
            "output" { "[Output] $($record.value.value)" }
            "warning" { "[Warning] $($record.value.Message)" }
            "verbose" { "[Verbose] $($record.value.Message)" }
            "progress" { "[Progress] $($record.Value.Activity) - $($record.Value.StatusDescription) - Pct complete: $($record.Value.PercentComplete) - Seconds remaining: $($record.Value.SecondsRemaining)" }
            "error" { "[Error] $((($record | ConvertTo-Json | ConvertFrom-Json).value.exception | where-object {-not [string]::IsNullOrWhiteSpace($_)}) -join " - ")" } # Må kunne gøres smartere!
            default { "[$($record.type)][Unknown] $($record.Summary)" }
        }
        if (-not $ImportModule -and $LogMessage -match $ModuleMatchPattern) {
            # Figure out how to remove it from JobRecords["verbose"] and JobRecords["Any"]
        }
        else {
            $JobRecords["Any"] += $LogMessage
            $JobRecords[$record.type] += @($record) 
        }
        
    }

    if ($CopyToClipboard) {
        Write-Verbose "Copying LogMessages to clipboard"
        Set-Clipbboard -Value $JobRecords["LogMessages"]
    }

    return $JobRecords
}
Function Get-COMObjects {
    if (-not $IsLinux) {
        $Objects = Get-ChildItem HKLM:\Software\Classes -ErrorAction SilentlyContinue | Where-Object {$_.PSChildName -match '^\w+\.\w+$' -and (Test-Path -Path "$($_.PSPath)\CLSID")}
        $Objects | Select-Object -ExpandProperty PSChildName
    }
}

Function Get-CustomError {
    <#
    .SYNOPSIS
    Generate custom error message based on call stack and error information.
     
    .Parameter ErrorObject
    ErrorObject received by try/catch block
 
    .Parameter Message
    Message to prepend to output
 
    .EXAMPLE
    try {
        Invoke-RESTMethod -uri www.site.com -method POST -body '{"Operation": "De-func instruction"}'
    }
    catch
        throw (Get-CustomError -ErrorObject $_ )
    }
    #>


    [CMDLetbinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({ $null -ne $_.Exception })]
        [Object]$ErrorObject,

        [Parameter(Mandatory = $false)]
        [String]$Message

    )
    
    begin {
        Function GetVariableValues {
            [CMDLetbinding()]
            param(
                [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
                [String[]]$Variables,
                [Parameter(Mandatory = $false)]
                [Int]$MaxParameterValueLength = 100
            )
            begin {
            }
            process {
                $Variables = $Variables | Where-Object { $_ } # Filter out null values - This should be fixed now
                $MaxVariableLength = $Variables | Sort-Object -Property Length -Descending | Select-Object -First 1 -ExpandProperty Length # Get a padding-length to present variables more readable
                Write-Verbose "Max length: $MaxParameterValueLength"
                $Result = @()
                try {
                    foreach ($v in $Variables) {
                        $VarName = $v.replace('$', '')     
                        if ($VarName -match '^env:') {
                            # Environment variable
                            Write-Verbose "Looking for environment variable '$VarName'"
                            $Value = (Get-Item -Path $VarName -ErrorAction SilentlyContinue)
                        }
                        else {
                            # Regular PS variable
                            $Value = $null 
                            $scope = 2 # Skip known scopes.
                            while ($null -eq $Value ) {
                                try {
                                    Write-Verbose "Looking for variable '$VarName' in scope $scope relative to this function"
                                    $Value = (Get-Variable -Name $VarName -Scope $scope -ErrorAction Stop) 
                                                    
                                }
                                catch [System.Management.Automation.ItemNotFoundException] {
                                    Write-Verbose "variable not found in scope. Moving on to next"
                                }
                                catch {
                                    Write-Verbose "Out of scopes, or unexpected error: $($_.Exception.Message )"
                                    break
                                }
                                $scope++ 
                            }
                        }
                        if ($null -eq $Value) {
                            Write-Verbose "Unable to find variable '$VarName'"
                            $Value = "<Couldn't find value>"
                        }
                        else {
                            $Value = $Value.Value 
                            if ($Value -isnot [string]) {
                                $Value = "[$($Value.GetType().Name)] $($Value.ToString())"
                            }

                        }

                        if ($Value.Length -gt $MaxParameterValueLength) {
                            $Value = "$($Value.SubString(0,$MaxParameterValueLength)) (Truncated)"
                        }

                        $Result += "$($v.PadRight($MaxVariableLength," ")): $($Value)"   
                    } 
                }
                catch {
                    $Result = "<unhandled exception looking for variables>:", $_.Exception.MEssage 
                }
            }
            end {
                return $Result
            } 
        }
    }
    process {
        try {
            $CustomError = @($Message)
            $CustomError += "Message: $($ErrorObject.Exception.Message)"
            # Get commandline causing the error and see if we can find any variables in ot as well
    
            $CustomError += " ---=== Error details ===---"
            $Line = $ErrorObject.InvocationInfo.ScriptLineNumber
            $Command = ($ErrorObject.InvocationInfo.Line)
            $Variables = @(($command | Select-String -Pattern '\s(\$\S+)' -AllMatches).Matches.foreach({ $_.Groups[1].Value })) # Find all words in command containing a dollar-sign.
            $MaxVariableLength = $Variables | Sort-Object -Property Length -Descending | Select-Object -First 1 -ExpandProperty Length # Get a padding-length to present variables more readable
            $MaxVariableValueLength = 100

            if (-not [string]::IsNullOrWhiteSpace($ErrorObject.InvocationInfo.MyCommand)) {
                $CustomError += "MyCommand: $($ErrorObject.InvocationInfo.MyCommand)"
            }
            if (-not [string]::IsNullOrWhiteSpace($ErrorObject.InvocationInfo.ScriptName)) {
                $ScriptName = $ErrorObject.InvocationInfo.ScriptName
                $CustomError += "ScriptName: $($ScriptName)"
            }
            if ([string]::IsNullOrWhiteSpace($ErrorObject.InvocationInfo.ScriptName) -and [string]::IsNullOrWhiteSpace($ErrorObject.InvocationInfo.MyCommand) ) {
                $CustomError += "No MyCommand or ScriptName found. If first command of CallStack is <ScriptBlock> exception was thrown from MainRunbook function in runbook"
            }
            $CustomError += "Line: $($Line)" # Line number where exception occurred
            # If exception is in a module, find the name and linenumber of function, instead of line xxxx in .psm1
            if ($ScriptName -match ".+\.psm1$") {
                try {
                    $ModuleName = (Split-Path -Path $ScriptName -Leaf).replace(".psm1", "")
                    $ModuleName += " ($((Get-Module -Name $ModuleName | Select-Object -ExpandProperty Version).ToString()))"
                    $ModuleContent = (Get-Content -Path $ScriptName)[0..($Line - 1)] # Get part of module up untill the failing line, and read it backwards, while counting lines, till we reach the function definition
                    while ($Null -eq $FunctionName -and $Line -ge 0) {
                        $null = [int]$FunctionLine++
                        if (($ModuleContent[$line - 1].trim() -replace "\s\s+", " ") -match "^function\s(\S+)\s\{") {
                            $FunctionName = $Matches[1]
                        }
                        $Line--
                    }
                }
                catch {
                    if ($null -eq $ModuleName) {
                        $ModuleName = "<Couldn't get module name>"
                    }
                    if ($null -eq $FunctionName) {
                        $FunctionName = "<Couldnt get function name> - $($_.Exception.Message)" # ToDo: Investigate when/why this can happen: if (($ModuleContent[$line - 1].trim() -replace "\s\s+"," ") -match "^function\s(\S+)\s\{") { - You cannot call a method on a null-valued expression.
                    }
                } 
                $CustomError += "Module: $ModuleName"
                $CustomError += "FunctionName: $($FunctionName)"
                $CustomError += "FunctionLine: $($FunctionLine)"
            }
        
            $CustomError += "Command: $($Command.trim())" # Command causing the exception
            if ($Variables.count -gt 0) {    
                # Go through the variables found in commandline and see if we can present some values to help troubleshoot. - ToDo: Add this to the CallStack iteration as well.
                $CustomError += "Variables detected:"
                $CustomError += GetVariableValues -Variables $Variables
            }
            $CustomError += " "
    
            # Det kunne vøre løkkert med noget switch på fejl-type, eller lignende.
            # Men det lader ikke til der er nogen konsekvent måde at identificere forskellige fejltyper (f.eks. er Get-WmiObject og Invoke-RESTMethod vidt forskellige)
    
            # System.Net.WebRequest
            if ($null -ne $ErrorObject.Exception.Response) {
                $CustomError += " ---=== HTTP Response ===---"
                # $Headers = foreach ($h in ($ErrorObject.Exception.Response.Headers)) {"$($h): $($ErrorObject.Exception.Response.GetResponseHeader($h))"} # Udlæsning virker fint, men har vi noget at bruge dem til?
                $ResponseStream = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream()) # Kan indeholde mere brugbare oplysninger end bare "Bad request" eller lignende.
                $Response = $ResponseStream.ReadToEnd() 
                $ResponseStream.Close()
                # if (-not [string]::IsNullOrWhiteSpace($Headers)) {
                # $CustomError += "Headers:"
                # $CustomError += $Headers
                # $CustomError += " "
                # }
                if (-not [string]::IsNullOrWhiteSpace($Response)) {
                    $CustomError += "Web Response:"
                    $CustomError += $Response
                    $CustomError += " "
                }    
            }

            $CustomError += " ---=== Call Stack ===---"
            $CallStack = @(Get-PSCallStack)
            # Reverse the callstack for more intuitive order
            $CallStack = $CallStack[($CallStack.length - 1 )..0]

            # If we feel like indenting the different steps in the callstack. Initial attempts looked like crap
            $Indent = ""
            Foreach ($cs in $CallStack) {
                $CustomError += "$($Indent)Script: $($cs.ScriptName)"
                $CustomError += "$($Indent)Command: $($cs.Command)"
                $CustomError += "$($Indent)Parameters:"
                $MaxParameterLength = $cs.InvocationInfo.BoundParameters.Keys | ForEach-Object { $_.ToString().length } | Sort-Object -Descending | Select-Object -First 1 # Needed to pad output for readability
                $MaxParameterValueLength = $MaxVariableValueLength
                $CustomError += foreach ($key in $cs.InvocationInfo.BoundParameters.Keys) {
                    $Value = $cs.InvocationInfo.BoundParameters[$key]
                    if ($Value -isnot [string]) {
                        $Value = "[$($Value.GetType().Name)] $($Value.ToString())"
                    }
                    if ($Value.Length -gt $MaxParameterValueLength) {
                        $ParameterValue = "$($Value.substring(0,$MaxParameterValueLength)) (Truncated)"
                    }
                    else {
                        $ParameterValue = $Value
                    }
                    "`t$($key.ToString().PadRight($MaxParameterLength," ")): $($ParameterValue)"
                }

                $CustomError += "$($Indent)ScriptLineNumber: $($cs.ScriptLineNumber)"
                $CustomError += "$($Indent)ScriptLine: $($cs.Position)"
                $Variables = @(($cs.Position.Text | Select-String -Pattern '\s(\$\S+)' -AllMatches).Matches.foreach({ $_.Groups[1].Value })) # Find all words in command containing a dollar-sign.
                if ($Variables.count -gt 0) {
                    $CustomError += "Variables detected:"
                    $CustomError += GetVariableValues -Variables $Variables
                }
                $CustomError += " "
                $Indent += $Indent
            }


        }
        catch {
            $CustomError = @("Unable to process errorobject and/or callstack "  )
            $CustomError += "Got error: $($_.InvocationInfo.Line.trim()) - $($_.Exception.Message)"
            $CustomError += " "
            $CustomError += "Received invocation info:" 
            $CustomError += $ErrorObject.InvocationInfo | Out-String
            $CustomError += "Received exception:"
            $CustomError += $ErrorObject.Exception.Message | Out-String
        }

        try {
            Write-Verbose "Padding output for readability"
            $Headers = $CustomError.Where({ $_ -match ":" }).foreach({ $_.Split(":", 2)[0] })
            $MaxHeadersLength = $Headers | Sort-Object -Property length -Descending | Select-Object -ExpandProperty length -First 1
            $CustomErrorPadded = @()
            for ($x = 0 ; $x -lt $CustomError.count ; $x++) {
                if ($CustomError[$x] -match '^\$env') {
                    $envsplit = $CustomError[$x].split(":")
                    $cesplit = @(($envsplit[0..1] -join ":"), ($envsplit[2..($envsplit.Length - 1)] -join "").trim())
                }
                else {
                    $cesplit = $CustomError[$x].split(":", 2)
                } 
                $cesplit[0] = $cesplit[0].replace("`t", " ").padright($MaxHeadersLength, " ")
                $CustomErrorPadded += $cesplit -join ": " 
            }
            $CustomError = $CustomErrorPadded

        }
        catch {
        }
        $CustomError = $CustomError -join ([environment]::NewLine)
        return $CustomError
    }
    end {
    }
}
Function Get-GraphAPIHeaders {
    [CMDLetbinding()]
    param (
        [string]$AppID,
        [string]$AppSecret,
        [string]$TenantID,
        [string]$AccessToken,
        [string]$ResourceURL = "https://graph.microsoft.com/"
    )
    # Get an Graph API header, with token
    if ((-not $AccessToken) -and ($appid)) {
        $RequestBody = @{client_id = $appID; client_secret = $AppSecret; grant_type = "client_credentials"; scope = "https://graph.microsoft.com/.default"; }
        $OAuthResponse = $OAuthResponse = (Invoke-Webrequest -UseBasicParsing -Method Post -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Body $RequestBody).content | ConvertFrom-Json
        $AccessToken = $OAuthResponse.access_token
    }
    elseif (-not $AccessToken) {
        $AccessToken =(Get-AzAccessToken -ResourceUrl $ResourceURL).Token
    }
    
        $headers = @{
            "Authorization" = "Bearer $AccessToken"
            "Content-Type"  = "application/json"
        }
                       
    return $headers
                        
}

Function Get-MatchingString {
    param(
        [string]$Text,
        [string]$RegEx,
        [switch]$CaseSensitive
    )
    $Text | Select-String -Pattern $RegEx -AllMatches -CaseSensitive:$CaseSensitive | ForEach-Object {$_.Matches.Value}
}
Function Get-NetIPAdapters {
    Param(
    [Parameter(Mandatory = $false)]
        [String[]]$ComputerName
    )
    if ($ComputerName.length -lt 1 -or $computername.Count -lt 1) {
        $computername = @($env:COMPUTERNAME)
    }
    $OutPut = @()
    #Vis netkort med tilhørende IP adr.
    foreach ($pc in $Computername) {
        $OutPut += Get-NetAdapter -CimSession $pc | Select-Object Name,InterfaceDescription,IfIndex,Status,MacAddress,LinkSpeed,@{N="IPv4";E={(Get-NetIpaddress -CimSession $pc -InterfaceIndex $_.ifindex -AddressFamily IPv4 ).IPAddress}},@{N="IPv6";E={(Get-NetIpaddress -CimSession $pc -InterfaceIndex $_.ifindex -AddressFamily IPv6 ).IPAddress}},@{N="Computer";E={$pc}} | Sort-Object -Property Name
    }
    $OutPut
}

Function Get-NthDayOfMonth {
    <#
    .SYNOPSIS
    Returns the Nth weekday of a specified month
    

    .DESCRIPTION
    Returns the Nth weekday of a specified month.
    If no weekday is specified an array is returned containing dates for the 7 weekdays throughout the specified month.
    If no Month or Year is specified current month will be used.

    .PARAMETER Month
    The month to process, in numeric format. If no month is specified current month is used.

    .PARAMETER Weekday
    The weekday to lookup. If no weekday is specified, all weekdays are returned.

    .PARAMETER Number
    The week number in the specified month. If no number is specified, all weekdays are returned.

    .PARAMETER Year
    The year to process. If no year is specified, current year is used.

    .INPUTS
    None. You cannot pipe objects to this function

    .OUTPUTS
    PSCustomObject

    .EXAMPLE
    PS> #Get the 3rd tuesday of march 1962
    PS> Get-NthDayOfMonth -Month 3 -Year 1962 -Weekday Tuesday -Number 3
           
    20. marts 1962 00:00:00
#>

    [cmdletbinding()]
    param (
        [validateset(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)]
        [int]$Month,
        [ValidatePattern("^[0-9]{4}$")]
        [int]$Year,
        [validateset("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")]
        [string]$Weekday,
        [validateset(-5, -4, -3, -2, -1, 1, 2, 3, 4, 5)]
        $Number
    )

    if ($number) {
        [int]$number = $number #If parameter is cast as [int] in param section it will default to 0. If no type is defined null value is allowed, but any value will be string by default
    }


    #Find last date of current month. Workarounds to avoid any cultural differences (/,- or whatever as date seperator as well as mm/dd/yyyy, yyyy/dd/mm or whatever syntax)
    $BaseDate = Get-Date
    if ($Month) {
        $CurMonth = [int](Get-Date -Format MM)
        if ($CurMonth -ge $Month) {
            $BaseDate = (Get-Date $BaseDate).AddMonths(-($CurMonth - $Month))
        }
        else {
            $BaseDate = (Get-Date $BaseDate).AddMonths(($Month - $CurMonth))
        }
    }

    if ($Year) {
        $CurYear = [int](Get-Date -Format yyyy)
        if ($CurYear -ge $Year) {
            $BaseDate = (Get-Date $BaseDate).AddYears(-($CurYear - $Year))
        }
        else {
            $BaseDate = (Get-Date $BaseDate).AddYears(($Year - $CurYear))
        }
    
    }

    $CurDate = (Get-Date $BaseDate).Day
    if ($CurDate -gt 1) {
        $FirstDate = (Get-Date $BaseDate).AddDays(-($CurDate - 1)).Date
    }
    else {
        $FirstDate = (Get-Date $BaseDate).date
    }
    $NextMonth = (Get-Date $FirstDate).AddMonths(1)
    $LastDate = (Get-Date $NextMonth).AddDays(-1)

    # Build the object to get dates for each weekday
    $Props = [ordered]@{"Monday"    = @()
                        "Tuesday"   = @()
                        "Wednesday" = @()
                        "Thursday"  = @()
                        "Friday"    = @()
                        "Saturday"  = @()
                        "Sunday"    = @()
    }
    $DaysOfMonth = New-Object -TypeName PSObject -Property $Props

    #We start on day one and add the numeric values to parse through the dates
    $DaysToProcess = @(0..($LastDate.Day - 1))
    Foreach ($Day in $DaysToProcess) {
        $Date = (Get-Date $FirstDate).AddDays($Day)
        #Get dates corresponding the 7 weekdays
        $CurDayValue = $Date.DayOfWeek.value__
        if ($CurDayValue -eq 0) {
            $DaysOfMonth.Sunday += $Date
        }
        if ($CurDayValue -eq 1) {
            $DaysOfMonth.Monday += $Date
        }
        if ($CurDayValue -eq 2) {
            $DaysOfMonth.Tuesday += $Date
        }
        if ($CurDayValue -eq 3) {
            $DaysOfMonth.Wednesday += $Date
        }
        if ($CurDayValue -eq 4) {
            $DaysOfMonth.Thursday += $Date
        }
        if ($CurDayValue -eq 5) {
            $DaysOfMonth.Friday += $Date
        }
        if ($CurDayValue -eq 6) {
            $DaysOfMonth.Saturday += $Date
        }
    }
    # Is there an actual $Weekday number $number in the selected month, or is it out of bounds.
    $NumberWithinRange = ($number -ge -$DaysOfMonth.$Weekday.count -and $number -le $DaysOfMonth.$Weekday.Count -and -not [string]::IsNullOrWhiteSpace($number) )

    if ($Weekday -and $NumberWithinRange) {
        if ($number -lt 0) {
            $number = $number 
        }
        else {
            $number = $number - 1
        }
        Return $DaysOfMonth.$Weekday[($number)]
    }

    if ($Weekday -and -not $NumberWithinRange -and -not [string]::IsNullOrWhiteSpace($number)) {
        Write-Warning "No $Weekday number $number in selected month" 
        break   
    }

    if ($Weekday) {
        Return $DaysOfMonth.$Weekday
    }
    if ($Number) {
        $Days = $DaysOfMonth | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name | Sort-Object
        foreach ($Day in $days) {
            #Recycle the $props from earlier with weekdays in correct order
            $props.$Day = $DaysOfMonth.$Day[($number - 1)]
        }
        $Result = New-Object -TypeName PSObject -Property $props
        Return $Result
    }

    Return $DaysOfMonth
}

Function Get-StringASCIIValues {
[CMDLetbinding()]
    param (
        [string]$String
   )

   return $String.ToCharArray() | ForEach-Object {$_ + " : " + [int][Char]$_}
}

Function Get-StringHash {
    #https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-7.1 (ex. 4)
    [CMDLetBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]]$String,
        [Parameter(Mandatory = $false)]
        [ValidateSet("SHA1","SHA256","SHA384","SHA512","MACTripleDES","MD5","RIPEMD160")]
        [string]$Algorithm = "SHA256",
        [Parameter(Mandatory = $false)]
        [int]$GroupCount = 2,
        [Parameter(Mandatory = $false)]
        [String]$Seperator = "-"
    )

    $Result = @()
    Write-Verbose "Received $($String.count) string(s)"
    Write-Verbose "Using algorithm: $Algorithm"
    $stringAsStream = [System.IO.MemoryStream]::new()
    $writer = [System.IO.StreamWriter]::new($stringAsStream)
    foreach ($t in $String) {
        $writer.write($t)
        $writer.Flush()
        $stringAsStream.Position = 0
        $HashString = Get-FileHash -InputStream $stringAsStream -Algorithm $Algorithm | Select-Object -ExpandProperty Hash
        Write-Verbose "$($HashString.length) characters in hash"
    }

    Write-Verbose "Dividing string to groups of $GroupCount characters, seperated by $Seperator"
    for ($x = 0 ; $x -lt $HashString.Length ; $x = $x + $GroupCount) {
        $Result += $HashString[$x..($x + ($GroupCount -1))] -join ""
    }
    Write-Verbose "$($Result.count) groups"
    $Result = $Result -join $Seperator
    Write-Verbose "Returning $($Result.length) character string"

    return $Result
}

Function Get-TMSAutomationJob {
    <#
        .SYNOPSIS
        Get Automation job details for TMS tasks

        .parameter RunbookName
        Speeds up job search (this is a standard parameter of Get-AzAutomationJob)

        .Parameter DeploymentID
        Deployment ID to look for in JSON input for runbook

        .Parameter ResourceGroupName
        Name of resourcegroup of automation account

        .Parameter AutomationAccountName
        Name of Automation Account to look in.

        .Parameter StartTime
        StartTime to search from

    #>

    [CMDLetbinding()]
    param(
        [String]$RunbookName,
        [Parameter(Mandatory = $true)]
        [String]$DeploymentID,
        [String]$ResourceGroupName = (Get-TMSAutomationVariable -Name ResourceGroupName),
        [String]$AutomationAccountName = (Get-TMSAutomationVariable -Name AutomationAccountName),
        [Datetime]$StartTime = (get-date).addhours(-2)
    )

    $Splat = @{"ResourceGroupName" = $ResourceGroupName
                "AutomationAccountName" = $AutomationAccountName
                "Starttime" = $StartTime
                }

    if ($RunbookName) {
        Write-Verbose "Looking for jobs for specifik runbook: '$Runbookname'"
        $splat.Add("RunbookName","$RunbookName")
    }
    Write-Verbose "Getting jobs:"
    Write-Verbose ($Splat | Convertto-JSON)
    $Jobs = Get-AzAutomationJob @Splat | get-azautomationjob # First get metadata, then details :-/
    
    Write-Verbose "$($Jobs.count) jobs found. Looking for deployment ID"
    $Result = @()
    $JobCounter = 0
    foreach ($job in ($jobs | Where-Object {$_.JobParameters.json})) {
        $JobCounter++
        if (($job.JobParameters.json | ConvertFrom-JSON).DeploymentId -eq $DeploymentID) {
            Write-Verbose "$($JobCounter)/$($Jobs.count) : Job matched deployment id '$DeploymentId'"
            $Result += $job
        }
        else {
            Write-Verbose "$($JobCounter)/$($Jobs.count) : Job did not match deployment id '$DeploymentId'"
        }
    }
    Write-Verbose "Returning $($Result.count) jobs for deployment id '$DeploymentId'"
    return $Result 
}
Function Get-Unicode {
    param(
        [string]$Word
    )
    $word.ToCharArray() | ForEach-Object {
        $_ + ": " + [int][char]$_ 
    }
}

Function Get-UserVariables {
    #Get, and display, any variable not defined in the $SysVars variable
    get-childitem variable: | Where-Object {$SysVars -notcontains $_.Name}
}

Function Get-WebRequestError {
<#
        .SYNOPSIS
        Read more detailed error from failed Invoke-Webrequest and Invoke-RestMethod
        https://stackoverflow.com/questions/35986647/how-do-i-get-the-body-of-a-web-request-that-returned-400-bad-request-from-invoke

        .Parameter ErrorObject
        $_ from a Catch block

    #>


    [CMDLetbinding()]
    param (
        [object]$ErrorObject
    )

    $streamReader = [System.IO.StreamReader]::new($ErrorObject.Exception.Response.GetResponseStream())
    $ErrResp = $streamReader.ReadToEnd() | ConvertFrom-Json
    $streamReader.Close()
    return $ErrResp
}

Function New-CSVExportable {
    param($Object
    )
    # Gennemgår properties for alle elementer, og tilføjer manglende så alle elementer har samme properties til CSV eksport eller lignende
    $AllMembers = @()
    foreach ($Item in $Object) {
        $ItemMembers = ($item | ConvertTo-Csv -NoTypeInformation -Delimiter ";")[0] -split ";" -replace '"','' #For at sikre vi får alle properties i den korrekte rækkefølge (Get-Member kan være lidt tilfældig i rækkefølgen)
        foreach ($itemmember in $ItemMembers) {
            if ($ItemMember -notin $AllMembers) {
                $AllMembers += $ItemMember
            }
        }
        
    }
    #$AllMembers = $AllMembers | Select-Object -Unique

    for ($x = 0 ; $x -lt $Object.Count ; $x++) {
        $CurMembers = $Object[$x] | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
        for ($y = 0 ; $y -lt $AllMembers.count ; $y++) {
            if ($AllMembers[$y] -notin $CurMembers) {
                $Object[$x] | Add-Member -MemberType NoteProperty -Name $AllMembers[$y] -Value "N/A"
            }
        }
    }
    return $Object

}

Function New-YAMLTemplate {
 
    <#
    .SYNOPSIS
    Generates Azure Pipelines based on the comment based help, and parameter definitions, in the input script

    .Description
    Generates Azure Pipelines based on the comment based help in the input script.
    For help on comment based help see https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.2
    Script parameters are parsed, and YAML template, pipeline and variable files are generated with default values, and validateset values prepopulated.

    Default variables that will be defined in pipeline yaml:
        deployment: DeploymentDisplayName converted to lower-case, spaces replaced with underscores, and non-alphanumeric characters removed.
        deploymentDisplayName: Value of DeploymentDisplayName parameter

    For each supplied environment, default variables will be created:
        <ENV>_ServiceConnectionAD: '<ServiceConnection parameter value>'
        <ENV>_ServiceConnectionResources: '<ServiceConnection parameter value>'
        <ENV>_EnvironmentName: '<ENV>'

    Unless otherwise specified with the -Agent parameter, the template will use the first of the valid options from the validateset.

    The script will output the following files:
        if -WikiOnly is not specified:
            <scriptname>.yml - the template for the script itself.
            deploy-<scriptname>.yml.sample - a sample pipeline yaml.
            deploy-<scriptname>-<environment>-vars.yml - a variable file for the <environment> stage of the pipeline file. One file for each environment specified in -Environment parameter
        if -WikiOnly, or -Wiki, is specified:
            <scriptname>.md - A Wiki file based on the comment based help.

    Outputfiles will be placed in the same directory as the source script, unless the -OutDir parameter is specified.
    The template for the script will have the extension .yml and the sample files will have the extension .yml.sample so the yaml selection list in Azure DevOps isn't crowded.


    .Parameter ScriptPath
    Path to script to generate YAML templates for

    .Parameter DeploymentDisplayName
    Display Name of deployment when pipeline is run. Will default to "Deployment of <scriptfile>"

    .Parameter Environment
    Name of environment(s) to deploy to. Will default to "Dev"

    .Parameter Overwrite
    Overwrite existing YAML files

    .Parameter ServiceConnection
    Name of serviceconnection. Will default to "IteraCCoEMG"

    .Parameter Wiki
    Generate Wiki file as well as template files.

    .Parameter WikiOnly
    Determines if only a Wiki should be generated.

    .PARAMETER NoSample
    Don't add .sample extension to pipeline files

    .Parameter OutDir
    Directory to output files to. If omitted file will be written to script location.

    .Parameter Agent
    Agent type to use when running pipeline. If none specified, default will be first in ValidateSet.

    .Example
    PS> $ScriptPath = "C:\Scripts\AwesomeScript.ps1"
    PS> New-YAMLTemplate -ScriptPath $ScriptPath -Environment "DEV","TEST" -Overwrite

    This will generate a template file, a pipeline file and two variable files for deployment of C:\Scripts\AwesomeScript.ps1 to DEV and TEST environments.
    Existing files will be overwritten, and files placed in C:\Scripts
    No Wiki file will be created.


    .Example
    PS> $ScriptPath = "C:\Scripts\AwesomeScript.ps1"
    PS> New-YAMLTemplate -ScriptPath $ScriptPath -Wiki -Environment Prod

    This will generate a template file, a pipeline file, a single varibles files for deployment of C:\Scripts\AwesomeScript.ps1 to Prod environment, as well as a Wiki file.
    If files already exist the script will return a message stating no files are generated.

    .Example
    PS> $ScriptPath = "C:\Scripts\AwesomeScript.ps1"
    PS> New-YAMLTemplate -ScriptPath $ScriptPath -WikiOnly -OutDir C:\Wikis -OverWrite

    This will generate a Wiki file only as C:\Wikis\AwesomeScript.md
    If the file already exist, it will be overwritten.

    #>


    # TODO: Fix so Wikis doesn't strip $-signs
    
    [CMDLetbinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Get-ChildItem -File -Path $_ })]
        [String]$ScriptPath,
        [Parameter(Mandatory = $false)]
        [String]$DeploymentDisplayName = "Deployment of $(Split-Path -Path $ScriptPath -Leaf)",
        [Parameter(Mandatory = $false)]
        [String[]]$Environment = "Test",
        [Parameter(Mandatory = $false)]
        [Switch]$Overwrite,
        [Parameter(Mandatory = $false)]
        [String]$ServiceConnection = "IteraCCoEMG",
        [Parameter(Mandatory = $false)]
        [switch]$WikiOnly,
        [Parameter(Mandatory = $false)]
        [switch]$Wiki,
        [Parameter(Mandatory = $false)]
        [Switch]$NoSample,
        [Parameter(Mandatory = $false)]
        [ValidateScript({ Test-Path -Path $_ })]
        [String]$OutDir,
        [Parameter(Mandatory = $false)]
        [ValidateSet("Mastercard Payment Services", "vmImage: windows-latest", "vmImage: ubuntu-latest")]
        [String]$Agent 
    )

    # Pipeline PowerShell task: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/powershell?view=azure-devops

    $ScriptName         = Split-Path -Path $ScriptPath -Leaf
    $ScriptDirectory    = Split-Path -Path $ScriptPath -Parent
    # This weird way of getting the file is necessary to get filesystem name casing. Otherwise whatever casing is passed in the parameter is used.
    # Should be obsolete after lower-case standards have been decided, except for producing a warning.
    $ScriptFile         = Get-Item -Path "$ScriptDirectory\*.*" | Where-Object { $_.Name -eq $ScriptName }

    # Retrieve info from comment-based help and parameter definitions.
    $ScriptHelp                 = Get-Help -Name $ScriptPath -full 
    $ScriptCommand              = (Get-Command -Name $ScriptPath)
    $ScriptCommandParameters    = $ScriptCommand.Parameters
    $ScriptHelpParameters       = $ScriptHelp.parameters
    $ScriptBaseName             = $ScriptFile.BaseName
    $VariablePrefix             = $ScriptBaseName.replace("-", "_").ToLower() # String to prefix variable names, to avoid duplicate names when adding several scripts to same pipeline varaibles file
    $ScriptExamples             = $ScriptHelp.Examples
    $ScriptSynopsis             = ($ScriptHelp.Synopsis | foreach-object { $_ | Out-String } ) -split "`r`n|`n" 
    $ScriptNotes                = if ($ScriptHelp.AlertSet.Alert.count -gt 0) { ($ScriptHelp.alertset.alert[0] | foreach-object { $_ | Out-String } ) -split "`r`n|`n" | ForEach-Object { if ( -not [string]::isnullorwhitespace($_)) { $_ } } }
    $ScriptLinks                = if ($ScriptHelp.relatedLinks.navigationLink.count -gt 0) { $ScriptHelp.relatedLinks.navigationLink | foreach-object {"[$($_.Uri)]($($_.Uri))"}}
    $ScriptDescription          = ($ScriptHelp.description | foreach-object { $_ | Out-String } ) -split ("`r`n") 
        
    # Header with a few reminderes, for the variables and pipeline files. soon-to-be obsolete when we all have become pipeline-gurus!
    $PipelineVariablesHeader = @()
    $PipelineVariablesHeader += '# Variable names cannot contain hyphens. Use underscores instead. If using double quotes, remember to escape special characters'
    $PipelineVariablesHeader += '# Booleans, and Numbers, must be passed as ${{variables.<variablename>}} to template to retain data type when received by template.'
    $PipelineVariablesHeader += '# Booleans still need to be prefixed with $ when passed to script, because Pipelines sucks (https://www.codewrecks.com/post/azdo/pipeline/powershell-boolean/)'
    $PipelineVariablesHeader += '# Split long strings to multiple lines by using >- , indenting value-lines ONE additional level and NO quotation marks surrounding entire value (https://yaml-multiline.info/ - (folded + strip))'


    # Get path of script relative to repo if possible.
    Push-Location -Path (Split-Path -Path $ScriptPath -Parent) -StackName RelativePath
    try {
        $RepoScriptPath = & git ls-files --full-name $ScriptPath
        if ([string]::IsNullOrWhiteSpace($RepoScriptPath)) {
            $RepoScriptPath = & git ls-files --full-name $ScriptPath --others
        }
            
        if ([string]::IsNullOrWhiteSpace($RepoScriptPath)) {
            throw "Couldn't run git or repo not found"
        }
        else {
            # We found the script as part of repo, so lets guess the project, repository and direct URL for the Wiki as well
            $RemoteGit          = & git remote -v # Returns something similar to 'origin https://<organization>@dev.azure.com/<organization>/<project>/_git/<repository> (push)'
            $RemoteURL          = "https://" + $RemoteGit.split(" ")[0].split("@")[-1] + "?path=/$RepoScriptPath"
            $RemotePath         = $RemoteGit[0].split("/")
            $DevOpsProject      = $RemotePath[4]
            $DevOpsRepository   = $RemotePath[6] -replace "\(fetch\)|\(push\)", ""
        }
    }
    catch {
        # If we can't find the script as part of the repo, fallback to the path relative to current location
        $RepoScriptPath = (Resolve-Path -Path $ScriptPath -Relative).replace("\", "/")
        Write-Warning "Couldn't find path relative to repository. Is file in a repo or git not installed? Relative references will fall back to $RepoScriptPath"
        Write-Warning $_.Exception.Message 
    }
    Pop-Location -StackName RelativePath
        
    if ($RepoScriptPath -cmatch "[A-Z]") {
        # File names should be in lower case in accordance with https://dev.azure.com/itera-dk/Mastercard.PaymentsOnboarding/_git/infrastructure?path=/readme.md
        Write-Warning "Scriptpath not in all lowercase: $RepoScriptPath"
    }

    if ([string]::isnullorwhitespace($OutDir)) {
        [string]$OutDir = $ScriptFile.DirectoryName.ToString()
    }
    $FSTemplateFilePath     = Join-Path -Path $OutDir -ChildPath $ScriptFile.Name.Replace(".ps1", ".yml")
    $FSPipelineFilePath     = Join-Path -Path (Split-Path -Path $FSTemplateFilePath -Parent) -ChildPath ("deploy-$(Split-Path -Path $FSTemplateFilePath -Leaf)")
    $FSVariablesFilePath    = @{}
    foreach ($env in $environment.tolower()) {
        $FSVariablesFilePath[$env] = $FSPipelineFilePath.Replace(".yml", "-$($env)-vars.yml") #Template for variable files. #ENV# is replace with corresponding environment name.
    }
    $FSWikiFilePath = $FSTemplateFilePath.replace(".yml", ".md") 
    # Wiki uses dash'es for spaces, and hex values for dash'es, so replace those in the filename.
    $FSWikiFilePath = Join-Path (Split-Path -Path $FSWikiFilePath) -ChildPath ((Split-Path -Path $FSWikiFilePath -Leaf).Replace("-", "%2D").Replace(" ", "-"))

    # Get path to yaml template file, relative to script location
    $RepoTemplateFilePath   = $RepoScriptPath.replace(".ps1", ".yml")
    if ([string]::IsNullOrWhiteSpace((Split-Path -Path $RepoTemplateFilePath))) {
        # If script is located in root of repo, we cant split-path it
        $RepoPipelineFilePath  = "/deploy-$(Split-Path -Path $RepoTemplateFilePath -Leaf)"
    }
    else {
        $RepoPipelineFilePath   = "/" + (Join-Path -Path (Split-Path -Path $RepoTemplateFilePath) -ChildPath ("deploy-$(Split-Path -Path $RepoTemplateFilePath -Leaf)")).replace("\","/")
    }

    # Save variable filenames in hashtable for easy references
    $RepoVariablesFilePath  = @{}
    foreach ($env in $environment.tolower()) {
        $RepoVariablesFilePath[$env] = $RepoPipelineFilePath.Replace(".yml", "-$($env)-vars.yml") #Template for variable files. #ENV# is replace with corresponding environment name.
    }

    #$RepoWikiFilePath = $RepoTemplateFilePath.replace(".yml", ".md") # Maybe we'll need this one day.... maybe not


    # Parse the parameters and get necessary values for YAML generation
    $ScriptParameters = @()
    foreach ($param in $ScriptHelpParameters.parameter) {
        $Command    = $ScriptCommandParameters[$param.name]
        $Props      = [ordered]@{   "Description"                 = $param.description 
                                    "Name"                        = $param.name
                                    "HelpMessage"                 = ($Command.Attributes | Where-Object { $_.GetType().Name -eq "ParameterAttribute" }).HelpMessage
                                    "Type"                        = $param.type
                                    "Required"                    = $param.required
                                    "DefaultValue"                = $param.defaultValue
                                    "ValidateSet"                 = ($Command.Attributes | Where-Object { $_.GetType().Name -eq "ValidateSetAttribute" }).ValidValues
                                    "ValidateScript"              = ($Command.Attributes | Where-Object { $_.GetType().Name -eq "ValidateScriptAttribute" }).scriptblock
                                }

        # Build a description text to add to variables, and parameters, in YAML files
        $YAMLHelp = ""
        if ($props.Description.length -gt 0) {
            $YAMLHelp += "$((($props.Description | foreach-object {$_.Text}) -join " ") -replace ("`r`n|`n|`r", " "))"
        }
        
        if ($Props.HelpMessage.Length -gt 0) {
            $YAMLHelp += " Help: $($Props.HelpMessage)"
        }

        $YAMLHelp += " Required: $($param.required)"

        if ($Props.ValidateSet.Count -gt 0) {
            $YAMLHelp += " ValidateSet: ($(($Props.ValidateSet | ForEach-Object {"'$_'"}) -join ","))"
        }

        if ($Props.ValidateScript.Length -gt 0) {
            $YAMLHelp += " ValidateScript: {$($Props.ValidateScript)}"
        }

        if ($YAMLHelp.Length -gt 0) {
            $Props.add("YAMLHelp", $YAMLHelp.Trim())
        }
        
        $ScriptParameters += New-Object -TypeName PSObject -Property $Props
    }

    if ($ScriptParameters.count -eq 0) {
        Write-Warning "No parameters found for $ScriptPath. Make sure comment based help is correctly entered: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-7.2"
    }

    # Build the YAMLParameters object containing more YAML specific information (could be done in previous loop... to do someday)
    $YAMLParameters     = @()
    $ScriptArguments    = ""
    foreach ($param in $ScriptParameters) {
        $ParamType = $ParamDefaultValue = $null 
        # There are really only 3 parameter types we can use when running Powershell in a pipeline
        switch ($Param.Type.Name ) {
            "SwitchParameter"                                   { $ParamType = "boolean" }
            { $_ -match "Int|Int32|long|byte|double|single" }   { $ParamType = "number" }
            default                                             { $ParamType = "string" } # Undeclared parameters will be of type Object and treated as string
        }

        # Not a proper switch, but this is where we figure out the correct default value
        switch ($Param.DefaultValue) {
            { $_ -match "\$" }                                                                          { $ParamDefaultValue = "'' # Scriptet default: $($Param.DefaultValue)" ; break } # If default value contains $ it most likely references another parameter.
            { (-not ([string]::IsNullOrWhiteSpace($Param.DefaultValue)) -and $ParamType -eq "String") } { $ParamDefaultValue = "'$($param.defaultValue)'" ; break } # Add single quotes around string values
            { $ParamType -eq "number" }                                                                 { $ParamDefaultValue = "$($param.defaultValue)" ; break } # No quotes around numbers as that would make it a string
            { $ParamType -eq "boolean" }                                                                { if ($param.defaultvalue -eq $true) { $ParamDefaultValue = "true" } else { $ParamDefaultValue = "false" } ; break } # Set a default value for booleans as well
            { $Param.ValidateSet.count -gt 0 }                                                          { if ($Param.ValidateSet -contains " ") { $ParamDefaultValue = "' '" } else { $ParamDefaultValue = "'$($Param.ValidateSet[0])'" } }
            default                                                                                     { $ParamDefaultValue = "''" } # If all else fails, set the default value to empty string
        }

        $YAMLParameterProps = @{"Name"          = $Param.name
                                "YAMLHelp"      = $Param.YAMLHelp
                                "Type"          = $ParamType
                                "Default"       = $ParamDefaultValue
                                "ValidateSet"   = $param.validateSet 
                                "VariableName"  = "$($VariablePrefix)_$($param.name)" # Property to use as variable name in YAML. #ENV# will be replaced with the different environments to deploy to
                                }
        $YAMLParameters += New-Object -TypeName PSObject -Property $YAMLParameterProps

        # Define the scriptarguments to pass to the script. The name of the variable will correspond with the name of the parameter
        if ($ParamType -eq "boolean") {
            $ScriptArguments += ("-$($Param.Name):`$`${{parameters.$($Param.name)}} ") # Add additional $ to turn "false" into "$false"
        }
        elseif ($param.type.name -eq "String") {
            $ScriptArguments += ("-$($Param.Name) '`${{parameters.$($Param.name)}}' ") # Make sure string values has single quotes around them so spaces and special characters survive
        } 
        else {
            #integer type
            $ScriptArguments += ("-$($Param.Name) `${{parameters.$($Param.name)}} ") # Numbers as-is
        } 
        if ($YAMLParameters[-1].VariableName.Length -gt $MaxParameterNameLength) {
            $MaxParameterNameLength = $YAMLParameters[-1].VariableName.Length # Used for padding in pipeline and variables file, to make them less messy.
        }
    } 
    $MaxParameterNameLength++ # To account for the colon in the YAML

    # Initialize PipelineVariables and set the corresponding template as comment
    # $PipelineVariables contains the content of the variable files for each environment
    $PipelineVariables = @()
    $PipelineVariables += " # $(Split-Path -Path $RepoTemplateFilePath -leaf)"

    # Default template parameters independent of script parameters
    $TemplateParameters = @()
    $TemplateParameters += " - name: serviceConnection # The name of the service Connection to use"
    $TemplateParameters += " type: string"
    $TemplateParameters += " default: false"

    # Build the template parameters
    foreach ($param in $YAMLParameters) {
        $TemplateParameters += ""
        $TemplateParameters += " - name: $($param.Name) # $($Param.YAMLHelp)"
        $TemplateParameters += " type: $($Param.type)"
        $TemplateParameters += " default: $($Param.Default)"
        if ($param.validateset) {
            $TemplateParameters += " values:"
            foreach ($value in $param.validateset) {
                if ($param.Type -eq "number") {
                    $TemplateParameters += " - $value"
                }
                else {
                    $TemplateParameters += " - '$value'"
                }
            }
        }
        $PipelineVariables += " $("$($Param.VariableName):".PadRight($MaxParameterNameLength)) $($param.Default) # $($Param.YAMLHelp)"
    }

    #region BuildTemplate
    $Template = @()
    $Template += "# Template to deploy $($ScriptFile.Name):"
    $Template += ""

    # Add script synopsis to template file if available
    if ($ScriptSynopsis.length -gt 0) {
        $Template += "# Synopsis:"
        $Template += $ScriptSynopsis | foreach-object {"#`t $_"}
    }
    $Template += ""

    # Add script description to template file if available
    if ($ScriptDescription.length -gt 0) {
        $Template += "# Description:"
        $Template += $ScriptDescription | ForEach-Object {"#`t $_"}
    }
    $Template += ""

    # Add script notes to template file if available
    if ($ScriptNotes.length -gt 0) {
        $Template += "# Notes:"
        $Template += $ScriptNotes | foreach-object {"#`t $_"}
    }
    $Template += ""

    $Template += "parameters:"
    $Template += $TemplateParameters
    $Template += ""
    $Template += "steps:"
    $Template += " - task: AzurePowerShell@5"
    $Template += " displayName: ""PS: $($ScriptFile.Name)"""
    $Template += " inputs:"
    $Template += ' azureSubscription: "${{parameters.serviceConnection}}"'
    $Template += ' scriptType: "FilePath"'
    $Template += " scriptPath: ""$RepoScriptPath"" # Relative to repo root"
    $Template += " azurePowerShellVersion: latestVersion"
    $Template += " scriptArguments: $ScriptArguments"
    $Template += " pwsh: true # Run in PowerShell Core"
    #endregion #BuildTemplate

    # Make variables nicely alligned
    $MaxEnvLength = $Environment | Group-Object -Property Length | Sort-Object -Property Name -Descending | Select-Object -First 1 -ExpandProperty Name
    $PadTo = "_ServiceConnectionResources".Length + $MaxEnvLength + " ".Length
    
    # Get agent options from ValidateSet for Agent parameter of this script/function
    $Command = Get-Command -Name $MyInvocation.MyCommand
    $ValidAgents = ($Command.Parameters["Agent"].Attributes | Where-Object { $_.GetType().Name -eq "ValidateSetAttribute" }).ValidValues

    if ($Agent) {
        $SelectedAgent = $Agent 
    }
    else {
        $SelectedAgent = $ValidAgents[0]
    }
    $AgentOptions = $ValidAgents | Where-Object {$_ -ne $SelectedAgent}

    #region BuildPipeline
    $Pipeline = @()
    $Pipeline += "# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/?view=azure-pipelines"
    $Pipeline += ""
    $Pipeline += "trigger: none"
    $Pipeline += ""
    $Pipeline += $PipelineVariablesHeader
    $Pipeline += "variables:"
    $Pipeline += " # Pipeline variables"
    $Pipeline += " $("deployment:".PadRight($PadTo)) '$((($DeploymentDisplayName.ToCharArray() | Where-Object {$_ -match '[\w| ]'}) -join '').replace(" ","_").tolower())' `t`t# Name of deployment"
    $Pipeline += " $("deploymentDisplayName:".PadRight($PadTo)) '$DeploymentDisplayName' `t# Name of deployment"
    foreach ($env in $Environment) {
        $Pipeline += ""
        $Pipeline += " $("$($env)_ServiceConnectionAD:".PadRight($PadTo)) '$($ServiceConnection)' `t`t# Name of connection to use for AD deployments in $env environment"
        $Pipeline += " $("$($env)_ServiceConnectionResources:".PadRight($PadTo)) '$($ServiceConnection)' `t`t# Name of connection to use for Azure resource deployments in $env environment"
        $Pipeline += " $("$($env)_EnvironmentName:".PadRight($PadTo)) '$env'"
    }


    $Pipeline += ""
    $Pipeline += "# Comment/remove incorrect agents"
    $Pipeline += "pool:" 
    $Pipeline += " $SelectedAgent"
    foreach ($agnt in $AgentOptions) {
        $Pipeline += " #$agnt"
    }

    $Pipeline += ""
    $Pipeline += "stages:"
    foreach ($env in $Environment) {
        $Pipeline += "# $env"
        $Pipeline += " - stage: '`${{variables.$($env)_EnvironmentName}}'"
        $Pipeline += " displayName: '`${{variables.$($env)_EnvironmentName}}'"

        $Pipeline += " variables:"
        $Pipeline += " - template: '$($RepoVariablesFilePath[$env])'"

        $Pipeline += " jobs:"
        $Pipeline += " - deployment: '`${{variables.deployment}}'"
        $Pipeline += " displayName: '`${{variables.deploymentDisplayName}}'"
        $Pipeline += " environment: '`${{variables.$($env)_EnvironmentName}}'"
        $Pipeline += " strategy:"
        $Pipeline += " runOnce:"
        $Pipeline += " deploy:"
        $Pipeline += " steps:"
        $Pipeline += " - checkout: self"
        if ($ScriptSynopsis.Length -gt 0) {
            $Pipeline += " " + ($ScriptSynopsis | ForEach-Object {"# $_"})
        }
        $Pipeline += " - template: '/$RepoTemplateFilePath' #Template paths should be relative to this file. For absolute path use /path/to/template"
        $Pipeline += " parameters:"
        $Pipeline += " $("#serviceConnection:".PadRight($MaxParameterNameLength)) `$`{{variables.$($env)_ServiceConnectionAD}} # Comment/remove the incorrect connection!" 
        $Pipeline += " $("serviceConnection:".PadRight($MaxParameterNameLength)) `$`{{variables.$($env)_ServiceConnectionResources}}"
        foreach ($param in $YAMLParameters) {
            $ParamValue = "`${{variables.$($param.VariableName)}}"
            $Pipeline += " $("$($param.Name):".PadRight($MaxParameterNameLength)) $ParamValue"
        }
        $Pipeline += ""
    }
    #endregion BuildPipeline

    #Finally output the files
    if ($NoSample) {
        $Suffix = ""
    }
    else {
        $Suffix = ".sample"
    }

    try {
        if ($Wiki -or $WikiOnly) {
            $ScriptWiki = @("<b>Name:</b> $(Split-Path $ScriptPath -Leaf)"
                ""
                "<b>Project:</b> $DevOpsProject"
                ""
                "<b>Repository:</b> $DevOpsRepository"
                ""
                "<b>Path:</b> <a href=""$RemoteURL""> $RepoScriptPath</a>"
                ""
                "<b>Synopsis:</b>"
                $ScriptSynopsis
                ""
                "<b>Description:</b>"
                $ScriptDescription
                ""
                if ($ScriptNotes.Length -gt 0) {
                    "<b>Notes:</b><br>"
                    ($ScriptNotes | Out-String).trim()
                    ""
                }
                ""
                if ($ScriptLinks.count -gt 0) {
                    "<b>Links:</b><br>"
                    ($ScriptLinks | Out-String).trim()
                    ""
                }
                "<b>Examples</b><br>"
                ($ScriptExamples | Out-String).trim()
                ""
                "<b>Parameters</b>"
                ($ScriptHelpParameters | Out-String) -split "`r`n|`n" | ForEach-Object {$_.trim()}
            ) -join "`r`n"
            $ScriptWiki | Out-File -FilePath $FSWikiFilePath -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
        }
        if (-not $WikiOnly) {
            $PipelineVariablesHeader += "variables:"
            $PipelineVariables = $PipelineVariablesHeader + $PipelineVariables
            foreach ($env in $Environment) {
                $PipelineVariables | Out-File -FilePath "$($FSVariablesFilePath[$Env])$Suffix" -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
            }
            $Pipeline   | Out-File -FilePath "$FSPipelineFilePath$Suffix" -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
            $Template   | Out-File -FilePath $FSTemplateFilePath -Encoding utf8 -NoClobber:(-not $Overwrite) -Force:$Overwrite
        }
    }
    catch {
        Write-Error "Unable to write files"
        Write-Error $_.Exception.Message 
    }
}

Function Prompt {
    if ( (Test-Path (Join-Path -Path "variable:" -ChildPath "PSDebugContext"))) {
        $nestedPromptLevel++
        $BasePrompt = " [DBG] PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
    }
    else {
        $BasePrompt = " PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) ";
    }
    (get-date -Format HH:mm:ss).ToString().Replace(".",":") + $BasePrompt
    # .Link
    # https://go.microsoft.com/fwlink/?LinkID=225750
    # .ExternalHelp System.Management.Automation.dll-help.xml
}

Function Remove-OldModules {
[ CMDLetbinding()]
  Param ()

  $Modules = Get-Module -ListAvailable | Where-Object {$_.ModuleBase -notmatch "\.vscode"} #Theres probably a reason for vs code specific modules
  $DuplicateModules = $Modules | Group-Object -Property Name | Where-Object {$_.Count -gt 1} | Select-Object  -ExpandProperty Group
  foreach ($Module in $DuplicateModules) {
    $RemoveModules = $DuplicateModules | Where-Object {$_.Name -eq $Module.Name} | Sort-Object -Property Version -Descending | Select-Object -Skip 1
    foreach ($mod in $RemoveModules) {
      Write-Host "$($Module.Name) : $($Module.ModuleBase)"
      Remove-Module -Name $Module.Name -Force -ErrorAction SilentlyContinue
      Remove-Item -Path $Module.ModuleBase -Recurse -Force -Confirm:$false 
    }
  }
}


Function Remove-StringsFromHash {
    [CMDLetbinding()]
    Param(
        [ValidateScript({$_ -is [hashtable] -or $_ -is [System.Collections.Specialized.OrderedDictionary]})]
        [object]$Hashtable,
        [String[]]$ClearStrings = @("password","secret")
    )
 
    $Hash       = $Hashtable.Clone()
    $HashKeys   = $Hash.keys | ForEach-Object {$_.ToString()}

    foreach ($key in $HashKeys) {
        if ($hash[$key] -is [hashtable] -or $hash[$key] -is [System.Collections.Specialized.OrderedDictionary]) {
            $hash[$key] = Remove-StringsFromHash -Hashtable $Hash[$key] -ClearStrings $ClearStrings
        }
        else {
            foreach ($string in $ClearStrings) {
                if ($key -match $string) {
                    $Hash[$key] = "N/A"
                }
            }
        }
    }
    return $Hash 
}
Function Set-AzTestSetup {

    [CMDLetbinding()]
    param(
        [Parameter(Mandatory =$true)]
        [String[]]$ResourceGroupName,
        [string]$Prefix,
        [int]$NumWinVMs,
        [int]$NumLinuxVMs,
        [string]$VMAutoshutdownTime,
        [string]$WorkspaceName,
        [string]$AutomationAccountName,
        [String]$Location,
        [string]$KeyvaultName,
        [switch]$PowerOff,
        [switch]$Force,
        [switch]$Remove

    )
    foreach ($RG in $ResourceGroupName) {
        if (-not $PSBoundParameters.ContainsKey("Prefix")) {[string]$Prefix = $RG}
        if (-not $PSBoundParameters.ContainsKey("NumWinVMs")) {[int]$NumWinVMs = 2}
        if (-not $PSBoundParameters.ContainsKey("NumLinuxVMs")) {[int]$NumLinuxVMs = 0}
        if (-not $PSBoundParameters.ContainsKey("VMAutoshutdownTime")) {[string]$VMAutoshutdownTime = "2300"}
        if (-not $PSBoundParameters.ContainsKey("WorkspaceName")) {[string]$WorkspaceName = ($Prefix + "-workspace")}
        if (-not $PSBoundParameters.ContainsKey("AutomationAccountName")) {[string]$AutomationAccountName = ($Prefix + "-automation")}
        if (-not $PSBoundParameters.ContainsKey("Location")) {[String]$Location = "westeurope"}
        if (-not $PSBoundParameters.ContainsKey("KeyvaultName")) {[string]$KeyvaultName = ($Prefix + "-keyvault")}
        if ($KeyvaultName.Length -gt 24) {
            $KeyvaultName = "$($KeyvaultName.Substring(0,15))-keyvault"
            Write-Host "Keyvault name truncated to: $KeyVaultName"
        }
        try {
            if (Get-AzResourceGroup -Name $RG -ErrorAction SilentlyContinue) {
                Write-Host "$RG exist"
                if ($Force -or $Remove) {
                    Write-Host "`tWill be deleted"
                    $WorkSpace = Get-AzOperationalInsightsWorkspace -ResourceGroupName $RG -Name $WorkspaceName -ErrorAction SilentlyContinue
                    $Keyvault = Get-AzKeyVault -VaultName $KeyvaultName -ResourceGroupName $RG -ErrorAction SilentlyContinue
                    if ($null -eq $Keyvault) {
                        $keyvault = Get-AzKeyVault -VaultName $KeyvaultName -InRemovedState -Location $location 
                        if ($null -ne $Keyvault) {
                            Write-Host "`tDeleting $KeyvaultName"
                            $null = Remove-AzKeyVault -VaultName $KeyvaultName -InRemovedState -Force -Confirm:$false -Location $Location
                        }
                    }
                    else {
                        Write-Host "`tDeleting $KeyvaultName"
                        Remove-AzKeyVault -VaultName $KeyvaultName -Force -Confirm:$false -Location $location
                        Start-Sleep -Seconds 1 
                        Remove-AzKeyVault -VaultName $KeyvaultName -InRemovedState -Force -Confirm:$false -Location $location
                    }
                    if ($WorkSpace) {
                        Write-Host "`tDeleting Workspace"
                        $workspace | Remove-AzOperationalInsightsWorkspace -ForceDelete -Force -Confirm:$false 
                    }
                    Write-Host "`tDeleting Resourcegroup and contained resources"
                    Remove-AzResourceGroup -Name $RG -Force -Confirm:$false
                }
                else {
                    Write-Host "Nothing to do"
                    continue
                }
            }
            if ($Remove) {
                Write-Host "Remove specified. Exiting"
                continue 
            }

            Write-Host "Creating $RG"
            New-AzResourceGroup -Name $RG -Location $Location 
            Write-Host "Creating $AutomationAccountName"
            New-AzAutomationAccount -ResourceGroupName $RG -Name $AutomationAccountName -Location $Location -Plan Basic -AssignSystemIdentity 
            Write-Host "Creating $KeyvaultName"
            New-AzKeyVault -Name $KeyvaultName -ResourceGroupName $RG -Location $Location -EnabledForDeployment -EnabledForTemplateDeployment -EnabledForDiskEncryption -SoftDeleteRetentionInDays 7 -Sku Standard 
            Set-AzKeyVaultAccessPolicy -VaultName $KeyvaultName -ResourceGroupName $RG -UserPrincipalName "robert.eriksen_itera.com#EXT#@roedomlan.onmicrosoft.com" -PermissionsToKeys all -PermissionsToSecrets all -PermissionsToCertificates all -PermissionsToStorage all -Confirm:$false 
            Set-AzKeyVaultAccessPolicy -VaultName $KeyvaultName -ResourceGroupName $RG -ObjectId (Get-AzAutomationAccount -ResourceGroupName $RG -Name $AutomationAccountName).Identity.PrincipalId -PermissionsToKeys all -PermissionsToSecrets all -PermissionsToCertificates all -PermissionsToStorage all -Confirm:$false 
            Set-AzKeyVaultAccessPolicy -VaultName $KeyvaultName -ResourceGroupName $RG -ServicePrincipalName 04e7eb7d-da63-4c13-b5ba-04331145fdff -PermissionsToKeys all -PermissionsToSecrets all -PermissionsToCertificates all -PermissionsToStorage all -Confirm:$false 
            Write-Host "Creating $WorkspaceName"
            New-azOperationalInsightsWorkspace -ResourceGroupName $RG -Name $WorkspaceName -Location $location -Sku pergb2018
            $VMCredentials = [pscredential]::new("roe",("Pokemon1234!" | ConvertTo-SecureString -AsPlainText -Force))
            # https://www.powershellgallery.com/packages/HannelsToolBox/1.4.0/Content/Functions%5CEnable-AzureVMAutoShutdown.ps1
            $ShutdownPolicy = @{}
            $ShutdownPolicy.Add('status', 'Enabled')
            $ShutdownPolicy.Add('taskType', 'ComputeVmShutdownTask')
            $ShutdownPolicy.Add('dailyRecurrence', @{'time'= "$VMAutoshutdownTime"})
            $ShutdownPolicy.Add('timeZoneId', "Romance Standard Time")
            $ShutdownPolicy.Add('notificationSettings', @{status='enabled'; timeInMinutes=30; emailRecipient="robert.eriksen@itera.com" })
            $VMPrefix = "$($RG[0])$($RG[-1])"
            if ($NumWinVMs -gt 0) {
                (1..$NumWinVMs) | ForEach-Object {
                    $VMName = ([string]"$($VMPrefix)-Win-$( $_)")
                    Write-Host "Deploying $VMName"
                    $null = New-AzVm -ResourceGroupName $RG -Name $VMName -Location $Location -Credential $VMCredentials -VirtualNetworkName "$($RG)-vnet" -SubnetName "$($RG)-Subnet" -SecurityGroupName "$($RG)-nsg" -PublicIpAddressName "$($VMName)-Public-ip" -OpenPorts 80,3389 -Size "Standard_B2s" -Image Win2019Datacenter 
                    $vm = Get-AzVM -ResourceGroupName $RG -Name $VMName 
                    $vm | Stop-AzVM -Confirm:$false -Force 
                    $Disk = Get-AzDisk | Where-Object {$_.ManagedBy -eq $vm.id}
                    $Disk.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new("Standard_LRS")
                    $disk | Update-AzDisk
                    $rgName = $vm.ResourceGroupName
                    $vmName = $vm.Name
                    $location = $vm.Location
                    $VMResourceId = $VM.Id
                    $SubscriptionId = ($vm.Id).Split('/')[2]
                    $ScheduledShutdownResourceId = "/subscriptions/$SubscriptionId/resourceGroups/$rgName/providers/microsoft.devtestlab/schedules/shutdown-computevm-$vmName"
                    if ($VMAutoshutdownTime -ne "Off") {
                        Write-Host "Setting autoshutdown: $VMAutoshutdownTime"
                        $ShutdownPolicy['targetResourceId'] = $VMResourceId
                        $null = New-azResource -Location $location -ResourceId $ScheduledShutdownResourceId -Properties $ShutdownPolicy -Force  
                    }
                    if (-not $PowerOff) {
                        Write-Host "Starting VM"
                        $vm | Start-AzVM
                    }
                }
            }
            if ($NumLinuxVMs -gt 0) {
                (1..$NumLinuxVMs) | ForEach-Object {
                    $VMName = ([string]"$($VMPrefix)-Lin-$($_)")
                    Write-Host "Deploying $VMName"
                    $null = New-AzVm -ResourceGroupName $RG -Name $VMName -Location $Location -Credential $VMCredentials -VirtualNetworkName "$($RG)-vnet" -SubnetName "$($RG)-Subnet" -SecurityGroupName "$($RG)-nsg" -PublicIpAddressName "$($VMName)-Public-ip" -OpenPorts 80,3389 -Size "Standard_B2s" -Image UbuntuLTS
                    $vm = Get-AzVM -ResourceGroupName $RG -Name $VMName 
                    $vm | Stop-AzVM -Confirm:$false -Force 
                    $Disk = Get-AzDisk | Where-Object {$_.ManagedBy -eq $vm.id}
                    $Disk.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new("Standard_LRS")
                    $disk | Update-AzDisk
                    $rgName = $vm.ResourceGroupName
                    $vmName = $vm.Name
                    $location = $vm.Location
                    $VMResourceId = $VM.Id
                    $SubscriptionId = ($vm.Id).Split('/')[2]
                    $ScheduledShutdownResourceId = "/subscriptions/$SubscriptionId/resourceGroups/$rgName/providers/microsoft.devtestlab/schedules/shutdown-computevm-$vmName"
                    if ($VMAutoshutdownTime -ne "Off") {
                        Write-Host "Setting autoshutdown: $VMAutoshutdownTime"
                        $ShutdownPolicy['targetResourceId'] = $VMResourceId
                        $null = New-azResource -Location $location -ResourceId $ScheduledShutdownResourceId -Properties $ShutdownPolicy -Force  
                    }
                    if (-not $PowerOff) {
                        Write-Host "Starting VM"
                        $vm | Start-AzVM
                    }
                }
            }
        }
        catch {
            throw $_ 
        }
    }
}

Function Set-SystemVariables {
    #Collect all variables and store them, so userdefined variables can be easily cleared without restarting the PowerShell session
    New-Variable -Name 'SysVars' -Scope 'Global' -Force
    $global:SysVars = Get-Variable | Select-Object -ExpandProperty Name
    $global:SysVars += 'SysVars'
}

Function Upsert-SNOWIncident {
    <#
    .SYNOPSIS
    Creates, or updates, a ServiceNow incident

    .DESCRIPTION
    Creates, or updates, a ServiceNow incident. If no other parameters are specified the oldest incident, matching user_name/email and short_description will be updated.

    For REST API tutorials/documentation sign up for a developer instance at https://developer.servicenow.com/dev.do and find "REST API Explorer" in the "All" menu.
    Or create a dummy incident and look at the properties of the returned incident.

    If an existing incident, with unknown incident number, should be updated the minumum recommended JSON properties are:
        Email or user_name of caller.
        short_description: Short description (subject) of incident.
    
    A query is performed against ServiceNow, looking for incidents matching short description and, if available, caller.
    If neither email, or user_name, is included in JSON the username of provided credentials will be used.
    If none can be found, a new incident will be created.
    

    If creating a new incident a, suggested, basic set of properties is:

    {
        "assignment_group": "<name of assignment group>",
        "user_name / email": "username / e-mail>",
        "category": "<category>",
        "subcategory": "<subcategory>",
        "contact_type": "<contact type>",
        "short_description": "<subject of incident>",
        "description": "<detailed description of incident>",
        "impact": "<1-3>",
        "urgency": "<1-3>",
        "comments" : "public comment",
        "work_notes" : "internal comment"
    }

    A complete list of properties can be found in the ServiceNow REST API Explorer.
    Property values will should be entered, as they appear in the GUI, with respect for datatype (string/number).
    Any unknown properties, or properties with incorrect datatypes, will be ignored, and the field left blank in ServiceNow.
    In case of mandatory fields, the field will require modification before a user is able to edit the incident.
    If no user_name or email property is included, and a new incident is to be created, the username of the supplied credentials will be used.

    .EXAMPLE
    PS> $JSON = '{
        "caller_id": "a8f98bb0eb32010045e1a5115206fe3a",
        "short_description": "VM vulnerability findings",
    }'
    PS> Upsert-SNOWIncident -Credential $credential -JSON $json -SNOWInstance MyHelpDesk

    This will connect to the MyHelpDesk ServiceNow instance, and look for an existing, incident with state of "New", "In Progress" or "On Hold" with the user with sys_id a8f98bb0eb32010045e1a5115206fe3a as caller,
    and a short description of "VM vulnerability findings".
    If multiple matches are found, the incident with the oldest creation date will be updated with the information provided in the JSON.
    All other fields will preserve any existing values. Any required fiels will be mandatory when the incident is edited in ServiceNow.

    .EXAMPLE
    PS> $JSON = '{
        "assignment_group": "Service Desk",
        "email": "abraham.lincoln@example.com",
        "category": "Software",
        "contact_type": "Virtual Agent",
        "description": "100000 VMs with vulenrability findings found",
        "impact": "2",
        "short_description": "VM vulnerability findings",
        "subcategory": null,
        "urgency": "3",
        "assigned_to" : "beth.anglin@example.com"
    }'
    PS> Upsert-SNOWIncident -Credential $credential -JSON $json -UpdateNewest -SNOWInstance MyHelpDesk

    This will connect to the MyHelpDesk ServiceNow instance, and look for an existing, incident with state of "New", "In Progress" or "On Hold" with "abraham.lincoln@example.com" as caller
    and a short description of "VM vulnerability findings".
    If multiple matches are found, the incident with the newest creation date will be updated with the information provided in the JSON.
    All other fields will preserve any existing values. Any required fiels will be mandatory when the incident is edited in ServiceNow.
    
    .EXAMPLE
    PS> $JSON = '{
        "assignment_group": "Service Desk",
        "email": "abraham.lincoln@example.com",
        "category": "Software",
        "contact_type": "Virtual Agent",
        "description": "100000 VMs with vulenrability findings found",
        "impact": "2",
        "short_description": "VM vulnerability findings",
        "subcategory": null,
        "urgency": "3",
        "assigned_to" : "beth.anglin@example.com"
    }'
    PS> Upsert-SNOWIncident -Credential $credential -JSON $json -AlwaysCreateNew -SNOWInstance MyHelpDesk -Attachment C:\Temp\Report.CSV

    This will connect to the MyHelpDesk ServiceNow instance, create a new incident with the information provided in the JSON and attach C:\Temp\Report.csv.
    All other fields will preserve any existing values. Any required fiels will be mandatory when the incident is edited in ServiceNow.
        
    .PARAMETER IncidentNumber
    If updating a known incident provide the incident number here.

    .PARAMETER JSON
    JSON containing ServiceNow properties (see Description for details)

    .Parameter AddTimestampToComments
    Add timestamp to comments field when updating existing incident. If a new incident is created no timestamp is added to comments.

    .Parameter Attachment
    Files to attach to incident.

    .Parameter ContentType
    If a specific content type should be used for upload. If omitted a content-type will be guesstimated based on extension.

    .PARAMETER UpdateNewest
    Based on creation timestamp.
    Will look for existing incident matching Caller and ShortDescription and add Description to the newest incident's work notes.
    If omitted oldest incident will be updated. If no incident is found a new will be created.

    .PARAMETER AlwaysCreateNew
    Always create a new incident, even if similar already exist

    .Parameter AppendDescription
    Appends provided description to existing description if updating existing incident.

    .Parameter NoRunbookInfo
    Don't add runbook information to work notes, if called from automation account job.

    .Parameter Credential
    Credentials to connect to ServiceNow with

    .PARAMETER SNOWInstance
    Name of ServiceNow Instance (<SNOWInstance>.service-now.com).

    #>

    [CMDLetbinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [string]$IncidentNumber,
        [Parameter(Mandatory = $true)]
        [String]$JSON,
        [Parameter(Mandatory = $false)]
        [Switch]$AddTimestampToComments,
        [Parameter(Mandatory = $false)]
        [String[]]$Attachment,
        [Parameter(Mandatory = $false)]
        [String]$ContentType,
        [Parameter(Mandatory = $false)]
        [Switch]$UpdateNewest,
        [Parameter(Mandatory = $false)]
        [Switch]$AlwaysCreateNew,
        [Parameter(Mandatory = $false)]
        [Switch]$AppendDescription,
        [Parameter(Mandatory = $false)]
        [Switch]$NoRunbookInfo,
        [Parameter(Mandatory = $true)]
        [pscredential]$Credential,
        [Parameter(Mandatory = $true)]
        [String]$SNOWInstance
    )

    # Find out if we're runnning in Automation Account
    if ($null -ne (Get-ChildItem ENV:AUTOMATION_ASSET_SANDBOX_ID -ErrorAction SilentlyContinue)) {
        Write-Verbose "Running in Automation Account"
        $IsAutomationAccount = $true
    }
    else {
        $IsAutomationAccount = $false 
    }

    if ($IsAutomationAccount -and (-not $NoRunbookInfo)) {
        # If running in AA, get runbook info, unless disabled by NoRunbookInfo switch
        Write-Verbose "Running in Automation Account - Getting Runbookinfo"
        $Context = Get-AZContext 
        $AllSubscriptions = Get-AzSubscription | Where-Object {$_.TenantId -eq $Context.Tenant.Id}
        foreach ($sub in $AllSubscriptions) {
            Select-AzSubscription -SubscriptionObject $sub 
            $ThisSubscription = $sub
            try {
                $AutomationAccounts = Get-AzResource -ResourceType Microsoft.Automation/automationAccounts -ErrorAction Stop | Get-AzAutomationAccount -ErrorAction Stop
            }
            catch {
                $RunbookInfo = "Unknown Automation Account: $($ThisSubscription.Name) / $($ENV:Username) / $($Source)"
            }
            $JobId = $PSPrivateMetaData.JobId
            foreach ($AA in $AutomationAccounts) {
                $ThisJob = $AA | Get-AzAutomationJob -id $JobId -ErrorAction SilentlyContinue
                if ($null -ne $ThisJob) {
                    Write-Verbose "Found jobid: $JobId"
                    $RunbookInfo = "Runbook: $($ThisSubscription.Name) / $($ThisJob.ResourceGroupName) / $($ThisJob.AutomationAccountName) / $($ThisJob.RunbookName)"
                    break
                }
            }
            if ($null -ne $ThisJob) {
                break # Break out of Foreach sub loop if we found our job
            }
        }
    }
    elseif (-not $NoRunbookInfo) {
        if ([string]::IsNullOrWhiteSpace($PSCMDLet.MyInvocation.ScriptName)) {
            $Source = "PS> $($PSCMDLet.MyInvocation.Line)"
        }
        else {
            $Source = $PSCMDLet.MyInvocation.ScriptName
        }
        $RunbookInfo = "Script: $((Get-WmiObject -Namespace root\cimv2 -Class Win32_ComputerSystem).Domain)\$($ENV:Computername) / $($ENV:Username) / $($Source)"
    }

        
    # Build auth header
    $UserName = $Credential.UserName
    $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $Credential.GetNetworkCredential().Password)))

    # Set proper headers
    $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $headers.Add('Authorization', ('Basic {0}' -f $base64AuthInfo))
    $headers.Add('Accept', 'application/json')
    $headers.Add('Content-Type', 'application/json')

    # Convert JSON to Hash table, and create variables from each property to make things easier.
    $JSONObject = $JSON | ConvertFrom-Json 
    $JSONHash = @{}
    foreach ($prop in $JSONObject.psobject.properties.name) {
        New-Variable -Name $prop -Value $JSONObject.$Prop 
        $JSONHash.Add($prop, $JSONObject.$Prop)
    }

    # If no caller id (username or email) is specified in JSON, fallback to credentials username
    if (-not $user_name -and -not $email) {
        $user_name = $UserName 
    }

    # If we want to check for existing incident to update
    if ( -not $AlwaysCreateNew) {
        # While the correct caller can be set on new incidents using username/emailaddress, We need the sys_id value to query for existing incident to update
        Write-Verbose "Looking for caller sys_id"
        $Query = @()
        if ($user_name) {
            $Query += "name=$user_name"
        }
        if ($email) {
            $Query += "email=$email"
        }
        $Query = "GOTO$($Query -join "^OR")".Replace("=", "%3D").Replace(" ", "%20").Replace(",", "%2C").Replace("^", "%5E") # ^ = AND, ^OR = OR
        $URL = "https://$SNOWInstance.service-now.com/api/now/table/sys_user?sysparm_query=$Query&sysparm_limit=1"
        $User = (Invoke-RestMethod -Uri $URL -Method get -Headers $headers).result[0]
        if ($null -eq $user) {
            Write-Warning ("No user found matching '$user_name' $(if ($email) {"or '$email'"})")
        }
        else {
            $caller_id = $User.sys_id
        }
    }
    else {
        if ($user_name) {
            $caller_id = "$user_name"
        }
        elseif ($email) {
            $caller_id = "$email"
        }   
    }

    # If we need to update an existing incident but haven't got the incident number, find it based on ShortDescription and, if available, caller
    if ([string]::IsNullOrWhiteSpace($IncidentNumber) -and (-not $AlwaysCreateNew)) {
        $IncidentQuery = "stateIN1,2,3^short_description=$($short_description)" # State 1,2,3 = New, In Progress, On Hold
        
        if ($caller_id) {
            $IncidentQuery += "^caller_id=$($caller_id)"
        }
        
        $IncidentQuery = $IncidentQuery.Replace("=", "%3D").Replace(" ", "%20").Replace(",", "%2C").Replace("^", "%5E")
        $URL = "https://$SNOWInstance.service-now.com/api/now/table/incident?sysparm_query=$IncidentQuery"

        $Incidents = (Invoke-RestMethod -Uri $URL -Method get -Headers $headers).Result | Select-Object *, @{N = "CreatedOn"; E = { Get-Date $_.sys_created_on } } | Sort-Object -Property  CreatedOn -Descending
        if ($null -ne $Incidents) {
            if ($UpdateNewest) {
                $Incident = $Incidents[0]
            }
            else {
                $Incident = $Incidents[-1]
            }
        }
    }
    elseif (-not [String]::IsNullOrWhiteSpace($IncidentNumber)) {
        $URL = "https://$SNOWInstance.service-now.com/api/now/table/incident?sysparm_query=number=$IncidentNumber"
        $Incident = (Invoke-RestMethod -Uri $URL -Method get -Headers $headers).Result 

    }

    
    if ($Incident) { 
        #Update existing incident
        if ($AddTimestampToComments) {
            $comments = @("Updated: $(Get-Date -Format "dd. MMM yyyy HH:mm:ss")"
                $JSONHash["comments"]
            ) 
        }
        else {
            $comments = @($JSONHash["comments"])
        }
        if (-not ($incident.description -eq $JSONHash["description"])) {
            $comments += @(
                ""
                "Previous description:"
                $Incident.description
            ) 
        }

        if ($AppendDescription) {
            $JSONHash["description"] = $Incident.description + [environment]::newLine + $JSONHash["description"]
        }

        $comments = $comments -join "`r`n"
        $JSONHash["comments"] = $comments
        $URL = "https://$SNOWInstance.service-now.com/api/now/table/incident/$($Incident.sys_id)"
        $Result = Invoke-RestMethod -Uri $URL -Method put -Headers $headers -Body ($JSONHash | ConvertTo-Json)
    }
    else {
        #Create new incident
        Write-Verbose "Creating new incident"
        if (-not $caller_id) {
            $caller_id = $Credential.UserName
        }
        
        # Add runtime info to make it easier to find source of tickets
        if ($null -ne $RunbookInfo) {
            if ($null -eq $JSONHash["work_notes"]) {
                $JSONHash.add("work_notes", $RunbookInfo)
            }
            else {
                $JSONHash["work_notes"] += ([environment]::NewLine + $RunbookInfo)
            }
        }
        $JSONHash["caller_id"] = $caller_id
        $URL = "https://$SNOWInstance.service-now.com/api/now/table/incident"
        $Result = Invoke-RestMethod -Uri $URL -Method post -Headers $headers -Body ($JSONHash | ConvertTo-Json)
    }

    foreach ($File in $Attachment) {
        $FileType = $File.split(".")[-1]
        # Not really sure how important this is. JPG seems perfectly functional even if uploaded as zip, but upload fails if no content-type is specified.
        # Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
        # Not all content types are supported, depending on Service-Now instance configuration.
        if (-not $ContentType) {
            switch ($FileType) {
                "3g2" { $ContentType = "video/3gpp2" }
                "3gp" { $ContentType = "video/3gpp" }
                "7z" { $ContentType = "application/x-7z-compressed" }
                "aac" { $ContentType = "audio/aac" }
                "abw" { $ContentType = "application/x-abiword" }
                "arc" { $ContentType = "application/x-freearc" }
                "avi" { $ContentType = "video/x-msvideo" }
                "avif" { $ContentType = "image/avif" }
                "azw" { $ContentType = "application/vnd.amazon.ebook" }
                "bin" { $ContentType = "application/octet-stream" }
                "bmp" { $ContentType = "image/bmp" }
                "bz" { $ContentType = "application/x-bzip" }
                "bz2" { $ContentType = "application/x-bzip2" }
                "cda" { $ContentType = "application/x-cdf" }
                "csh" { $ContentType = "application/x-csh" }
                "css" { $ContentType = "text/css" }
                "csv" { $ContentType = "text/csv" }
                "doc" { $ContentType = "application/msword" }
                "docx" { $ContentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }
                "eot" { $ContentType = "application/vnd.ms-fontobject" }
                "epub" { $ContentType = "application/epub+zip" }
                "gif" { $ContentType = "image/gif" }
                "gz" { $ContentType = "application/gzip" }
                "htm" { $ContentType = "text/html" }
                "ico" { $ContentType = "image/vnd.microsoft.icon" }
                "ics" { $ContentType = "text/calendar" }
                "jar" { $ContentType = "application/java-archive" }
                "jpeg" { $ContentType = "image/jpeg" }
                "js" { $ContentType = "text/javascript)" }
                "json" { $ContentType = "application/json" }
                "jsonld" { $ContentType = "application/ld+json" }
                "mid" { $ContentType = "audio/x-midi" }
                "mjs" { $ContentType = "text/javascript" }
                "mp3" { $ContentType = "audio/mpeg" }
                "mp4" { $ContentType = "video/mp4" }
                "mpeg" { $ContentType = "video/mpeg" }
                "mpkg" { $ContentType = "application/vnd.apple.installer+xml" }
                "odp" { $ContentType = "application/vnd.oasis.opendocument.presentation" }
                "ods" { $ContentType = "application/vnd.oasis.opendocument.spreadsheet" }
                "odt" { $ContentType = "application/vnd.oasis.opendocument.text" }
                "oga" { $ContentType = "audio/ogg" }
                "ogv" { $ContentType = "video/ogg" }
                "ogx" { $ContentType = "application/ogg" }
                "opus" { $ContentType = "audio/opus" }
                "otf" { $ContentType = "font/otf" }
                "pdf" { $ContentType = "application/pdf" }
                "php" { $ContentType = "application/x-httpd-php" }
                "png" { $ContentType = "image/png" }
                "ppt" { $ContentType = "application/vnd.ms-powerpoint" }
                "pptx" { $ContentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation" }
                "rar" { $ContentType = "application/vnd.rar" }
                "rtf" { $ContentType = "application/rtf" }
                "sh" { $ContentType = "application/x-sh" }
                "svg" { $ContentType = "image/svg+xml" }
                "swf" { $ContentType = "application/x-shockwave-flash" }
                "tar" { $ContentType = "application/x-tar" }
                "tif" { $ContentType = "image/tiff" }
                "ts" { $ContentType = "video/mp2t" }
                "ttf" { $ContentType = "font/ttf" }
                "txt" { $ContentType = "text/plain" }
                "vsd" { $ContentType = "application/vnd.visio" }
                "wav" { $ContentType = "audio/wav" }
                "weba" { $ContentType = "audio/webm" }
                "webm" { $ContentType = "video/webm" }
                "webp" { $ContentType = "image/webp" }
                "woff" { $ContentType = "font/woff" }
                "woff2" { $ContentType = "font/woff2" }
                "xhtml" { $ContentType = "application/xhtml+xml" }
                "xls" { $ContentType = "application/vnd.ms-excel" }
                "xlsx" { $ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }
                "xml" { $ContentType = "application/xml" }
                "xul" { $ContentType = "application/vnd.mozilla.xul+xml" }
                "zip" { $ContentType = "application/zip" }
                default { $ContentType = "text/plain" }
            }
        }
        $headers["Content-Type"] = $ContentType
        Write-Verbose "Uploading '$File' using Content-Type '$ContentType'"
        $URL = "https://$SNOWInstance.service-now.com/api/now/attachment/file?table_name=incident&table_sys_id=$($Result.result.sys_id)&file_name=$(Split-Path -Path $File -Leaf)"
        $null = Invoke-RestMethod -Method Post -Uri $URL -Headers $headers -InFile $File 
    }
 
    return $Result
}
# Add default functionality to user profile
$ProfilePath    = $profile.CurrentUserAllHosts
$ProfileContent = @(Get-Content -Path $ProfilePath -ErrorAction SilentlyContinue)
$AddToProfile   = @('# Added by module: roe.Misc ')

if (-not ($ProfileContent-match "^(\s+)?Import-Module -Name roe.Misc *")) {
    Write-Host "Module roe.Misc : First import - Adding Import-Module roe.Misc to $profilepath"
    $AddToProfile += @(
        "Import-Module -Name roe.Misc -Verbose 4>&1 | Where-Object {`$_ -notmatch 'Exporting'}"
    ) 
}

if (-not ($ProfileContent-match "^(\s+)?Prompt*")) {
    Write-Host "Module roe.Misc : First import - Adding Prompt to $profilepath"
    $AddToProfile += @(
        "Prompt"
    )
}

if (-not ($ProfileContent -match "^(\s+)?Set-SystemVariables*")) {
    Write-Host "Module roe.Misc : First import - Adding Set-SystemVariables to $profilepath"
    $AddToProfile += @(
        "Set-SystemVariables"
    ) 
}

if ($AddToProfile.count -gt 1) {
    if (-not (Test-Path -Path $ProfilePath -ErrorAction SilentlyContinue)) {
        $null = New-Item -Path $ProfilePath -Force -ItemType File
    }
    $ProfileContent += $AddToProfile
    $ProfileContent | Set-Content -Path $ProfilePath -Force
}