AVDManagementFramework.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\AVDManagementFramework.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName AVDManagementFramework.Import.DoDotSource -Fallback $false
if ($AVDManagementFramework_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName AVDManagementFramework.Import.IndividualFiles -Fallback $false
if ($AVDManagementFramework_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
 
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
 
            This provides a central location to react to files being imported, if later desired
 
        .PARAMETER Path
            The path to the file to load
 
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
 
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )

    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }

    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }

    # Execute Postimport actions
    foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) {
        . Import-ModuleFile -Path $path
    }

    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'AVDManagementFramework' -Language 'en-US'

function New-AVDMFResourceName {
    <#
    .SYNOPSIS
        This function generates resource names as per the naming convention.
    .DESCRIPTION
        The function reads naming conventions and abbreviations from configuration files and outputs resource names to use.
    .EXAMPLE
        TODO: Add Examples
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    param (
        [Parameter()]
        [string] $ResourceType,
        [string] $DeploymentStage = $script:DeploymentStage,
        [string] $ResourceCategory, #This is part of the ABV category
        [string] $NameSuffix,
        [string] $AccessLevel, # Enterprise, Specialist, Privileged
        [string] $HostPoolType, # Shared, Dedicated
        [string] $Location = $script:Location,

        [string] $HostPoolInstance,

        [string] $ParentName,
        [string] $AddressPrefix,

        [Int] $InstanceNumber,
        [string] $UniqueNameString # For resources that require global name uniqueness (Storage Accounts / FunctionApps)

        #TODO: Change parameters to overloads so we don't have to provide them. (Except deployment stage?)
    )
    Write-PSFMessage -Level Debug -Message "Calculating a name for a resource of type {0}" -StringValues $ResourceType

    # Selecting a naming style
    $namingStyle = $script:NamingStyles | Where-Object { $_.ResourceType -eq $ResourceType }
    if (-not $namingStyle) {
        $namingStyle = $script:NamingStyles | Where-Object { $_.ResourceType -eq 'Default' }
    }
    Write-PSFMessage -Level Debug -Message "Resource type '{0}' is configured to use '{1}' naming style" -StringValues $ResourceType, $namingStyle.ResourceType

    [array] $nameArray = foreach ($component in $namingStyle.NameComponents) {
        if ($component -like "*Abv") {
            $componentName = $component -replace "Abv", ""
            $componentNC = $component -replace "Abv", "NC"

            #Check if component naming convention is available
            # TODO: Use hashtable for naming conventions instead of dynamic variable names!
            # Assumption - hashtable stored in $script:namingConvention
            try { Get-Variable -Name $componentNC -ErrorAction Stop | Out-Null }
            catch { throw "Could not find a naming convention for component: $componentName. It should be supplied in configuration as .\NamingConvention\Components\$($componentName).json" }

            # Default or custom abbreviation
            $componentNCmembers = (Get-Variable -Name $componentNC).value | Get-Member -MemberType NoteProperty | Where-Object Name -NE $componentName
            $abbreviationMarker = ($componentNCmembers | Where-Object Name -EQ ("{0}Abv" -f $ResourceType)).Name
            if (-Not $abbreviationMarker) { $abbreviationMarker = "Abbreviation" }

            if ($componentName -eq 'Subscription') {

                $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value
                $filterScript = [ScriptBlock]::Create("`$_.DeploymentStage -eq `$DeploymentStage")
            }
            elseif ($componentName -eq 'Location') {

                $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value
                $filterScript = [ScriptBlock]::Create("`$_.Location -eq `$Script:Location")
            }
            else {
                $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value
                $filterScript = [ScriptBlock]::Create("`$_.$componentName -eq `$$componentName")

            }
            #FRED: $script:namingConvention[$componentName].$abbreviationMarker
            $abv = ($namingConvention | Where-Object -FilterScript $filterScript).$abbreviationMarker
            if (-not $abv) {
                $errorMessage = "Resource Type: {0} - Naming Style: {1} - Could not find abbreviation for $componentName`: $((Get-Variable -Name $componentName).Value)" -f $ResourceType,$namingStyle.ResourceType
                Write-PSFMessage -Level Error -Message $errorMessage
                throw $errorMessage
            }
            $abv
        }

        if ($component -like "static_*") { $component -replace "static_", "" }

        if ($component -in ('-', '_')) { $component }

        if ($component -eq 'ParentName') {
            $ParentName
        }

        if ($component -eq 'NameSuffix') {
            $NameSuffix
        }

        if ($component -eq 'AddressPrefix') {
            $AddressPrefix -replace "/", "-"
        }
        if ($component -eq 'HostPoolInstance') {
            $HostPoolInstance
        }
    }
    $resourceName = $nameArray -join "" -replace "-All", "" -replace "All", ""
    if ($namingStyle.LowerCase) { $resourceName = $resourceName.ToLower() }

    if ($namingStyle.NameComponents -contains 'InstanceNumber') {
        #TODO: Move this part to the main loop.
        if ($InstanceNumber) {
            $resourceName = "{0}{1:D2}" -f $resourceName, $InstanceNumber
        }
        else {
            $scriptResourceType = (Get-Variable -Name "$($ResourceType)s" -Scope Script).Value
            $filterScript = [ScriptBlock]::Create("`$_ -like `"$resourceName*`"")
            $count = ($scriptResourceType.Keys | Where-Object -FilterScript $filterScript).Count

            #TODO: Fix this once we have resource name attribute for all resource
            if ($count -eq 0) {
                $count = ($scriptResourceType.GetEnumerator() | ForEach-Object { $_.Value.ResourceName } | Where-Object -FilterScript $filterScript).count
            }

            $resourceName = "{0}{1:D2}" -f $resourceName, ($Count + 1)
        }
    }

    if ($namingStyle.NameComponents -contains 'UniqueNameString') {
        $resourceName = "{0}{1}" -f $resourceName, $UniqueNameString
    }
    if ($namingStyle.NameComponents -contains 'FillUnique') {
        $subscriptionIdNoDash = $script:AzSubscriptionId -replace "-", ""
        $resourceName = "{0}x{1}" -f $resourceName, $subscriptionIdNoDash.substring(0, ($namingStyle.MaxLength - $resourceName.length - 1))
    }


    if ($resourceName.length -gt $namingStyle.MaxLength) { throw "Resulting resource name is longer than $($namingStyle.MaxLength) characters '$resourceName'" }

    $resourceName
}


function Convert-HashtableToArray {
    [OutputType('System.Array')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline=$true )]
        [Hashtable] $InputObject
    )
    process {
        $output = foreach ($key in $InputObject.Keys){
            $object = @{
                Name = $key
            }
            $Members = $InputObject[$key] | Get-Member -MemberType NoteProperty # TODO: $hash['h104p01-vnet-pv-01'].psobject.Properties.name
            $Members | ForEach-Object {$object[$_.Name] = $InputObject[$key].($_.Name)}

            $object
        }

        ,$output # "The comma makes it output an array ALWAYS, that's it" -Fred!
    }
}

function Get-AVDMFResourceInfo {
    [CmdletBinding()]
    param (
        [string] $ResourceId
    )
    $pattern = '^\/subscriptions\/(?<SubscriptionId>.+)\/resourceGroups\/(?<ResourceGroupName>.+)\/providers.+\/(?<ResourceName>.+$)'
    if($ResourceId -match $pattern){
        [PSCustomObject]@{
            SubscriptionId = $Matches.SubscriptionId
            ResourceGroupName = $Matches.ResourceGroupName
            ResourceName = $Matches.ResourceName
        }
    }
    else {throw "Resource ID is not valid: $ResourceId"}
}


function Get-RandomPassword {
    #Link: https://gist.github.com/onlyann/00d9bb09d4b1338ffc88a213509a6caf

    param(
        [Parameter(Mandatory = $false)]
        [ValidateRange(12, 256)]
        [int]
        $length = 14
    )
    $symbols = '!@#$%^&*'.ToCharArray()
    $characterList = 'a'..'z' + 'A'..'Z' + '0'..'9' + $symbols
    do {
        $password = ""
        for ($i = 0; $i -lt $length; $i++) {
            $randomIndex = [System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $characterList.Length)
            $password += $characterList[$randomIndex]
        }

        [int]$hasLowerChar = $password -cmatch '[a-z]'
        [int]$hasUpperChar = $password -cmatch '[A-Z]'
        [int]$hasDigit = $password -match '[0-9]'
        [int]$hasSymbol = $password.IndexOfAny($symbols) -ne -1

    }
    until (($hasLowerChar + $hasUpperChar + $hasDigit + $hasSymbol) -ge 3)

    $password #| ConvertTo-SecureString -AsPlainText
}

function New-AVDMFSubnetRange {
    [cmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")]
    param(

        # Address Space for the subnet, this can be any of the address spaces created under the vNet in the format X.X.X.X/X
        [Parameter(Mandatory = $true)][string]$AddressSpace,

        # Mask bits of the new subnet, written as XX (example 27)
        [Parameter(Mandatory = $true)][int]$NewSubnetMaskBits

    )
    #region: functions
    function ConvertFrom-DecimalIPtoBinary ([string]$DecimalIPAddress) {
        #Create an empty variable
        $Binary = $null

        #Extract octets from IP Address
        $Octets = $DecimalIPAddress.Split('.')

        #Convert each octet to Binary and add to the variable $Binary
        # Here we use ToString with '2' as the base, 2 means binary
        # We are also using padleft to make sure each octet is 8 bits long with leading zeros if needed
        $Octets | ForEach-Object { $Binary += ([convert]::ToString($_, 2)).PadLeft(8, "0") }

        return $Binary
    }
    function ConvertFrom-BinaryIPtoDecimal ([string]$BinaryIPAddress) {
        #Create an empty string
        $Decimal = $null

        #Split Binary address into 4 octets - And convert to decimal
        # Again, 2 is for the base.
        $Octets = for ($i = 0; $i -lt 4; $i++) {
            [convert]::ToInt32($BinaryIPAddress.Substring($i * 8, 8), 2)
        }

        # Join the octets into one string with "." as delimeter
        $Decimal = $Octets -join "."

        return $Decimal
    }
    function Find-IPAddressesInRange ($FirstIPAddress, $LastIPAddress) {
        # First we confirm the IP Addresses to Binary, then to int64
        $Int64IP1 = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $FirstIPAddress), 2)
        $Int64IP2 = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $LastIPAddress), 2)

        # Then we just create a loop of all the values in the range of int64
        $IPAddresses = for ($i = $Int64IP1; $i -le $Int64IP2; $i++) {
            #Finally, we convert the int64 to binary then back to a decimal IP Address
            ConvertFrom-BinaryIPtoDecimal ([convert]::ToString($i, 2)).padleft(32, "0")
        }
        return $IPAddresses
    }
    function Get-SubnetDetails ($IPAddress, $MaskBits) {
        $BinaryIPAddress = ConvertFrom-DecimalIPtoBinary $IPAddress
        $SubnetID = ConvertFrom-BinaryIPtoDecimal $BinaryIPAddress.Substring(0, $MaskBits).PadRight(32, '0')
        $BroadcastIP = ConvertFrom-BinaryIPtoDecimal $BinaryIPAddress.Substring(0, $MaskBits).PadRight(32, '1')
        return [PSCustomObject]@{
            SubnetID      = $SubnetID
            BroadcastIP   = $BroadcastIP
            AddressPrefix = "$SubnetID/$MaskBits"
        }
    }
    function Test-OverlappingSubnets ($SubnetA, $SubnetB) {
        $SubnetAIDDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetA.SubnetID), 2)
        $SubnetABCDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetA.BroadcastIP), 2)

        $SubnetBIDDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetB.SubnetID), 2)
        $SubnetBBCDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetB.BroadcastIP), 2)

        if ($SubnetAIDDigital -ge $SubnetBIDDigital -and $SubnetAIDDigital -le $SubnetBBCDigital) { $Overlap = $true }
        elseif ($SubnetABCDigital -ge $SubnetBIDDigital -and $SubnetABCDigital -le $SubnetBBCDigital) { $Overlap = $true }
        else { $Overlap = $false }

        return $Overlap
    }
    #endregion: functions


    #region: Analyzing Address Space
    Write-Verbose -Message "Analyzing Address Space"
    #Find the position of '/' in the provided address space.
    $AddressSpaceIndexOfMaskBits = $AddressSpace.IndexOf("/")
    $AddressSpaceID = $AddressSpace.Substring(0, $AddressSpaceIndexOfMaskBits)
    $AddressSpaceMaskBits = $AddressSpace.Substring($AddressSpaceIndexOfMaskBits + 1)

    Write-Verbose -Message "ID: $AddressSpaceID - MaskbBits: $AddressSpaceMaskBits"


    $AddressSpaceSize = [math]::Pow(2, 32 - $AddressSpaceMaskBits)
    Write-Verbose -Message "Address Space Size: $AddressSpaceSize"


    Write-Verbose -Message "Finished Analyzing Address Space"
    #endregion: Analyzing Address Space

    #region: Find all possible subnets
    Write-Verbose -Message "ENTER: Find all possible subnets"

    $NewSubnetSize = [math]::Pow(2, (32 - $NewSubnetMaskBits))
    Write-Verbose -Message "New Subnet Size: $NewSubnetSize"

    $NumberOfPossibleSubnets = $AddressSpaceSize / $NewSubnetSize
    Write-Verbose -Message "Number of Possible Subnets: $NumberOfPossibleSubnets"

    $PossibleSubnetsArray = @(Get-SubnetDetails $AddressSpaceID $NewSubnetMaskBits)
    for ($i = 1; $i -lt $NumberOfPossibleSubnets; $i++) {
        $LastSubnetInt64 = ([convert]::Toint64((ConvertFrom-DecimalIPtoBinary $PossibleSubnetsArray[$i - 1].BroadcastIP), 2))
        $NextSubnetID = ConvertFrom-BinaryIPtoDecimal ([convert]::ToString($LastSubnetInt64 + 1, 2)).padleft(32, '0')

        $PossibleSubnetsArray += Get-SubnetDetails $NextSubnetID $NewSubnetMaskBits
    }

    Write-Verbose -Message "Calculated $($PossibleSubnetsArray.Count) possible subnets."

    Write-Verbose -Message "Exit: Find all possible subnets"
    #endregion: Find all possible subnets

    #region: Collect vNet information
    Write-Verbose -Message "ENTER: Collect vNet information"

    $vNetSubnets = foreach ($key in $script:Subnets.Keys) {
        if ((ConvertFrom-DecimalIPtoBinary ($script:Subnets[$key].Properties.AddressPrefix.Substring(0, $script:Subnets[$key].Properties.AddressPrefix.IndexOf("/")))).Substring(0, $AddressSpaceMaskBits) `
                -eq
            (ConvertFrom-DecimalIPtoBinary ($AddressSpaceID)).Substring(0, $AddressSpaceMaskBits)) {
            $script:Subnets[$key]
        }
    }

    Write-Verbose -Message "Found $($vNetSubnets.count) subnets in the vNet belonging to the address space $AddressSpace"

    $UtilizedAddressesArray = foreach ($Subnet in $vNetSubnets) {
        $IndexOfSubnetMask = $Subnet.Properties.AddressPrefix.indexOf("/")
        $SubnetID = $Subnet.Properties.AddressPrefix.Substring(0, $IndexOfSubnetMask)
        $MaskBits = $Subnet.Properties.AddressPrefix.Substring($IndexOfSubnetMask + 1)
        Get-SubnetDetails -IPAddress $SubnetID -MaskBits $MaskBits
    }


    Write-Verbose -Message "Calculated utilized addresses"

    Write-Verbose -Message "Exit: Collect vNet information"
    #endregion: Collect vNet information

    #region: Return the first free subnet
    Write-Verbose -Message "ENTER: Return the first free subnet"

    foreach ($PossibleSubnet in $PossibleSubnetsArray) {
        foreach ($ExistingSubnet in $UtilizedAddressesArray) {
            $Overlap = $false
            if (Test-OverlappingSubnets $PossibleSubnet $ExistingSubnet) {
                $Overlap = $true
                break
            }
        }
        if (!($Overlap)) { return $PossibleSubnet }
    }

    Write-Verbose -Message "Exit: Return the first free subnet"
    #endregion: Return the first free subnet

    # if we did not return any subnet, throw an error
    throw "Could not find any free subnets"

}

function Set-AVDMFNameMapping {
    <#
    .SYNOPSIS
        Takes a dataset and converts any %XXXX% into mapping.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Hashtable] $Dataset
    )

    foreach ($item in ($dataset.GetEnumerator() | Where-Object { $null -ne $_.Value } )){
        if ($item.Value.GetType().Name -eq 'String'){
            $stringMappings = ([regex]::Matches($item.Value, '%.+?%')).Value | ForEach-Object { if ($_) { $_ -replace "%", "" } }
            foreach ($mapping in $stringMappings) {
                $mappedValue = $script:NameMappings[$mapping]
                if($null -ne $mappedValue ){
                    $item.Value = $item.Value -replace "%$mapping%", $mappedValue
                }
            }
            $dataset[$item.Key] = $item.Value
        }
        if ($item.Value.GetType().Name -eq 'PSCustomObject') {
            $dataset[$item.Key] =[PSCustomObject] (Set-AVDMFNameMapping -Dataset ($item.Value | ConvertTo-PSFHashtable))
        }
        if ($item.Value.GetType().Name -eq 'Object[]') {
            for($i=0;$i -lt $item.Value.Count;$i++){
                if($item.Value[$i].GetType().Name -eq 'String'){
                    $stringMappings = ([regex]::Matches($item.Value[$i], '%.+?%')).Value | ForEach-Object { if ($_) { $_ -replace "%", "" } }
                    foreach ($mapping in $stringMappings) {
                        $mappedValue = $script:NameMappings[$mapping]
                        $item.Value[$i] = $item.Value[$i] -replace "%$mapping%", $mappedValue
                    }
                }
                if($item.Value[$i].GetType().Name -eq  'PSCustomObject' ){
                    $item.Value[$i] = [PSCustomObject] (Set-AVDMFNameMapping -Dataset ($item.Value[$i] | ConvertTo-PSFHashtable))
                }
            }
        }
    }
    $Dataset
}

function Set-AVDMFStageEntries {
    <#
    .SYNOPSIS
        This function replaces "Stages" token in json objects depending on the current stage or a default one.
    .Example
        $json = @"
        {
            "SampleProperty": {
                "DeploymentStage": {
                    "Development": 10,
                    "Production": 5,
                    "Default": 15
                }
            }
        }
        "@
        $dataset = $json | ConvertFrom-Json | ConvertTo-PSFHashtable
        Set-AVDMFStageEntries -Dataset $dataset
 
        Assuming the current stage name is "Development", the output will be that SampleProperty = 10
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [Hashtable] $Dataset,

        [string] $DeploymentStage = $script:DeploymentStage,
        [string] $StageToken = "DeploymentStage"
    )
    foreach ($key in ([array]$Dataset.Keys)) {

        if ($null -eq $Dataset[$key]) { continue }

        if ($Dataset[$key].GetType().Name -eq 'PSCustomObject') {
            if ($Dataset[$key] | Get-Member -MemberType NoteProperty -Name $StageToken) {
                # Get list of configured stages under the stage token
                $configuredStages = ($Dataset[$key].$StageToken | Get-Member -MemberType NoteProperty).Name

                if ( $configuredStages -contains $DeploymentStage) {
                    $Dataset[$key] = $Dataset[$key].$StageToken.($DeploymentStage)
                    Write-PSFMessage -Level Verbose -Message "Set $key to $DeploymentStage value: $($Dataset[$key])"
                }
                elseif ( $configuredStages -contains "Default" ) {
                    $Dataset[$key] = $Dataset[$key].$StageToken.Default
                    Write-PSFMessage -Level Verbose -Message "Set $key to Default value: $($Dataset[$key])"
                }
                else {
                    throw "Could not resolve stage value ($DeploymentStage) for `r`n $($Dataset | Out-String)"
                }
            }
            else {
                # key is a PSCustomObject that does not have a stage token, maybe one of its children.
                $Dataset[$key] = [PSCustomObject] (Set-AVDMFStageEntries -Dataset ($Dataset[$key] | ConvertTo-PSFHashtable))
            }
        }
        if ($Dataset[$key].GetType().Name -eq 'Object[]') {
            # Key is an array of objects (example, SessionHosts > RemoteAppGroups)
            $Dataset[$key] = $Dataset[$key] | ForEach-Object {
                if ($_.GetType().Name -ne 'string') {
                    [PSCustomObject](Set-AVDMFStageEntries -Dataset ( $_ | ConvertTo-PSFHashtable) )
                }
                else {
                    $_
                }
            }
        }
    }
    $Dataset
}


function Invoke-AVDMFConfiguration {
    [CmdletBinding()]
    param (

    )

    if($script:Offline){
        throw "Cannot deploy when working offline. Please reload configuration without the offline switch."
    }

    # Create resource groups
    Write-PSFMessage -Level Host -Message "Invoking resource groups."
    foreach ($rg in $script:ResourceGroups.Keys) {
        $newAzResourceGroup = @{
            Name = $rg
            Location = $script:Location
            Force = $true
        }
        if($script:ResourceGroups[$rg].Tags){
            $newAzResourceGroup['Tags'] = $script:ResourceGroups[$rg].Tags
        }
        $null = New-AzResourceGroup @newAzResourceGroup
    }
    #TODO: Decide if we want to create RGs here or with deployment. decide on parallelism

    # Create network resources
    Write-PSFMessage -Level Host -Message "Invoking network resources."
    Invoke-AVDMFNetwork -ErrorAction Stop

    #Create storage resources
    Write-PSFMessage -Level Host -Message "Invoking Storage resources."
    Invoke-AVDMFStorage -ErrorAction Stop

    # Create Host Pools and Session Hosts
    Write-PSFMessage -Level Host -Message "Invoking Desktop Virtualization resources."
    Invoke-AVDMFDesktopVirtualization -ErrorAction Stop
}

function New-AVDMFConfiguration {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string] $Path = (Get-Location).Path,

        [switch] $Quiet
    )

    $modulePath = Split-Path -Path $MyInvocation.MyCommand.Module.Path
    $zipPath = Join-Path -Path $modulePath -ChildPath 'SampleConfiguration.zip'
    if(Test-Path -Path (Join-Path -Path $Path -ChildPath 'AVDMFConfiguration') -PathType Container){
        Stop-PSFFunction -Message "AVDMFConfiguration folder already exists. Please provide a different path." -EnableException $true -Category InvalidOperation
    }
    else{
        Expand-Archive -Path $zipPath -DestinationPath $Path -ErrorAction Stop
    }

    if(-Not $Quiet){
        $setPath = Join-PSFPath -Path $Path -Child 'AVDMFConfiguration','ConfigurationFiles'
        $newConfigurationWelcomeText = @"
Welcome To AVD Management Framework.
 
You just created a new configuration. The first step is to review the configuration and add users in the Host Pools.
 
You can deploy the configuration by running Set-AVDMFConfiguration -ConfigurationPath '$setPath' then Invoke-AVDMFConfiguration to create the resources in Azure.
 
Please make sure you connect to Azure using Add-AzAccount and Set-AzContext to your target subscription.
 
For more information please review the documentation. Happy AVD :)
"@

        Write-Host $newConfigurationWelcomeText -ForegroundColor Cyan
    }
}


function Set-AVDMFConfiguration {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ConfigurationPath,

        [string] $AzSubscriptionId = (Get-AzContext).Subscription.Id,
        [switch] $Force,

        # Use this parameter for testing offline to avoid Id resolutions.
        [switch] $Offline

    )

    #region: Initialize Variables
    $configurationVersion = '1.1.1'
    #endregion: Initialize Variables

    #region: Load Custom Environment Variables
    $environmentVariablesFilePath = Join-Path -Path $ConfigurationPath -ChildPath 'EnvironmentVariables.jsonc'
    if (Test-Path -Path $environmentVariablesFilePath) {
        Write-PSFMessage -Level Warning -Message "EnvironmentVariables.json file detected. This is not supposed to exist on DevOps. Please add it to .gitignore"

        $environmentVariables = Get-Content -Path $environmentVariablesFilePath | ConvertFrom-Json | ConvertTo-PSFHashtable
        $null = $environmentVariables.GetEnumerator() | ForEach-Object { New-Item -Path $_.Key -Value $_.Value -Force }
    }
    #endregion: Load Custom Environment Variables
    #region: Set DeploymentStage
    $script:DeploymentStage = $env:SYSTEM_STAGEDISPLAYNAME
    if ([string]::IsNullOrEmpty($DeploymentStage) -or [string]::IsNullOrWhiteSpace($DeploymentStage)) {
        throw "Deployment Stage is not defined, if running from local device create EnvironmentVariables.json file. Otherwise review environment variables."
        #TODO: Include environment variable name in error message.
    }
    #endregion: Set DeploymentStage

    #region: Offline Switch
    if($Offline){
        $script:Offline = $true
        Write-PSFMessage -Level "Warning" -Message "Working offline. Deployment blocked."
    }
    #endregion: Offline Switch

    #region: Register Name Mappings

    $nameMappingConfigPath = Join-Path -Path $ConfigurationPath -ChildPath "NameMappings"
    if (Test-Path $nameMappingConfigPath) {
        foreach ($file in Get-ChildItem -Path $nameMappingConfigPath -Filter "*.json*") {
            foreach ($dataset in (Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable )) {
                Register-AVDMFNameMapping @dataset
            }
        }
    }
    #endregion: Register Name Mappings

    #region: Populate Script Variables
    $script:AzSubscriptionId = $AzSubscriptionId
    $script:ConfigurationPath = $ConfigurationPath

    #endregion: Populate Script Variables


    if ($script:WVDConfigurationLoaded -and -not $Force) { throw "Configuration is already loaded. Use the force to reload." }
    if ($Force) { & "$moduleRoot\internal\scripts\variables.ps1" }

    #region: General Configuration
    $generalConfiguration = Get-Content -Path (Join-Path -Path $ConfigurationPath -ChildPath '\GeneralConfiguration\GeneralConfiguration.jsonc' -ErrorAction Stop ) | ConvertFrom-Json -ErrorAction Stop

    if ($generalConfiguration.ConfigurationVersion -ne $configurationVersion) {
        throw "current configuration version $($generalConfiguration.ConfigurationVersion) must match $configurationVersion."
    }
    Write-PSFMessage -Message "Configuration version: {0}" -StringValues $configurationVersion

    $script:Location = $GeneralConfiguration.Location
    $script:TimeZone = $generalConfiguration.TimeZone # TODO: Remove this variable from all files.



    #endregion

    #region: Naming Conventions
    $namingConventionsRoot = Join-Path -Path $ConfigurationPath -ChildPath NamingConventions

    $script:NamingStyles = Get-Content -Path $namingConventionsRoot\NamingStyles.jsonc -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop

    $namingConventionsComponentsRoot = Join-Path -Path $namingConventionsRoot -ChildPath "Components"

    foreach ($componentNC in (Get-ChildItem -Path $namingConventionsComponentsRoot -Filter "*.json*")) {
        # We create a script variable for each component by adding 'NC' to the name of the file

        $NCContent = Get-Content -Path $componentNC.FullName -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        New-Variable -Scope 'script' -Name ("{0}NC" -f $componentNC.BaseName) -Value $NCContent
    }

    #endregion: Naming Conventions

    #region: Define Registrable Components
    $components = [ordered] @{
        # Tags
        'GlobalTags'                   = @{Command = (Get-Command -Name Register-AVDMFGlobalTag); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "GlobalTags") }
        # Network
        'AddressSpaces'                = @{Command = (Get-Command Register-AVDMFAddressSpace); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\AddressSpaces") }
        'RouteTables'                  = @{Command = (Get-Command Register-AVDMFRouteTable); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\RouteTables") }
        'NetworkSecurityGroups'        = @{Command = (Get-Command Register-AVDMFNetworkSecurityGroup); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\NetworkSecurityGroups") }
        'VirtualNetworks'              = @{Command = (Get-Command Register-AVDMFVirtualNetwork); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\VirtualNetworks") }
        # Storage
        'StorageAccounts'              = @{Command = (Get-Command Register-AVDMFStorageAccount); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Storage\StorageAccounts") }
        # Desktop Virtualization
        'Workspaces'                   = @{Command = (Get-Command Register-AVDMFWorkspace); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\Workspaces") }
        'VMTemplates'                  = @{Command = (Get-Command Register-AVDMFVMTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\VMTemplates") }
        'RemoteAppTemplates'           = @{Command = (Get-Command Register-AVDMFRemoteAppTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\RemoteAppTemplates") }
        'ReplacementPlanTemplates'     = @{Command = (Get-Command Register-AVDMFReplacementPlanTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\ReplacementPlanTemplates") }
        'ScalingPlanScheduleTemplates' = @{Command = (Get-Command Register-AVDMFScalingPlanScheduleTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\ScalingPlanScheduleTemplates") }
        'ScalingPlanTemplates'         = @{Command = (Get-Command Register-AVDMFScalingPlanTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\ScalingPlanTemplates") }
        'HostPools'                    = @{Command = (Get-Command Register-AVDMFHostPool); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\HostPools") }

    }
    #endregion: Define Registrable Components

    #region: Load Component Configuration
    foreach ($key in $components.Keys) {
        if (-not (Test-Path $components[$key].ConfigurationPath)) { continue }
        Write-PSFMessage -Level Verbose -Message "Loading configuration for $key"

        foreach ($file in Get-ChildItem -Path $components[$key].ConfigurationPath -Recurse -Filter "*.json*") {
            Write-PSFMessage -Level Verbose -Message "`tLoading $key from $($file.FullName)"
            foreach ($dataset in (Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable -Include $($components[$key].Command.Parameters.Keys))) {

                Write-PSFMessage -Level Verbose -Message "`t`tRegistering dataset:`r`n $($dataset | Format-List | Out-String -Width 120)"
                $dataset = Set-AVDMFNameMapping -Dataset $dataset
                $dataset = Set-AVDMFStageEntries -Dataset $dataset
                & $components[$key].Command @dataset -ErrorAction Stop
            }
        }
    }
    #endregion: Load Component Configuration

    #region: Add Tags
    $taggedResources = @(
        'ResourceGroup'
        'VirtualNetwork'
        'NetworkSecurityGroup'
        'RouteTable'
        'StorageAccount'
        'PrivateLink'
        'HostPool'
        'ApplicationGroup'
        'Workspace'
        'SessionHost'
    )
    foreach ($resourceType in $taggedResources) {
        $scriptVariable = Get-Variable -Scope script -Name "$($resourceType)s" -ValueOnly
        if (($script:GlobalTags.keys -contains $resourceType) -or ($script:GlobalTags.keys -contains 'All')) {
            $keys = [array] $scriptVariable.Keys
            foreach ($key in $keys) { $scriptVariable[$key] = Add-AVDMFTag -ResourceType $resourceType -ResourceObject $scriptVariable[$key] }
        }
    }

    #endregion: Add Tags

    $script:WVDConfigurationLoaded = $true
}

function Initialize-AVDMFDesktopVirtualization {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    param (
        [string] $ResourceGroupName,
        [string] $ResourceCategory
    )
    switch ($ResourceCategory) {
        'HostPool' {
            $filteredHostPools = @{}
            $script:HostPools.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredHostPools.Add($_.Key, $_.Value) }

            $filteredApplicationGroups = @{}
            $script:ApplicationGroups.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredApplicationGroups.Add($_.Key, $_.Value) }

            $filteredRemoteApps = @{}
            $script:RemoteApps.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredRemoteApps.Add($_.Key, $_.Value) }
            if ($null -eq ([array] ($filteredRemoteApps | Convert-HashtableToArray))) {
                $filteredRemoteApps = @()
            }
            else {
                $filteredRemoteApps = [array] ($filteredRemoteApps | Convert-HashtableToArray)
            }

            $filteredReplacementPlans = @{}
            $script:ReplacementPlans.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredReplacementPlans.Add($_.Key, $_.Value) }

            $filteredScalingPlans = @{}
            $script:ScalingPlans.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredScalingPlans.Add($_.Key, $_.Value) }

            $filteredTemplateSpecs = @{}
            $script:TemplateSpecs.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredTemplateSpecs.Add($_.Key, $_.Value) }

            $templateParams = @{
                HostPools         = [array] ($filteredHostPools | Convert-HashtableToArray)
                ApplicationGroups = [array] ($filteredApplicationGroups | Convert-HashtableToArray)
                RemoteApps        = $filteredRemoteApps
                ScalingPlan       = if ($filteredScalingPlans.Keys.Count) { ([array] ($filteredScalingPlans | Convert-HashtableToArray))[0] } else { @{} } #TODO: There can only be one, review the code here.
                ReplacementPlan   = ([array] ($filteredReplacementPlans | Convert-HashtableToArray))[0] #TODO: There can only be one, review the code here.
                TemplateSpec      = ([array] ($filteredTemplateSpecs | Convert-HashtableToArray))[0]  #TODO: There can only be one, review the code here.
                ResourceGroupName = $ResourceGroupName
                Location          = $script:Location
            }
        }
        'Workspace' {
            $filteredWorkspaces = @{}
            $script:Workspaces.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredWorkspaces.Add($_.Key, $_.Value) }
            $templateParams = @{
                Workspaces = [array] ($filteredWorkspaces | Convert-HashtableToArray)
            }
        }
    }
    $templateParams
}

function Invoke-AVDMFDesktopVirtualization {
    [CmdletBinding()]
    param (
        [string] $TimeStamp = (Get-Date -Format 'FileDateTimeUniversal')
    )
    if($script:Offline){
        throw "Cannot deploy when working offline. Please reload configuration without the offline switch."
    }
    #region: Initialize Variables

    $bicepWorkspaces = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\Workspaces.bicep"
    $bicepHostPools = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\HostPools.bicep"
    #endregion: Initialize Variables

    # Host Pools
    $hostPoolJobs = @()
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'HostPool') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'HostPool'

            try {
                $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop
            }
            catch {
                New-AzResourceGroup -Name $rg -Location $script:Location
            }
            $hostPoolJobs += New-AzSubscriptionDeployment -Location $script:Location -Name "AVDMFHostPoolDeployment-$TimeStamp" -TemplateFile $bicepHostPools -TemplateParameterObject $templateParams
            #$hostPoolJobs += New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Incremental -TemplateFile $bicepHostPools @templateParams -ErrorAction Stop -Confirm:$false -Force -AsJob
            #TODO: We switched to incremental for the FunctionApp to take over host deployment. We need to add logic to remove RemoteApps, Application groups, that are no longer in configuration.

            #TODO: See if we can merge this into the main deployment
        }
        $dateTime = Get-Date
    }
    while ($hostPoolJobs.State -contains "Running") {
        Start-Sleep -Seconds 5
        $timeSpan = New-TimeSpan -Start $dateTime -End (Get-Date)
        $count = ($hostPoolJobs | Where-Object { $_.State -eq "Running" }).count
        Write-PSFMessage -Level Host -Message "Waiting for $count hostpool deployments to complete - Been waiting for $($timeSpan.ToString())"
    }
    Write-PSFMessage -Level Host -Message "Hostpool jobs completed."
    #$hostPoolJobs | Receive-Job

    #region: Update SessionDesktop name
    #TODO: Check if there is a put method yet for 'Microsoft.DesktopVirtualization/applicationgroups/desktops'
    foreach ($item in ($script:ApplicationGroups.GetEnumerator() |  Where-Object {$_.Value.ApplicationGroupType -eq 'Desktop'}) ) {
        Write-PSFMessage -Level Host -Message 'Updating SessionDesktop Friendly Name'
        $null = Update-AzWvdDesktop -ResourceGroupName $item.Value.ResourceGroupName -ApplicationGroupName $item.Key -Name 'SessionDesktop' -FriendlyName $item.Value.FriendlyName -ErrorAction Stop
    }
    #endregion

    # Workspaces
    Write-PSFMessage -Level Host -Message "Creating workspaces"
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Workspace') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'Workspace'

            try {
                $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop
            }
            catch {
                New-AzResourceGroup -Name $rg -Location $script:Location
            }
            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepWorkspaces @templateParams -ErrorAction Stop -Confirm:$false -Force
        }
    }

}

function Test-AVDMFDesktopVirtualization {
    [CmdletBinding()]
    param (

    )

    #region: Initialize Variables
    $bicepHostPools = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\HostPools.bicep"
    $bicepWorkspaces = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\Workspaces.bicep"
    #endregion: Initialize Variables

    # Host Pools
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'HostPool') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'HostPool'

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepHostPools @templateParams -ErrorAction Stop -WhatIf
        }
    }
    # Workspaces
    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Workspace') {
            $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'Workspace'

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepWorkspaces @templateParams -ErrorAction Stop -WhatIf
        }
    }
}

function Get-AVDMFApplicationGroup {
    $script:ApplicationGroups
}

function Register-AVDMFApplicationGroup {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolResourceId,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $Location = $script:Location,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $Name,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $FriendlyName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [ValidateSet('Desktop', 'RemoteApp')]
        [string] $ApplicationGroupType,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string[]] $RemoteAppReference,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string[]] $Users,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [ValidateSet("AAD", "ADDS")]
        [string] $SessionHostJoinType = "ADDS",

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'ApplicationGroup' -ParentName $HostPoolName -NameSuffix $Name

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/applicationgroups/$ResourceName"

        $principalId = @()
        if ($Users.count -ge 1) {
            $principalId += foreach ($user in $Users) {
                try {
                    if ($user -like "*@*" ) {
                        Write-PSFMessage -Level Verbose -Message "Resolving Id for user: $user"
                        if (-Not $script:Offline) {
                            $id = (Get-AzADUser -UserPrincipalName $user -ErrorAction Stop).Id
                        }
                        else {
                            $id = 'XXXXXX-XXXX-XXXX-XXXX-OFFLINE'
                        }

                    }
                    else {
                        Write-PSFMessage -Level Verbose -Message "Resolving Id for group: $user"
                        if (-Not $script:Offline) {
                            $id = (Get-AzADGroup -DisplayName $user -ErrorAction Stop).Id
                        }
                        else {
                            $id = 'XXXXXX-XXXX-XXXX-XXXX-OFFLINE'
                        }
                    }
                    if ($null -eq $id) {
                        throw
                    }
                    $id
                }
                catch {
                    throw "Could not resolve id for $user - If the name is correct then ensure the service principal used is assigned 'Directory readers' role."
                }
            }
        }
        else {

            if ($ApplicationGroupType -eq 'RemoteApp') {
                Write-PSFMessage -Level Warning -Message "No users defined for Host Pool: {0} - RemoteApp: {1}. Review documentation for how to assign users or groups in AVDMF configuration." -StringValues $HostPoolName, $resourceName
            }
            else {
                Write-PSFMessage -Level Warning -Message "No users defined for Host Pool: {0}. Review documentation for how to assign users or groups in AVDMF configuration." -StringValues $HostPoolName
            }
        }

        $script:ApplicationGroups[$resourceName] = [PSCustomObject]@{
            PSTypeName           = 'AVDMF.DesktopVirtualization.ApplicationGroup'
            ResourceGroupName    = $ResourceGroupName
            Location             = $Location
            HostPoolId           = $HostPoolResourceId
            ApplicationGroupType = $ApplicationGroupType
            FriendlyName         = $FriendlyName
            PrincipalId          = $principalId
            SessionHostJoinType  = $SessionHostJoinType
            Tags                 = $Tags
        }

        # Link Application group to workspace
        $script:Workspaces.GetEnumerator() | Where-Object { $_.value.ReferenceName -eq $script:hostpools.$hostpoolname.WorkspaceReference } | ForEach-Object { $_.value.ApplicationGroupReferences += $resourceID }


        # Register remote Apps
        if ($RemoteAppReference) {
            Write-PSFMessage -Level Verbose -Message "Registering Remote Apps"
            foreach ($remoteApp in $RemoteAppReference) {
                if ($script:RemoteAppTemplates[$remoteApp]) {
                    $registerRemoteAppParams = @{
                        ResourceGroupName    = $resourceGroupName
                        ApplicationGroupName = $resourceName
                        RemoteAppTemplate    = $script:RemoteAppTemplates[$remoteApp]
                    }
                    Register-AVDMFRemoteApp @registerRemoteAppParams
                }
                else {
                    throw "Could not find RemoteApp Template: $remoteApp"
                }
            }
        }
    }
}



function Get-AVDMFHostPool {
    $script:HostPools
}

function Register-AVDMFHostPool {
    <#
    .SYNOPSIS
        A short one-line action-based description, e.g. 'Tests if a function is valid'
    .DESCRIPTION
        A longer description of the function, its purpose, common use cases, etc.
    .NOTES
        Information or caveats about the function e.g. 'This function is not supported in Linux'
    .LINK
        Specify a URI to a help page, this will show when Get-Help -Online is used.
    .EXAMPLE
            Application Groups for RemoteApp Host Pools
                {
                    "Name": "Common Apps",
                    "RemoteAppReference":[
                        "SAPAnalyzer",
                        "SAPLogon"
                    ],
                    "Users":[
                        "BusinessAppGroup@oq.com"
                    ]
                }
    #>



    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [ValidateSet("Personal", "Pooled", "RemoteApp")]
        [string] $PoolType,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolMetadataLocation = $script:Location,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $ReplacementPlan,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $ScalingPlan,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [int] $MaxSessionLimit,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $NumberOfSessionHosts,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $FriendlyName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $WorkSpaceReference,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VirtualNetworkReference,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $SubnetNSG,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $SubnetRouteTable,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountReference,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        $RemoteAppGroups,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string[]] $Users,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VMTemplate,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [ValidateSet("AAD", "ADDS")]
        [string] $SessionHostJoinType = "ADDS",

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $UseAvailabilityZones = $false,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $StartVMOnConnect = $false,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $CustomRdpProperty = "drivestoredirect:s:;redirectwebauthn:i:0;redirectlocation:i:0;redirectclipboard:i:1;redirectprinters:i:0;devicestoredirect:s:;redirectcomports:i:0;redirectsmartcards:i:0;usbdevicestoredirect:s:;camerastoredirect:s:;autoreconnectionenabled:i:1;",

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        #Validate the value of Pooled only parameters against PoolType parameter
        $exclusiveParameters = @(
            @{Name = "ReplacementPlan"; PoolTypes = @("Pooled", "RemoteApp") }
            @{Name = "MaxSessionLimit"; PoolTypes = @("Pooled", "RemoteApp") }
        )
        foreach ($parameter in $exclusiveParameters) {
            $parameterValue = (Get-Variable -Name $parameter.Name -ErrorAction SilentlyContinue).Value
            if ($PoolType -in $parameter.PoolTypes -and ( [string]::IsNullOrEmpty($parameterValue))) {
                $errorMessage = "Parameter ({0}) is required for {1} Host Pools" -f $parameter.Name, ($parameter.PoolTypes -join ' and ')
                Write-PSFMessage -Level Error -Message $errorMessage
                throw $errorMessage
            }
            elseif ($PoolType -notin $parameter.PoolTypes -and (-Not [string]::IsNullOrEmpty($parameterValue))) {
                $errorMessage = "Parameter ({0}) is not supported for {1} Host Pools" -f $parameter.Name, ($parameter.PoolTypes -join ' and ')
                Write-PSFMessage -Level Error -Message $errorMessage
                throw $errorMessage
            }
        }

        $ResourceName = New-AVDMFResourceName -ResourceType 'HostPool' -AccessLevel $AccessLevel -HostPoolType $PoolType
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'HostPool' -AccessLevel $AccessLevel -HostPoolType $PoolType
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'HostPool'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/hostpools/$ResourceName"

        #Register Subnet
        $subnetParams = @{
            Scope              = $AccessLevel + 'Access'  #TODO: Change the parameter name from scope to Access Level, also change it in subnet configurations
            NamePrefix         = $resourceName
            VirtualNetworkName = $script:VirtualNetworks[$VirtualNetworkReference].ResourceName
            VirtualNetworkID   = $script:VirtualNetworks[$VirtualNetworkReference].ResourceID
            NSGID              = $script:NetworkSecurityGroups[$SubnetNSG].ResourceID
            RouteTableID       = $script:RouteTables[$SubnetRouteTable].ResourceID
        }

        $subnetID = Register-AVDMFSubnet @subnetParams -PassThru

        # Pickup Storage Account
        #TODO: Change Storage Accounts into HashTables
        if ($StorageAccountReference) {
            $storageAccount = $script:StorageAccounts[$StorageAccountReference]
            Register-AVDMFFileShare -Name $resourceName.ToLower() -StorageAccountName $storageAccount.Name -ResourceGroupName $storageAccount.ResourceGroupName
        }

        # Get Azure Virtual Desktop App Object Id for permission assignment
        Write-PSFMessage -Level Verbose -Message "Getting Azure Virtual Desktop App (9cdead84-a844-4324-93f2-b2e6bb768d07) Object Id for permission assignment"
        if (-Not $script:Offline) {
            $avdAppObjectId = (Get-AzADServicePrincipal -ApplicationId '9cdead84-a844-4324-93f2-b2e6bb768d07').Id
        }
        else {
            $avdAppObjectId = 'XXXXXX-XXXX-XXXX-XXXX-OFFLINE'
        }

        Write-PSFMessage -Level Verbose -Message "Azure Virtual Desktop App Object Id is: {0}" -StringValues $avdAppObjectId

        $script:HostPools[$ResourceName] = [PSCustomObject]@{
            PSTypeName           = 'AVDMF.DesktopVirtualization.HostPool'
            ResourceGroupName    = $resourceGroupName
            ResourceID           = $resourceID

            Location             = $HostPoolMetadataLocation
            PoolType             = $PoolType
            MaxSessionLimit      = $MaxSessionLimit
            NumberOfSessionHosts = $NumberOfSessionHosts
            CustomRdpProperty    = $CustomRdpProperty
            StartVMOnConnect     = $StartVMOnConnect
            AVDAppObjectId       = $avdAppObjectId

            WorkSpaceReference   = $WorkSpaceReference

            SubnetID             = $subnetID

            VMTemplate           = $VMTemplate

            # SessionHostJoinType = $script:SessionHostJoinType

            Tags                 = $Tags



        }
        #TODO: Check if users are provided.
        if ($PoolType -eq "RemoteApp") {
            # We assume only Remote App AGs are used for RemoteApp Host Pools
            foreach ($applicationGroup in $RemoteAppGroups) {
                # Register each application group
                $applicationGroupParams = @{
                    HostPoolName         = $resourceName
                    ResourceGroupName    = $resourceGroupName
                    Location             = $HostPoolMetadataLocation
                    HostPoolResourceId   = $resourceID
                    Users                = $applicationGroup.Users
                    Name                 = $applicationGroup.Name
                    FriendlyName         = $applicationGroup.Name
                    ApplicationGroupType = 'RemoteApp'
                    RemoteAppReference   = $applicationGroup.RemoteAppReference
                    SessionHostJoinType  = $SessionHostJoinType


                }
                #TODO: Add logic to check if all remote app references exist
                Register-AVDMFApplicationGroup @applicationGroupParams -ErrorAction Stop
            }
        }
        else {
            # This would apply for pooled and personal pools. Only creating one AG of type Desktop.
            $applicationGroupParams = @{
                HostPoolName         = $resourceName
                ResourceGroupName    = $resourceGroupName
                Location             = $HostPoolMetadataLocation
                HostPoolResourceId   = $resourceID
                Users                = $Users
                Name                 = "Desktop"
                FriendlyName         = $FriendlyName
                ApplicationGroupType = 'Desktop'
                SessionHostJoinType  = $SessionHostJoinType

            }
            Register-AVDMFApplicationGroup @applicationGroupParams
        }

        # Register Scaling Plan
        if (-Not [string]::IsNullOrEmpty($ScalingPlan)) {
            $scalingPlanParams = @{
                ResourceGroupName   = $resourceGroupName
                Location            = $HostPoolMetadataLocation
                HostPoolName        = $resourceName
                HostPoolId          = $resourceID
                ScalingPlanTemplate = $script:ScalingPlanTemplates[$ScalingPlan]
            }
            Register-AVDMFScalingPlan @scalingPlanParams
        }

        # Register TemplateSpec
        $templateSpecParams = @{
            ResourceGroupName = $resourceGroupName
            HostPoolName      = $resourceName
            TemplateFileName  = $script:VMTemplates[$VMTemplate].TemplateFileName
        }
        $templateSpecResourceId = Register-AVDMFTemplateSpec @templateSpecParams



        # Register Replacement Plan
        $hostPoolInstance = $ResourceName.Substring($ResourceName.Length - 2, 2)
        $sessionHostNamePrefix = New-AVDMFResourceName -ResourceType 'SessionHostPrefix' -AccessLevel $AccessLevel -HostPoolType $PoolType -HostPoolInstance $hostPoolInstance

        $hostPoolSessionHostParameters = $script:VMTemplates[$VMTemplate].Parameters | ConvertFrom-Json -Depth 99 -AsHashtable
        $hostPoolSessionHostParameters['SubnetID'] = $subnetID


        $replacementPlanParams = @{
            ResourceGroupName       = $resourceGroupName
            HostPoolName            = $resourceName
            TargetSessionHostCount  = $NumberOfSessionHosts
            SessionHostNamePrefix   = $sessionHostNamePrefix
            SubnetId                = $subnetID
            ReplacementPlanTemplate = $script:ReplacementPlanTemplates[$ReplacementPlan]
            SessionHostParameters   = $hostPoolSessionHostParameters | ConvertTo-Json -Depth 99 -Compress
            SessionHostTemplate     = $templateSpecResourceId
        }
        if (-Not [string]::IsNullOrEmpty($ScalingPlan)) {

            $replacementPlanParams.ScalingPlanExclusionTag = $script:ScalingPlanTemplates[$ScalingPlan].ExclusionTag
        }
        Register-AVDMFReplacementPlan @replacementPlanParams
    }
}



function Get-AVDMFRemoteApp {
    $script:RemoteApps
}

function Register-AVDMFRemoteApp {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ApplicationGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [object] $RemoteAppTemplate
    )
    process {
        $resourceName = "$ApplicationGroupName/$($RemoteAppTemplate.RemoteAppName)"
        Write-PSFMessage -Level Verbose -Message "Registering Remote App: $resourceName"
        #TODO: Validate inputs would create a working remote app
        #register Remote App
        $script:RemoteApps[$resourceName] = [PSCustomObject]@{
            PSTypeName           = 'AVDMF.DesktopVirtualization.RemoteApp'
            ResourceGroupName    = $ResourceGroupName
            ApplicationGroupName = $ApplicationGroupName
            RemoteAppName        = $RemoteAppTemplate.RemoteAppName
            RemoteAppProperties  = $RemoteAppTemplate.RemoteAppProperties | ConvertTo-PSFHashtable
        }

    }
}



function Get-AVDMFRemoteAppTemplate {
    $script:RemoteAppTemplates
}

function Register-AVDMFRemoteAppTemplate {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $Name,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        $Properties

    )
    process {
        #TODO: Validate inputs would create a working remote app

        #register Remote App Template
        $script:RemoteAppTemplates[$ReferenceName] = [PSCustomObject]@{
            PSTypeName          = 'AVDMF.DesktopVirtualization.RemoteAppTemplate'
            RemoteAppName       = $Name
            RemoteAppProperties = $Properties
        }
    }
}



function Get-AVDMFReplacementPlan {
    $script:ReplacementPlans
}

function Register-AVDMFReplacementPlan {
    <#
    .SYNOPSIS
        This function registers a replacement plan for a host pool.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $TargetSessionHostCount,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $SessionHostNamePrefix,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $ADOrganizationalUnitPath,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $SubnetId,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [object] $ReplacementPlanTemplate,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $ScalingPlanExclusionTag = "ScalingPlanExclusion",

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $UniqueNameString = "",

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $SessionHostTemplate,

        [PSCustomObject] $SessionHostParameters,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    $resourceName = New-AVDMFResourceName -ResourceType 'FunctionApp' -ParentName $HostPoolName -InstanceNumber 1 -UniqueNameString $UniqueNameString -NameSuffix $ReplacementPlanTemplate.ReplacementPlanNameSuffix
    $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Web/site/$ResourceName"

    $script:ReplacementPlans[$resourceName] = [PSCustomObject]@{
        ResourceGroupName                            = $ResourceGroupName
        HostPoolName                                 = $HostPoolName
        ResourceID                                   = $resourceID
        TargetSessionHostCount                       = $TargetSessionHostCount
        SessionHostNamePrefix                        = $SessionHostNamePrefix
        SubnetId                                     = $SubnetId
        SessionHostTemplate                          = $SessionHostTemplate
        SessionHostParameters                        = $SessionHostParameters
        TagScalingPlanExclusionTag                   = $ScalingPlanExclusionTag
        Tags                                         = $Tags

        # Replacement plan template
        AllowDownsizing                              = $ReplacementPlanTemplate.AllowDownsizing
        AppPlanName                                  = $ReplacementPlanTemplate.AppPlanName
        AppPlanTier                                  = $ReplacementPlanTemplate.AppPlanTier
        DrainGracePeriodHours                        = $ReplacementPlanTemplate.DrainGracePeriodHours
        FixSessionHostTags                           = $ReplacementPlanTemplate.FixSessionHostTags
        FunctionAppZipUrl                            = $ReplacementPlanTemplate.FunctionAppZipUrl
        MaxSimultaneousDeployments                   = $ReplacementPlanTemplate.MaxSimultaneousDeployments
        ReplaceSessionHostOnNewImageVersion          = $ReplacementPlanTemplate.ReplaceSessionHostOnNewImageVersion
        ReplaceSessionHostOnNewImageVersionDelayDays = $ReplacementPlanTemplate.ReplaceSessionHostOnNewImageVersionDelayDays
        SessionHostInstanceNumberPadding             = $ReplacementPlanTemplate.SessionHostInstanceNumberPadding
        SHRDeploymentPrefix                          = $ReplacementPlanTemplate.SHRDeploymentPrefix
        TagDeployTimestamp                           = $ReplacementPlanTemplate.TagDeployTimestamp
        TagIncludeInAutomation                       = $ReplacementPlanTemplate.TagIncludeInAutomation
        TagPendingDrainTimestamp                     = $ReplacementPlanTemplate.TagPendingDrainTimestamp
        TargetVMAgeDays                              = $ReplacementPlanTemplate.TargetVMAgeDays
        RemoveAzureADDevice                          = $ReplacementPlanTemplate.RemoveAzureADDevice
    }
}



function Get-AVDMFReplacementPlanTemplate {
    $script:ReplacementPlanTemplates
}

function Register-AVDMFReplacementPlanTemplate {
    <#
    .SYNOPSIS
        This function registers a replacement plan template.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReplacementPlanNameSuffix,

        [PSCustomObject] $Tags = [PSCustomObject]@{},

        ### This is generated from replacement plan parameters helper script
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $AllowDownsizing = $true,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $AppPlanName = 'Y1',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $AppPlanTier = 'Dynamic',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [int] $DrainGracePeriodHours = 24,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $FixSessionHostTags = $true,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $FunctionAppZipUrl = 'https://github.com/WillyMoselhy/AVDReplacementPlans/releases/download/v0.1.5/FunctionApp.zip',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [int] $MaxSimultaneousDeployments = 20,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $ReplaceSessionHostOnNewImageVersion = $true,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [int] $ReplaceSessionHostOnNewImageVersionDelayDays = 0,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [int] $SessionHostInstanceNumberPadding = 2,
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $SHRDeploymentPrefix = 'AVDSessionHostReplacer',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $TagDeployTimestamp = 'AutoReplaceDeployTimestamp',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $TagIncludeInAutomation = 'IncludeInAutoReplace',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $TagPendingDrainTimestamp = 'AutoReplacePendingDrainTimestamp',
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [int] $TargetVMAgeDays = 45,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [bool] $RemoveAzureADDevice



    )

    #register AVD Replacement Plan Template
    $script:ReplacementPlanTemplates[$ReferenceName] = [PSCustomObject]@{
        PSTypeName                                   = 'AVDMF.DesktopVirtualization.AVDReplacementPlanTemplate'
        ReplacementPlanNameSuffix                    = $ReplacementPlanNameSuffix
        Tags                                         = $Tags
        ### This is generated from replacement plan parameters helper script

        AllowDownsizing                              = $AllowDownsizing
        AppPlanName                                  = $AppPlanName
        AppPlanTier                                  = $AppPlanTier
        DrainGracePeriodHours                        = $DrainGracePeriodHours
        FixSessionHostTags                           = $FixSessionHostTags
        FunctionAppZipUrl                            = $FunctionAppZipUrl
        MaxSimultaneousDeployments                   = $MaxSimultaneousDeployments
        ReplaceSessionHostOnNewImageVersion          = $ReplaceSessionHostOnNewImageVersion
        ReplaceSessionHostOnNewImageVersionDelayDays = $ReplaceSessionHostOnNewImageVersionDelayDays
        SessionHostInstanceNumberPadding             = $SessionHostInstanceNumberPadding
        SHRDeploymentPrefix                          = $SHRDeploymentPrefix
        TagDeployTimestamp                           = $TagDeployTimestamp
        TagIncludeInAutomation                       = $TagIncludeInAutomation
        TagPendingDrainTimestamp                     = $TagPendingDrainTimestamp
        TargetVMAgeDays                              = $TargetVMAgeDays
        RemoveAzureADDevice                          = $RemoveAzureADDevice
    }
}



function Get-AVDMFScalingPlan {
    $script:ScalingPlans
}

function Register-AVDMFScalingPlan {
    <#
    .SYNOPSIS
        This function registers a Scaling plan for a host pool.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $Location = $script:Location,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolId,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [PSCustomObject] $ScalingPlanTemplate,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )

    $resourceName = New-AVDMFResourceName -ResourceType 'ScalingPlan' -ParentName $HostPoolName -InstanceNumber 1

    $script:ScalingPlans[$resourceName] = [PSCustomObject]@{
        ResourceGroupName = $ResourceGroupName
        Location          = $Location
        HostPoolId        = $HostPoolId
        Timezone          = $ScalingPlanTemplate.Timezone
        Schedules         = $ScalingPlanTemplate.Schedules
        ExclusionTag      = $ScalingPlanTemplate.ExclusionTag
        Tags              = $Tags
    }
}



function Get-AVDMFScalingPlanScheduleTemplate {
    $script:ScalingPlanScheduleTemplates
}

function Register-AVDMFScalingPlanScheduleTemplate {
    <#
    .SYNOPSIS
        This function registers a scaling plan schedule template.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [PSCustomObject] $Parameters
    )

    #register AVD Replacement Plan Template
    $script:ScalingPlanScheduleTemplates[$ReferenceName] = [PSCustomObject]@{
        PSTypeName = 'AVDMF.DesktopVirtualization.AVDScalingPlanScheduleTemplate'
        Parameters = $Parameters | ConvertTo-Json -Depth 100 | ConvertFrom-Json -AsHashtable
    }
}



function Get-AVDMFScalingPlanTemplate {
    $script:ScalingPlanTemplates
}

function Register-AVDMFScalingPlanTemplate {
    <#
    .SYNOPSIS
        This function registers a scaling plan template.
 
    .DESCRIPTION
        The Register-AVDMFScalingPlanTemplate function creates and registers a scaling plan template for Azure Virtual Desktop.
        It requires the time zone, schedules and a reference name as mandatory parameters.
        It also optionally allows for an exclusion tag and additional tags to be defined.
 
    .EXAMPLE
        Register-AVDMFScalingPlanTemplate -ReferenceName "MyTemplate" -Timezone "Pacific Standard Time" -Schedules @("Schedule1", "Schedule2")
 
    .NOTES
        The scaling plan template created by this function is stored in the ReplacementPlanTemplates array,
        using the provided reference name as the key.
 
    #>

    [CmdletBinding()]
    param (
        # The name of the scaling plan template to be registered. This will be used as the key in the ReplacementPlanTemplates array.
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        # The time zone in which the scaling plan is to be implemented. Only accepts valid time zone names.
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [ValidateSet(
            ErrorMessage = 'Invalid Time Zone, Please use a valid Timezone from: https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones',
            'Afghanistan Standard Time',
            'Arab Standard Time',
            'Arabian Standard Time',
            'Arabic Standard Time',
            'Argentina Standard Time',
            'Atlantic Standard Time',
            'AUS Eastern Standard Time',
            'Azerbaijan Standard Time',
            'Bangladesh Standard Time',
            'Belarus Standard Time',
            'Cape Verde Standard Time',
            'Caucasus Standard Time',
            'Central America Standard Time',
            'Central Asia Standard Time',
            'Central Europe Standard Time',
            'Central European Standard Time',
            'Central Pacific Standard Time',
            'Central Standard Time (Mexico)',
            'China Standard Time',
            'E. Africa Standard Time',
            'E. Europe Standard Time',
            'E. South America Standard Time',
            'Eastern Standard Time',
            'Egypt Standard Time',
            'Fiji Standard Time',
            'FLE Standard Time',
            'Georgian Standard Time',
            'GMT Standard Time',
            'Greenland Standard Time',
            'Greenwich Standard Time',
            'GTB Standard Time',
            'Hawaiian Standard Time',
            'India Standard Time',
            'Iran Standard Time',
            'Israel Standard Time',
            'Jordan Standard Time',
            'Korea Standard Time',
            'Mauritius Standard Time',
            'Middle East Standard Time',
            'Montevideo Standard Time',
            'Morocco Standard Time',
            'Mountain Standard Time',
            'Myanmar Standard Time',
            'Namibia Standard Time',
            'Nepal Standard Time',
            'New Zealand Standard Time',
            'Pacific SA Standard Time',
            'Pacific Standard Time',
            'Pakistan Standard Time',
            'Paraguay Standard Time',
            'Romance Standard Time',
            'Russian Standard Time',
            'SA Eastern Standard Time',
            'SA Pacific Standard Time',
            'SA Western Standard Time',
            'Samoa Standard Time',
            'SE Asia Standard Time',
            'Singapore Standard Time',
            'South Africa Standard Time',
            'Sri Lanka Standard Time',
            'Syria Standard Time',
            'Taipei Standard Time',
            'Tokyo Standard Time',
            'Tonga Standard Time',
            'Turkey Standard Time',
            'Ulaanbaatar Standard Time',
            'UTC',
            'UTC+12',
            'UTC-02',
            'UTC-11',
            'Venezuela Standard Time',
            'W. Central Africa Standard Time',
            'W. Europe Standard Time',
            'West Asia Standard Time',
            'West Pacific Standard Time'
        )]
        [string] $Timezone,

        # Array of schedule names that are associated with the scaling plan template.
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string[]] $Schedules,

        # Optional parameter. Defines a tag that, when assigned to a resource, will exclude it from the scaling plan.
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $ExclusionTag = 'ScalingPlanExclusion',

        # Optional parameter. Defines a set of additional tags that will be assigned to the scaling plan template.
        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )

    [array] $scheduleArray = ($Schedules | ForEach-Object { $script:ScalingPlanScheduleTemplates[$_] }).Parameters

    $script:ScalingPlanTemplates[$ReferenceName] = [PSCustomObject]@{
        PSTypeName   = 'AVDMF.DesktopVirtualization.AVDScalingPlanTemplate'
        Timezone     = $Timezone
        Schedules    = $scheduleArray
        ExclusionTag = $ExclusionTag
        Tags         = $Tags
    }
}



function Get-AVDMFSessionHost {
    $script:SessionHosts
}

function Register-AVDMFSessionHost {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolType,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolInstance,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $InstanceNumber,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [object] $VMTemplate,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [object] $SubnetID,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [ValidateSet("AAD", "ADDS")]
        [string] $SessionHostJoinType,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $DomainName,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $OUPath,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $AvailabilityZone = '',

        [PSCustomObject] $Tags = [PSCustomObject]@{}

    )
    process {

        $ResourceName = New-AVDMFResourceName -ResourceType 'VirtualMachine' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -HostPoolInstance $HostPoolInstance -InstanceNumber $InstanceNumber

        $script:SessionHosts[$resourceName] = [PSCustomObject]@{ # TODO: Is it a good idea to switch this to hashtable not custom object?
            ResourceGroupName     = $ResourceGroupName
            VMSize                = $VMTemplate.VMSize
            TimeZone              = $script:TimeZone
            SubnetID              = $SubnetID
            AdminUsername         = $VMTemplate.AdminUserName
            AdminPassword         = $VMTemplate.AdminPassword
            ImageReference        = $VMTemplate.ImageReference
            AcceleratedNetworking = $VMTemplate.AcceleratedNetworking
            Tags                  = $Tags
            AvailabilityZone      = $AvailabilityZone
            PreJoinRunCommand     = $VMTemplate.PreJoinRunCommand

            # Add Session Host
            WVDArtifactsURL       = $VMTemplate.WVDArtifactsURL

            SessionHostJoinType   = $SessionHostJoinType
        }
        # AAD or Domain Join
        switch ($SessionHostJoinType) {
            "AAD" {

            }
            "ADDS" {
                $script:SessionHosts[$resourceName] | Add-Member -MemberType NoteProperty -Name DomainName -Value $DomainName
                $script:SessionHosts[$resourceName] | Add-Member -MemberType NoteProperty -Name OUPath -Value $OUPath
                $script:SessionHosts[$resourceName] | Add-Member -MemberType NoteProperty -Name DomainJoinUserName -Value $script:DomainJoinUserName
                $script:SessionHosts[$resourceName] | Add-Member -MemberType NoteProperty -Name DomainJoinPassword -Value $script:DomainJoinPassword
            }
        }
    }
}



function Get-AVDMFTemplateSpec {
    $script:TemplateSpecs
}

function Register-AVDMFTemplateSpec {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $TemplateFileName

    )

    $resourceName = New-AVDMFResourceName -ResourceType 'TemplateSpec' -ParentName $HostPoolName -InstanceNumber 1
    $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Resources/templateSpecs/$resourceName"

    $templateFilePath = Join-PSFPath -Path $script:ConfigurationPath -Child 'DesktopVirtualization', 'VMTemplates', 'TemplateFiles', $TemplateFileName
    Write-PSFMessage -Level Verbose -Message "Loading bicep file from {0}" -StringValues $templateFilePath

    $templateJSON = [string] (bicep build $templateFilePath --stdout )
    if ([string]::IsNullOrEmpty($templateJSON)) {
        Stop-PSFFunction -Message "Could not load VM Template file: $templateFilePath" -EnableException $true -Category InvalidData
    }

    $script:TemplateSpecs[$resourceName] = [PSCustomObject]@{
        PSTypeName        = 'AVDMF.DesktopVirtualization.TemplateSpec'
        ResourceGroupName = $ResourceGroupName
        ResourceID        = $resourceID

        TemplateFileName  = $TemplateFileName
        TemplateJSON      = $templateJSON
    }

    $resourceID

}



function Get-AVDMFVMTemplate {
    $script:VMTemplates
}

function Register-AVDMFVMTemplate {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [PSCustomObject] $Parameters,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $TemplateFileName
    )
    process {
        $script:VMTemplates[$ReferenceName] = @{
            Parameters       = $Parameters | ConvertTo-Json -Depth 100 -Compress # Converting to JSON as this is how it is stored as a FunctipnApp Configuration.
            TemplateFileName = $TemplateFileName
        }
    }
}



function Get-AVDMFWorkspace {
    $script:Workspaces
}

function Register-AVDMFWorkspace {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $FriendlyName,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $Location = $script:Location,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $ResourceName = New-AVDMFResourceName -ResourceType 'Workspace' -AccessLevel $AccessLevel -HostPoolType $HostPoolType
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Workspace' -AccessLevel $AccessLevel -HostPoolType $HostPoolType
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Workspace'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/workspaces/$ResourceName"

        $script:Workspaces[$ResourceName] = [PSCustomObject]@{
            PSTypeName                 = 'AVDMF.DesktopVirtualization.Workspace'
            ResourceID                 = $resourceID
            ReferenceName              = $ReferenceName
            ResourceGroupName          = $resourceGroupName
            Location                   = $Location
            FriendlyName               = $FriendlyName
            ApplicationGroupReferences = @()
            Tags                       = $Tags
        }
    }
}



function Register-AVDMFGlobalSettings {
    param (
        # Stage
        [string]
        $Stage
    )
    $script:AVDMFGlobalSettings = [PSCustomObject]@{
        Stage = $Stage
    }
}

function Add-AVDMFTag {
    <#
    .SYNOPSIS
        Adds tags to resources
    #>

    [CmdletBinding()]
    param (
        # ResourceType
        [Parameter(Mandatory = $true)]
        [string] $ResourceType,

        # Resource Object
        [Parameter(Mandatory = $true)]
        $ResourceObject
    )
    # Tags that apply to all resources
    if($script:GlobalTags['All']){
        $effectiveTags = $script:GlobalTags['All'].Clone()
    }

    # Tags that apply to all instaces of a specific resource type
    if($script:GlobalTags[$ResourceType]){
        $resourceTypeTags = $script:GlobalTags[$ResourceType]
        foreach($item in $resourceTypeTags.GetEnumerator()) {$effectiveTags[$item.Key] = $item.Value}
    }

    if($ResourceObject.Tags){
        $resourceSpecificTags = $ResourceObject.Tags | ConvertTo-PSFHashtable
        foreach($item in $resourceSpecificTags.GetEnumerator()) {$effectiveTags[$item.Key] = $item.Value}
    }



    if ($effectiveTags) {
        $ResourceObject | Add-Member -MemberType NoteProperty -Name Tags -Value $effectiveTags -Force
    }

    $ResourceObject
}

function Get-AVDMFGlobalTag {
    $script:GlobalTags
}

function Register-AVDMFGlobalTag {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ResourceType,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [PSCustomObject] $Tags
    )
    process {
        $Tags = $Tags | ConvertTo-PSFHashtable
        $script:GlobalTags[$ResourceType] = $Tags
    }
}

function Get-AVDMFNameMapping {
    $script:NameMappings
}

function Register-AVDMFNameMapping {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $Name,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $VariableName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [Object] $Value


    )
    process {
        if($VariableName -like "$*"){
            throw "Variable names in Name Mapping cannot start with '$'"
        }
        $variableNameValue = if($VariableName -like "env:*"){
            (Get-Item -Path $VariableName).Value
        }
        else{
            (Get-Variable -Name $VariableName).Value
        }
        $script:NameMappings[$Name] = $Value.$variableNameValue
    }
}

function Initialize-AVDMFNetwork {
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    param (
        [string] $ResourceGroupName
    )

    $filteredVirtualNetworks = @{}
    $filteredSubnets = @{}
    $filteredNetworkSecurityGroups = @{}
    $filteredRouteTables = @{}


    $script:VirtualNetworks.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } -PipelineVariable vNet | ForEach-Object {
        $filteredVirtualNetworks.Add($vNet.Key, $vNet.Value)
        $script:Subnets.GetEnumerator() | Where-Object { $_.value.VirtualNetworkName -eq $vNet.Value.ResourceName } | ForEach-Object { $filteredSubnets.Add($_.Key, $_.Value) }
    }

    $script:RouteTables.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } -PipelineVariable routeTable | ForEach-Object {
        $filteredRouteTables.Add($routeTable.Key, $routeTable.Value)
    }

    foreach ($nsg in $script:NetworkSecurityGroups.keys) {
        if ($script:NetworkSecurityGroups[$nsg].ResourceGroupName -eq $ResourceGroupName) {
            $filteredNetworkSecurityGroups[$nsg] = $script:NetworkSecurityGroups[$nsg]
        }
    }

    $templateParams = @{

        VirtualNetworks       = [array] ($filteredVirtualNetworks | Convert-HashtableToArray)
        Subnets               = [array] ($filteredSubnets | Convert-HashtableToArray)
        NetworkSecurityGroups = [array] ($filteredNetworkSecurityGroups | Convert-HashtableToArray)
        RouteTables           = [array] ($filteredRouteTables | Convert-HashtableToArray)

    }
    $templateParams

}

function Initialize-AVDMFRemotePeering {
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    $templateParams = @{
        RemotePeerings = [array] ($script:RemotePeerings | Convert-HashtableToArray)
    }
    $templateParams

}

function Invoke-AVDMFNetwork {
    [CmdletBinding()]
    param (
        [ValidateSet('All', 'DeployNetwork', 'RemotePeering')]
        [string[]] $Action = 'All'
    )
    if($script:Offline){
        throw "Cannot deploy when working offline. Please reload configuration without the offline switch."
    }

    if ($Action -contains 'All' -or $Action -contains 'DeployNetwork') {
        Write-PSFMessage -Level Verbose -Message "Starting Action: DeployNetwork"
        # TODO: Handle multiple peerings scenario
        #Initialize Variables
        $bicepVirtualNetwork = "$($moduleRoot)\internal\Bicep\Network\Network.bicep"

        foreach ($rg in $script:ResourceGroups.Keys) {
            if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Network') {
                $templateParams = Initialize-AVDMFNetwork -ResourceGroupName $rg
                try {
                    Write-PSFMessage -Level Verbose -Message "Checking if resource group exists: {0}" -StringValues $rg
                    $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop
                }
                catch {
                    Write-PSFMessage -Level Verbose -Message "Creating resource group {0} in Location {1}" -StringValues $rg, $script:Location #TODO: This is a repeated message and should use the power of PSFramework
                    New-AzResourceGroup -Name $rg -Location $script:Location
                }
                Write-PSFMessage -Level Verbose -Message "Deploying network resources in {0}" -StringValues $rg
                New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepVirtualNetwork @templateParams -ErrorAction Stop -Confirm:$false -Force
            }
        }
    }

    if ($Action -contains 'All' -or $Action -contains 'RemotePeering') {
        # Create remote peerings
        if ($script:RemotePeerings.count) {
            Write-PSFMessage -Level Verbose -Message "Starting Action: RemotePeering"

            $templateParams = Initialize-AVDMFRemotePeering

            $currentSubscription = (Get-AzContext).Subscription.Id
            $targetSubscription = $templateParams.RemotePeerings.SubscriptionId

            Write-PSFMessage -Level Verbose -Message "Switching to remote network subscription context ({0})" -StringValues $targetSubscription
            $null = Set-AzContext -SubscriptionId $templateParams.RemotePeerings.SubscriptionId

            # We are not using Azure Deployment for remote peering so we limit the needed permissions on the hub network
            # WE only need network contributor permissions on the hyb vNet using this approach.

            $remoteVNet = Get-AzVirtualNetwork -Name $templateParams.RemotePeerings.RemoteVNetNAme -ResourceGroupName $templateParams.RemotePeerings.ResourceGRoupName
            try{
                Add-AzVirtualNetworkPeering -Name $templateParams.RemotePeerings.Name -VirtualNetwork $remoteVNet -RemoteVirtualNetworkId $templateParams.RemotePeerings.LocalVNetResourceId -ErrorAction Stop
            }
            catch{
                if($_.Exception.Message -eq 'Peering with the specified name already exists'){
                    Write-PSFMessage -Level Warning -Message "Peering with the specified name already exists."
                }
                else{
                    $peeringError = $_
                }
            }
            finally{
                Write-PSFMessage -Level Verbose -Message "Switching back to local subscription context ({0})" -StringValues $targetSubscription
                $null = Set-AzContext -SubscriptionId $currentSubscription
                if($peeringError) {throw $peeringError}
            }
        }
    }
}

function Test-AVDMFNetwork {
    [CmdletBinding()]
    param (

    )

    #region: Initialize Variables
    $bicepVirtualNetwork = "$($moduleRoot)\internal\Bicep\Network\Network.bicep"
    #endregion: Initialize Variables

    foreach ($rg in $script:ResourceGroups.Keys) {

        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Network') {
            $templateParams = Initialize-AVDMFNetwork -ResourceGroupName $rg
            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepVirtualNetwork @templateParams -ErrorAction Stop -WhatIf
        }
    }
}

function Get-AVDMFAddressSpace {
    $script:AddressSpaces
}

function Register-AVDMFAddressSpace {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $Scope,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $AddressSpace,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [int] $SubnetMask
    )
    process {
        $Script:AddressSpaces += [PSCustomObject]@{
            Scope        = $Scope
            AddressSpace = $AddressSpace
            subnetMask   = $SubnetMask
        }
    }
}



function Get-AVDMFNetworkSecurityGroup {
    $script:NetworkSecurityGroups
}

function Register-AVDMFNetworkSecurityGroup {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ReferenceName,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [array] $SecurityRules,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $HostPoolType,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'NetworkSecurityGroup' -AccessLevel $AccessLevel -HostPoolType $HostPoolType

        #Register Resource Group if needed
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1
        # At the moment we do not have a reason for multiple network RGs.
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/networkSecurityGroups/$resourceName"


        $script:NetworkSecurityGroups[$ReferenceName] = [PSCustomObject]@{
            PSTypeName        = 'AVDMF.Network.NetworkSecurityGroup'
            ResourceName      = $resourceName
            ResourceGroupName = $resourceGroupName
            ResourceID        = $resourceID
            ReferenceName     = $ReferenceName
            SecurityRules     = if($SecurityRules ) {@($SecurityRules | ForEach-Object { $_ | ConvertTo-PSFHashtable })} else {$null}
            Tags = $Tags
        }
    }
}



function Get-AVDMFRemotePeering {
    $script:RemotePeerings
}

function Register-AVDMFRemotePeering {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $RemoteVNetResourceID,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $LocalVNetResourceId
    )
    process {

        $remoteVNet = Get-AVDMFResourceInfo -ResourceId $RemoteVNetResourceID
        $localVNet = Get-AVDMFResourceInfo -ResourceId $LocalVNetResourceId

        $referenceName = "Peering_{0}_To_{1}" -f $RemoteVNet.ResourceName, $LocalVNet.ResourceName #this is used for the hashtable.
        $name = "Peering_To_{0}" -f $LocalVNet.ResourceName


        $script:RemotePeerings[$referenceName] = [PSCustomObject]@{
            PSTypeName          = 'AVDMF.Network.RemotePeering'
            Name                = $name
            SubscriptionId      = $remoteVNet.SubscriptionId #TODO: Implement Remote Subscription Support.
            ResourceGroupName   = $remoteVNet.ResourceGroupName
            RemoteVNetName      = $remoteVNet.ResourceName
            LocalVNetResourceId = $LocalVNetResourceId
        }
    }

}

function Get-AVDMFRouteTable {
    $script:RouteTables
}

function Register-AVDMFRouteTable {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [array] $Routes,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Boolean] $DisableBgpRoutePropagation,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $HostPoolType,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'RouteTable' -AccessLevel $AccessLevel -HostPoolType $HostPoolType

        #Register Resource Group if needed
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1
        # At the moment we do not have a reason for multiple network RGs.
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network'

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/routeTables/$resourceName"

        $routesHashTable = @($Routes | ForEach-Object { $_ | ConvertTo-PSFHashtable })
        foreach ($item in $routesHashTable) {
            $item.properties = $item.properties | ConvertTo-PSFHashtable
        }

        $script:RouteTables[$ReferenceName] = [PSCustomObject]@{
            PSTypeName                 = 'AVDMF.Network.RouteTable'
            ResourceName               = $resourceName
            ResourceGroupName          = $resourceGroupName
            ResourceID                 = $resourceID
            ReferenceName              = $ReferenceName
            Routes                     = $routesHashTable #@($Routes | ForEach-Object { $_ | ConvertTo-PSFHashtable })
            DisableBgpRoutePropagation = $DisableBgpRoutePropagation
            Tags                       = $Tags
        }
    }
}

function Get-AVDMFSubnet {
    $script:Subnets
}

function Register-AVDMFSubnet {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $Scope,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $NamePrefix,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $VirtualNetworkName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $VirtualNetworkID,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [bool] $PrivateLink = $false,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string] $NSGID ,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string] $RouteTableID ,

        [switch] $PassThru

    )

    process {
        #region: Calculate subnet range and prefix
        [array] $scope = ($Script:AddressSpaces | Where-Object { $_.Scope -eq $Scope })
        if ($scope.count -gt 1) { throw "Found multiple scopes, please review address spaces configuration and avoid duplicates." }

        [string] $addressSpace = $scope.AddressSpace
        [int] $subnetMask = $scope.SubnetMask
        Write-Verbose "Will use the address space $addressSpace and subnet mask $subnetMask"

        if (-not ($addressSpace -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{2}$')) {
            throw "$addressSpace is not a valid address space"
        }

        $addressPrefix = (New-AVDMFSubnetRange -AddressSpace $addressSpace -NewSubnetMaskBits $subnetMask -ErrorAction 'Stop').AddressPrefix
        #endregion: Calculate subnet range and prefix

        $resourceName = New-AVDMFResourceName -ResourceType 'Subnet' -ParentName $NamePrefix -AddressPrefix $addressPrefix
        $resourceID = "$VirtualNetworkID/subnets/$resourceName"

        #Build Subnet properties
        $properties = @{
            addressPrefix                  = $addressPrefix
            privateEndpointNetworkPolicies = if ($PrivateLink) { "Disabled" } else { "Enabled" }
        }
        if ($NSGID) {
            $properties['networkSecurityGroup'] = @{id = $NSGID }
        }
        if ($RouteTableID) {
            $properties['routeTable'] = @{id = $RouteTableID }
        }

        $script:Subnets[$resourceName] = [PSCustomObject]@{
            PSTypeName         = 'AVDMF.Network.Subnet'
            VirtualNetworkName = $VirtualNetworkName
            ResourceID         = $resourceID
            Properties         = $properties
        }

        if ($PassThru) { $resourceID }
    }

}



function Get-AVDMFVirtualNetwork {
    $script:VirtualNetworks
}

function Register-AVDMFVirtualNetwork {
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ReferenceName,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string[]] $DNSServers,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [array] $DefaultSubnets,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [array] $VirtualNetworkPeerings,

        [string] $AccessLevel = 'All',
        [string] $HostPoolType = 'All',

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {
        $resourceName = New-AVDMFResourceName -ResourceType 'VirtualNetwork' -AccessLevel $AccessLevel -HostPoolType $HostPoolType

        #Register Resource Group if needed
        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network'
        # At the moment we do not have a reason for multiple network RGs.

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/VirtualNetworks/$resourceName"

        #Register Virtual Networks
        [string]$addressSpace = ($Script:AddressSpaces | Where-Object Scope -EQ 'VirtualNetwork').AddressSpace

        if (-not ($addressSpace -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{2}$')) {
            throw "$addressSpace is not a valid address space"
        }
        Write-PSFMessage -Level Verbose -Message 'Configuring peerings'
        # Configure Peerings
        $peerings = @(foreach ($peering in $VirtualNetworkPeerings) {
                $RemoteNetworkName = ($peering.RemoteVnetId -split "/")[-1]
                Write-PSFMessage -Level Verbose -Message "Configuring peering with $RemoteNetworkName"
                @{
                    Name              = "PeeringTo_$RemoteNetworkName"
                    RemoteNetworkID   = $peering.RemoteVnetId
                    UseRemoteGateways = [bool] $peering.useRemoteGateways
                }
                if ($peering.CreateRemotePeering) {
                    Write-PSFMessage -Level Verbose -Message "Registering remote peering."
                    Register-AVDMFRemotePeering -RemoteVNetResourceID $peering.RemoteVNetId -LocalVNetResourceId $resourceID
                }
                else {
                    Write-PSFMessage -Level Warning -Message "Peering of Virtual Network '$ReferenceName ($resourceName)' to '$RemoteNetworkName' is not configured to create remote peering. You must manually create peering in the remote network." # Add link to help on website.
                }
            })



        $script:VirtualNetworks[$ReferenceName] = [PSCustomObject]@{
            PSTypeName             = 'AVDMF.Network.VirtualNetwork'
            ResourceName           = $resourceName
            ResourceGroupName      = $resourceGroupName
            ResourceID             = $resourceID
            AddressSpace           = $addressSpace
            DNSServers             = $DNSServers
            VirtualNetworkPeerings = $peerings
            Tags                   = $Tags
        }

        #Register Default Subnets
        foreach ($subnet in $DefaultSubnets) {
            $paramRegisterAVDMFSubnet = @{
                VirtualNetworkName = $resourceName
                VirtualNetworkID   = $resourceID
            }
            if ($subnet.NSG)        { $paramRegisterAVDMFSubnet['NSGID']        = $script:NetworkSecurityGroups[$subnet.NSG].ResourceID }
            if ($subnet.RouteTable) { $paramRegisterAVDMFSubnet['RouteTableID'] = $script:RouteTables[$subnet.RouteTable].ResourceID }
            $subnet | Register-AVDMFSubnet @paramRegisterAVDMFSubnet -ErrorAction Stop # TODO: Why do we have pipeline here?
        }
    }

}



function Get-AVDMFResourceGroup {
    $script:ResourceGroups
}



function Register-AVDMFResourceGroup {
    [CmdletBinding()]
    param (
        [string] $Name,
        [string] $ResourceCategory
    )

    $script:ResourceGroups[$Name] = [PSCustomObject]@{
        PSTypeName  = 'AVDMF.ResourceGroup'
        ResourceCategory = $ResourceCategory
    }

}





function Initialize-AVDMFStorage {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")]
    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]

    param (
        [string] $ResourceGroupName
    )

    $filteredStorageAccounts = @{}
    $filteredPrivateLinks = @{}
    $filteredFileShares = @{}
    $filteredFileShareAutoGrowLogicApps = @{}

    $script:StorageAccounts.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredStorageAccounts.Add($_.Key, $_.Value) }

    $script:PrivateLinks.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredPrivateLinks.Add($_.Key, $_.Value) }

    $script:FileShares.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredFileShares.Add($_.Key, $_.Value) }

    $script:FileShareAutoGrowLogicApps.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredFileShareAutoGrowLogicApps.Add($_.Key, $_.Value) }


    $templateParams = @{
        StorageAccounts            = [array] ($filteredStorageAccounts | Convert-HashtableToArray)
        PrivateLinks               = [array] ($filteredPrivateLinks | Convert-HashtableToArray)
        FileShares                 = [array] ($filteredFileShares | Convert-HashtableToArray)
        FileShareAutoGrowLogicApps = [array] ($filteredFileShareAutoGrowLogicApps | Convert-HashtableToArray)
    }
    $templateParams

}

function Invoke-AVDMFStorage {
    [CmdletBinding()]
    param (

    )
    if($script:Offline){
        throw "Cannot deploy when working offline. Please reload configuration without the offline switch."
    }
    #region: Initialize Variables
    $bicepStorage = "$($moduleRoot)\internal\Bicep\Storage\Storage.bicep"
    #endregion: Initialize Variables

    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Storage') {
            $templateParams = Initialize-AVDMFStorage -ResourceGroupName $rg

            try{
                Get-AzResourceGroup -Name $rg -ErrorAction Stop | Out-Null
            }
            catch{
                New-AzResourceGroup -Name $rg -Location $script:Location
            }

            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Incremental -TemplateFile $bicepStorage @templateParams -ErrorAction Stop -Confirm:$false -Force
            # Cannot use Complete mode with Private links, see: https://feedback.azure.com/forums/217313-networking/suggestions/40395946-private-endpoint-arm-template-deployment-fix-comp

        }
    }
}

function Test-AVDMFStorage {
    [CmdletBinding()]
    param (

    )
    #region: Initialize Variables
    $bicepStorage = "$($moduleRoot)\internal\Bicep\Storage\Storage.bicep"
    #endregion: Initialize Variables

    foreach ($rg in $script:ResourceGroups.Keys) {
        if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Storage') {
            $templateParams = Initialize-AVDMFStorage -ResourceGroupName $rg
            try{
                Get-AzResourceGroup -Name $rg -ErrorAction Stop | Out-Null
            }
            catch{
                Write-Warning -Message "Resourcegroup $rg does not exist. Skipping test for: `r`n$($templateParams.Values.ResourceID | out-string)"
                continue
            }
            New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepStorage @templateParams -ErrorAction Stop -WhatIf
        }
    }
}

function Get-AVDMFFileShareAutoGrowLogicApp {
    $script:FileShareAutoGrowLogicApps
}

function Register-AVDMFFileShareAutoGrowLogicApp {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountResourceId,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $TargetFreeSpaceGB,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $Enabled = $true,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [bool] $AllowShrink = $true,



        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )

    $storageAccountName = $StorageAccountResourceId | Split-Path -Leaf
    $resourceName = New-AVDMFResourceName -ResourceType 'LogicApp' -ParentName $StorageAccountName -NameSuffix "FileShareAutoGrow"

    $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Logic/workflows/$resourceName"


    $script:FileShareAutoGrowLogicApps[$resourceName] = [PSCustomObject]@{
        PSTypeName               = 'AVDMF.Storage.FileShareAutoGrowLogicApp'
        ResourceGroupName        = $resourceGroupName
        ResourceID               = $resourceID
        Name                     = $ResourceName
        StorageAccountResourceId = $StorageAccountResourceId
        TargetFreeSpaceGB        = $TargetFreeSpaceGB
        Enabled                  = $Enabled
        AllowShrink              = $AllowShrink
        Tags                     = $Tags
    }

}



function Get-AVDMFFileShareQuotaLogicApp {
    $script:FileShareQuotaLogicApps
}



function Get-AVDMFFileShare {
    $script:FileShares
}

function Register-AVDMFFileShare {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $Name,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $StorageAccountName,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string] $ResourceGroupName
    )
    process {
        $script:FileShares[$Name] = [PSCustomObject]@{
            PSTypeName         = 'AVDMF.Storage.FileShare'
            ResourceName       = $Name
            ResourceGroupName  = $resourceGroupName
            StorageAccountName = $StorageAccountName
        }
    }
}



function Get-AVDMFPrivateLink {
    $script:PrivateLinks
}

function Register-AVDMFPrivateLink {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ResourceGroupName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $StorageAccountID,

        [PSCustomObject] $Tags = [PSCustomObject]@{}

    )
    process {
        $SubnetId = $script:subnets[($script:subnets.keys | Where-Object { $_ -like 'PrivateLinks*' })].ResourceId

        $resourceName = New-AVDMFResourceName -ResourceType 'PrivateLink' -ParentName $StorageAccountName

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/privateEndpoints/$ResourceName"

        $script:PrivateLinks[$resourceName]= [PSCustomObject]@{
            PSTypeName        = 'AVDMF.Storage.PrivateLink'
            ResourceGroupName = $resourceGroupName
            ResourceID        = $resourceID

            StorageAccountID  = $StorageAccountID
            SubnetID          = $SubnetId

            Tags = $Tags

        }
    }
}



function Get-AVDMFStorageAccount {
    $script:StorageAccounts
}

function Register-AVDMFStorageAccount {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $AccessLevel,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $accountType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $HostPoolType,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $Kind,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $ReferenceName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [int] $shareSoftDeleteRetentionDays,

        [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )]
        [string] $UniqueNameString = "",

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $DirectoryServiceOptions,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $DomainName,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $DomainGuid,

        [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )]
        [string] $DefaultSharePermission,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [PSCustomObject] $FileShareAutoGrow,

        [PSCustomObject] $Tags = [PSCustomObject]@{}
    )
    process {

        $ResourceName = New-AVDMFResourceName -ResourceType 'StorageAccount' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -UniqueNameString $UniqueNameString

        $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Storage' -AccessLevel 'All' -HostPoolType 'All' -InstanceNumber 1
        Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Storage'
        # At the moment we do not have a reason for multiple storage RGs.

        $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Storage/storageAccounts/$ResourceName"

        $script:StorageAccounts[$ReferenceName] = [PSCustomObject]@{
            PSTypeName              = 'AVDMF.Storage.StorageAccount'
            ResourceGroupName       = $resourceGroupName
            ResourceID              = $resourceID
            Name                    = $ResourceName
            ReferenceName           = $ReferenceName
            AccountType             = $accountType
            Kind                    = $Kind
            SoftDeleteDays          = $ShareSoftDeleteRetentionDays
            DirectoryServiceOptions = $DirectoryServiceOptions
            DomainName              = $DomainName
            DomainGuid              = $DomainGuid
            DefaultSharePermission  = $DefaultSharePermission
            Tags                    = $Tags
        }

        #register Private Link
        Register-AVDMFPrivateLink -ResourceGroupName $resourceGroupName -StorageAccountName $ResourceName -StorageAccountID $resourceID

        # register Auto Grow Logic App
        if($null -ne $FileShareAutoGrow.Enabled){
            Register-AVDMFFileShareAutoGrowLogicApp -ResourceGroupName $resourceGroupName -StorageAccountResourceId $resourceID -TargetFreeSpaceGB $FileShareAutoGrow.TargetFreeSpaceGB -Enabled $FileShareAutoGrow.Enabled -AllowShrink $FileShareAutoGrow.AllowShrink
        }

    }
}



<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'AVDManagementFramework.ScriptBlockName' -Scriptblock {
 
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "AVDManagementFramework.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name AVDManagementFramework.alcohol
#>


New-PSFLicense -Product 'AVDManagementFramework' -Manufacturer 'wmoselhy' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2021-04-26") -Text @"
Copyright (c) 2021 wmoselhy
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@


# General Settings
$script:WVDConfigurationLoaded = $false
$script:NameMappings = @{}

# Resource Groups
$script:ResourceGroups = @{}

# Network
$script:VirtualNetworks = @{}
$script:Subnets = @{}
$Script:AddressSpaces = @()
$script:NetworkSecurityGroups = @{}
$script:RemotePeerings = @{}
$script:RouteTables = @{}

# Storage
$script:StorageAccounts = @{}
$script:FileShares = @{}
$script:PrivateLinks = @{}
$script:FileShareAutoGrowLogicApps = @{}

# DesktopVirtualization
$script:HostPools = @{}
$script:ApplicationGroups = @{}
$script:RemoteAppTemplates = @{}
$script:RemoteApps = @{}
$script:Workspaces = @{}
$script:VMTemplates = @{}
$script:TemplateSpecs = @{} # We have one template spec created per host pool.
$script:SessionHosts = @{}
$script:ReplacementPlanTemplates = @{}
$script:ReplacementPlans = @{}
$script:ScalingPlanTemplates = @{}
$script:ScalingPlanScheduleTemplates = @{}
$script:ScalingPlans = @{}

# Tags
$script:GlobalTags = @{}
#endregion Load compiled code