AzOps.psm1

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

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName AzOps.Import.DoDotSource -Fallback $false
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $script:doDotSource = $true }
if ($AzOps_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 AzOps.Import.IndividualFiles -Fallback $false
if ($AzOps_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
            > . 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)\localized\en-us\*.psd1" -Module 'AzOps' -Language 'en-US'

class AzOpsRoleAssignment {
    [string]$ResourceType
    [string]$Name
    [string]$Id
    [hashtable]$Properties

    AzOpsRoleAssignment($Properties) {
        $this.Properties = [ordered]@{
            DisplayName = $Properties.DisplayName
            PrincipalId = $Properties.ObjectId
            RoleDefinitionName = $Properties.RoleDefinitionName
            ObjectType  = $Properties.ObjectType
            RoleDefinitionId = '/providers/Microsoft.Authorization/RoleDefinitions/{0}' -f $Properties.RoleDefinitionId
        }
        $this.Id = $Properties.RoleAssignmentId
        $this.Name = ($Properties.RoleAssignmentId -split "/")[-1]
        $this.ResourceType = "Microsoft.Authorization/roleAssignments"
    }
}

class AzOpsRoleDefinition {
    [string]$ResourceType
    [string]$Name
    [string]$Id
    [hashtable]$Properties
    
    AzOpsRoleDefinition($Properties) {
        $this.Id = $Properties.AssignableScopes[0] + '/providers/Microsoft.Authorization/roleDefinitions/' + $Properties.Id
        $this.Name = $Properties.Id
        $this.Properties = [ordered]@{
            AssignableScopes = @($Properties.AssignableScopes)
            Description         = $Properties.Description
            Permissions         = @(
                [ordered]@{
                    Actions = @($Properties.Actions)
                    DataActions = @($Properties.DataActions)
                    NotActions = @($Properties.NotActions)
                    NotDataActions = @($Properties.NotDataActions)
                }
            )
            RoleName         = $Properties.Name
        }
        $this.ResourceType = "Microsoft.Authorization/roleDefinitions"
    }
}

class AzOpsScope {

    [string]$Scope
    [string]$Type
    [string]$Name
    [string]$StatePath
    [string]$ManagementGroup
    [string]$ManagementGroupDisplayName
    [string]$Subscription
    [string]$SubscriptionDisplayName
    [string]$ResourceGroup
    [string]$ResourceProvider
    [string]$Resource

    hidden [string]$StateRoot

    #region Internal Regex Helpers
    hidden [regex]$regex_tenant = '/$'
    hidden [regex]$regex_managementgroup = '(?i)^/providers/Microsoft.Management/managementgroups/[^/]+$'
    hidden [regex]$regex_managementgroupExtract = '(?i)^/providers/Microsoft.Management/managementgroups/'

    hidden [regex]$regex_subscription = '(?i)^/subscriptions/[^/]*$'
    hidden [regex]$regex_subscriptionExtract = '(?i)^/subscriptions/'

    hidden [regex]$regex_resourceGroup = '(?i)^/subscriptions/.*/resourcegroups/[^/]*$'
    hidden [regex]$regex_resourceGroupExtract = '(?i)^/subscriptions/.*/resourcegroups/'

    hidden [regex]$regex_managementgroupProvider = '(?i)^/providers/Microsoft.Management/managementgroups/[\s\S]*/providers'
    hidden [regex]$regex_subscriptionProvider = '(?i)^/subscriptions/.*/providers'
    hidden [regex]$regex_resourceGroupProvider = '(?i)^/subscriptions/.*/resourcegroups/[\s\S]*/providers'

    hidden [regex]$regex_managementgroupResource = '(?i)^/providers/Microsoft.Management/managementGroups/[\s\S]*/providers/[\s\S]*/[\s\S]*/'
    hidden [regex]$regex_subscriptionResource = '(?i)^/subscriptions/([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})/providers/[\s\S]*/[\s\S]*/'
    hidden [regex]$regex_resourceGroupResource = '(?i)^/subscriptions/([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})/resourcegroups/[\s\S]*/providers/[\s\S]*/[\s\S]*/'
    #endregion Internal Regex Helpers

    #region Constructors
    AzOpsScope ([string]$Scope, [string]$StateRoot) {

        <#
            .SYNOPSIS
                Creates an AzOpsScope based on the specified resource ID or File System Path
            .DESCRIPTION
                Creates an AzOpsScope based on the specified resource ID or File System Path
            .PARAMETER Scope
                Scope == ResourceID or File System Path
            .INPUTS
                None. You cannot pipe objects to Add-Extension.
            .OUTPUTS
                System.String. Add-Extension returns a string with the extension or file name.
            .EXAMPLE
                New-AzOpsScope -Scope "/providers/Microsoft.Management/managementGroups/3fc1081d-6105-4e19-b60c-1ec1252cf560"
                Creates an AzOpsScope based on the specified resource ID
        #>


        Write-PSFMessage -Level Verbose -String 'AzOpsScope.Constructor' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps
        $this.StateRoot = $StateRoot
        if (Test-Path -Path $scope) {
            if ((Get-Item $scope).GetType().ToString() -eq 'System.IO.FileInfo') {
                #Strong confidence based on content - file
                Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps
                $this.InitializeMemberVariablesFromFile($Scope)
            }
            else {
                # Weak confidence based on metadata at scope - directory
                Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps
                $this.InitializeMemberVariablesFromDirectory($Scope)
            }
        }
        else {
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariables' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps
            $this.InitializeMemberVariables($Scope)
        }
    }

    # Overloaded constructors - repeat member assignments in each constructor definition
    #AzOpsScope ([System.IO.DirectoryInfo]$Path, [string]$StateRoot) {
    hidden [void] InitializeMemberVariablesFromDirectory([System.IO.DirectoryInfo]$Path) {

        $managementGroupFileName = "microsoft.management_managementGroups-*$(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')"
        $subscriptionFileName = "microsoft.subscription_subscriptions-*$(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')"
        $resourceGroupFileName = "microsoft.resources_resourceGroups-*$(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')"

        if ($Path.FullName -eq (Get-Item $this.StateRoot).FullName) {
            # Root tenant path
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory.RootTenant' -StringValues $Path -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps
            $this.InitializeMemberVariables("/")
            return
        }
        # Always look into AutoGeneratedTemplateFolderPath folder regardless of path specified
        if ($Path.FullName -notlike "*$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") {
            $Path = Join-Path $Path -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')"
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory.AutoGeneratedFolderPath' -StringValues $Path -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps
        }

        if ($managementGroupScopeFile = (Get-ChildItem -Force -Path $Path -File | Where-Object Name -like $managementGroupFileName)) {
            [string] $managementGroupID = $managementGroupScopeFile.Name.Replace('microsoft.management_managementgroups-', '').Replace('.parameters', '').Replace($(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'), '')
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.Input.FromFileName.ManagementGroup' -StringValues $managementGroupID -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps
            $this.InitializeMemberVariables("/providers/Microsoft.Management/managementGroups/$managementGroupID")
        }
        elseif ($subscriptionScopeFileName = (Get-ChildItem -Force -Path $Path -File | Where-Object Name -like $subscriptionFileName)) {
            [string] $subscriptionID = $subscriptionScopeFileName.Name.Replace('microsoft.subscription_subscriptions-', '').Replace('.parameters', '').Replace($(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'), '')
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.Input.FromFileName.Subscription' -StringValues $subscriptionID -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps
            $this.InitializeMemberVariables("/subscriptions/$subscriptionID")
        }
        elseif ((Get-ChildItem -Force -Path $Path -File | Where-Object Name -like $resourceGroupFileName) -or
            ((Get-ChildItem -Force -Path $Path.Parent -File | Where-Object Name -like $subscriptionFileName))
        ) {
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory.ParentSubscription' -StringValues $Path.Parent -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps
            $parent = (New-AzOpsScope -Path $Path.Parent)
            $this.InitializeMemberVariables($("/subscriptions/{0}/resourceGroups/{1}" -f $parent.Subscription, $Path.Name))
        }
        else {
            #Error
            Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.Input.BadData.UnknownType' -StringValues $Path -FunctionName AzOpsScope -ModuleName AzOps
            throw "Invalid File Structure! Cannot find Management Group / Subscription / Resource Group files in $Path!"
        }
    }

    #AzOpsScope ([System.IO.FileInfo]$Path, [string]$StateRoot) {
    hidden [void] InitializeMemberVariablesFromFile([System.IO.FileInfo]$Path) {
        if (-not $Path.Exists) { throw 'Invalid Input!' }

        if ($Path.Extension -ne '.json') {
            # Try to determine based on directory
            Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.NotJson' -StringValues $Path -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
            $this.InitializeMemberVariablesFromDirectory($Path.Directory)
            return
        }
        else {
            $resourcePath = Get-Content $Path | ConvertFrom-Json -AsHashtable

            if (!$resourcePath) {
                # Empty file with .json is not valid JSON file. Empty Json should've minimum file content '{}'
                # However, due to bug that is combination of Get-Content and ConvertFrom-Json when empty file with .json (that is valid file but not valid Json),
                # switch statement is failing to handle $null value unless assigned explicitly.
                $resourcePath = $null
            }

            switch ($resourcePath) {
                { $_.parameters.input.value.Keys -contains "ResourceId" } {
                    # Parameter Files - resource from parameters file
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceId' -StringValues $($resourcePath.parameters.input.value.ResourceId) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $this.InitializeMemberVariables($resourcePath.parameters.input.value.ResourceId)
                    break
                }
                { $_.parameters.input.value.Keys -contains "Id" } {
                    # Parameter Files - ManagementGroup and Subscription
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.Id' -StringValues $($resourcePath.parameters.input.value.Id) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $this.InitializeMemberVariables($resourcePath.parameters.input.value.Id)
                    break
                }
                { $_.parameters.input.value.Keys -contains "Type" } {
                    # Parameter Files - Determine Resource Type and Name (Management group)
                    # Management group resource id do contain '/provider'
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.Type' -StringValues ("$($resourcePath.parameters.input.value.Type)/$($resourcePath.parameters.input.value.Name)") -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $this.InitializeMemberVariables("$($resourcePath.parameters.input.value.Type)/$($resourcePath.parameters.input.value.Name)")
                    break
                }
                { $_.parameters.input.value.Keys -contains "ResourceType" } {
                    # Parameter Files - Determine Resource Type and Name (Any ResourceType except management group)
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceType' -StringValues ($resourcePath.parameters.input.value.ResourceType) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $currentScope = New-AzOpsScope -Path ($Path.Directory)

                    # Creating Resource Id based on current scope, resource Type and Name of the resource
                    $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($resourcePath.parameters.input.value.ResourceType)/$($resourcePath.parameters.input.value.Name)")
                    break
                }
                { $_.resources -and
                    $_.resources[0].type -eq 'Microsoft.Management/managementGroups' } {
                    # Template - Management Group
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.managementgroups' -StringValues ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $currentScope = New-AzOpsScope -Path ($Path.Directory)
                    $this.InitializeMemberVariables("$($currentScope.scope)")
                    break
                }
                { $_.resources -and
                    $_.resources[0].type -eq 'Microsoft.Management/managementGroups/subscriptions' } {
                    # Template - Subscription
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.subscriptions' -StringValues ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $currentScope = New-AzOpsScope -Path ($Path.Directory.Parent)
                    $this.InitializeMemberVariables("$($currentScope.scope)")
                    break
                }
                { $_.resources -and
                    $_.resources[0].type -eq 'Microsoft.Resources/resourceGroups' } {
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.resourceGroups' -StringValues ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $currentScope = New-AzOpsScope -Path ($Path.Directory)
                    $this.InitializeMemberVariables("$($currentScope.scope)")
                    break
                }
                { $_.resources } {
                    # Template - 1st resource
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.resource' -StringValues ($_.resources[0].type), ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps
                    $currentScope = New-AzOpsScope -Path ($Path.Directory)
                    $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($_.resources[0].type)/$($_.resources[0].name)")
                    break
                }
                Default {
                    Write-PSFMessage -Level Warning  -String 'AzOpsScope.Input.BadData.TemplateParameterFile' -StringValues $Path -FunctionName AzOpsScope -ModuleName AzOps
                    Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory' -StringValues $Path -FunctionName AzOpsScope -ModuleName AzOps
                    $this.InitializeMemberVariablesFromDirectory($Path.Directory)
                }
            }
        }
    }
    #endregion Constructors

    hidden [void] InitializeMemberVariables([string]$Scope) {
        Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariables.Start' -StringValues ($scope) -FunctionName InitializeMemberVariables -ModuleName AzOps
        $this.Scope = $Scope

        if ($this.IsResource()) {
            $this.Type = "resource"
            $this.Name = $this.IsResource()
            $this.Subscription = $this.GetSubscription()
            $this.SubscriptionDisplayName = $this.GetSubscriptionDisplayName()
            $this.ManagementGroup = $this.GetManagementGroup()
            $this.ManagementGroupDisplayName = $this.GetManagementGroupName()
            $this.ResourceGroup = $this.GetResourceGroup()
            $this.ResourceProvider = $this.IsResourceProvider()
            $this.Resource = $this.GetResource()
            if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) {
                $this.StatePath = $this.GetAzOpsResourcePath() + ".json"
            }
            else {
                if ( (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') -notcontains 'parameters.json' -and
                    ("$($this.ResourceProvider)/$($this.Resource)" -eq 'Microsoft.Authorization/policyDefinitions')
                ) {
                    $this.StatePath = ($this.GetAzOpsResourcePath() + '.parameters' + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))
                }
                else {
                    $this.StatePath = ($this.GetAzOpsResourcePath() + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))
                }
            }
        }
        elseif ($this.IsResourceGroup()) {
            $this.Type = "resourcegroups"
            $this.ResourceProvider = "Microsoft.Resources"
            $this.Resource = "resourceGroups"
            $this.Name = $this.IsResourceGroup()
            $this.Subscription = $this.GetSubscription()
            $this.SubscriptionDisplayName = $this.GetSubscriptionDisplayName()
            $this.ManagementGroup = $this.GetManagementGroup()
            $this.ManagementGroupDisplayName = $this.GetManagementGroupName()
            $this.ResourceGroup = $this.GetResourceGroup()
            if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) {
                $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.resources_resourcegroups-$($this.ResourceGroup).json").ToLower() )
            }
            else {
                $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.resources_resourcegroups-$($this.ResourceGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')).ToLower())
            }
        }
        elseif ($this.IsSubscription()) {
            $this.Type = "subscriptions"
            $this.ResourceProvider = "Microsoft.Management"
            $this.Resource = "managementGroups/subscriptions"
            $this.Name = $this.IsSubscription()
            $this.Subscription = $this.GetSubscription()
            $this.SubscriptionDisplayName = $this.GetSubscriptionDisplayName()
            if ($script:AzOpsAzManagementGroup) {
                $this.ManagementGroup = $this.GetManagementGroup()
                $this.ManagementGroupDisplayName = $this.GetManagementGroupName()
            }
            if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) {
                $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription).json").ToLower())
            }
            else {
                $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower())
            }

        }
        elseif ($this.IsManagementGroup()) {
            $this.Type = "managementGroups"
            $this.ResourceProvider = "Microsoft.Management"
            $this.Resource = "managementGroups"
            $this.Name = $this.GetManagementGroup()
            $this.ManagementGroup = ($this.GetManagementGroup()).Trim()
            $this.ManagementGroupDisplayName = ($this.GetManagementGroupName()).Trim()
            if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) {
                $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup).json").ToLower())
            }
            else {
                $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower())
            }
        }
        elseif ($this.IsRoot()) {
            $this.Type = "root"
            $this.Name = "/"
            $this.StatePath = $this.StateRoot.ToLower()
        }
        Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariables.End' -StringValues ($scope) -FunctionName InitializeMemberVariables -ModuleName AzOps
    }
    #endregion Initializers

    [String] ToString() {
        return $this.Scope
    }

    #region Validators
    [bool] IsRoot() {
        if (($this.Scope -match $this.regex_tenant)) {
            return $true
        }
        return $false
    }
    [bool] IsManagementGroup() {
        if (($this.Scope -match $this.regex_managementgroup)) {
            return $true
        }
        return $false
    }
    [string] IsSubscription() {
        if (($this.Scope -match $this.regex_subscription)) {
            return ($this.Scope.Split('/')[2])
        }
        return $null
    }
    [string] IsResourceGroup() {
        if (($this.Scope -match $this.regex_resourceGroup)) {
            return ($this.Scope.Split('/')[4])
        }
        return $null
    }
    [string] IsResourceProvider() {

        if ($this.Scope -match $this.regex_managementgroupProvider) {
            return (($this.regex_managementgroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1]
        }
        if ($this.Scope -match $this.regex_subscriptionProvider) {
            return (($this.regex_subscriptionProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1]
        }
        if ($this.Scope -match $this.regex_resourceGroupProvider) {
            return (($this.regex_resourceGroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1]
        }

        return $null
    }
    [string] IsResource() {

        if ($this.Scope -match $this.regex_managementgroupResource) {
            return ($this.regex_managementgroupResource.Split($this.Scope) | Select-Object -last 1)
        }
        if ($this.Scope -match $this.regex_subscriptionResource) {
            return ($this.regex_subscriptionResource.Split($this.Scope) | Select-Object -last 1)
        }
        if ($this.Scope -match $this.regex_resourceGroupResource) {
            return ($this.regex_resourceGroupResource.Split($this.Scope) | Select-Object -last 1)
        }
        return $null
    }
    #endregion Validators

    #region Data Accessors
    <#
        Should Return Management Group Name
    #>

    [string] GetManagementGroup() {

        if ($this.GetManagementGroupName()) {
            foreach ($mgmt in $script:AzOpsAzManagementGroup) {
                if ($mgmt.DisplayName -eq $this.GetManagementGroupName()) {
                    return $mgmt.Name
                }
            }
        }
        if ($this.Subscription) {
            foreach ($mgmt in $script:AzOpsAzManagementGroup) {
                foreach ($child in $mgmt.Children) {
                    if ($child.DisplayName -eq $this.subscriptionDisplayName) {
                        return $mgmt.Name
                    }
                }
            }
        }
        return $null
    }

    [string] GetAzOpsManagementGroupPath([string]$managementgroupName) {
        if ($groupObject = $script:AzOpsAzManagementGroup | Where-Object Name -eq $managementgroupName) {
            $parentMgName = $groupObject.parentId -split "/" | Select-Object -Last 1
            $parentObject = $script:AzOpsAzManagementGroup | Where-Object Name -eq $parentMgName
            if ($groupObject.parentId -and $parentObject) {
                $parentPath = $this.GetAzOpsManagementGroupPath($parentMgName)
                $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name
                return Join-Path $parentPath -ChildPath ($childPath.ToLower())
            }
            else {
                $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name
                return Join-Path $this.StateRoot -ChildPath ($childPath.ToLower())
            }
        }
        else {
            Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' -StringValues $managementgroupName -FunctionName AzOpsScope -ModuleName AzOps
            throw "Management Group not found: $managementgroupName"
        }
    }

    <#
        Should Return Management Group Display Name
    #>

    [string] GetManagementGroupName() {
        if ($this.Scope -match $this.regex_managementgroupExtract) {
            $mgId = $this.Scope -split $this.regex_managementgroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1

            if ($mgId) {
                $mgDisplayName = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $mgId).DisplayName
                if ($mgDisplayName) {
                    #Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroupName.Found.Azure' -StringValues $mgDisplayName -FunctionName AzOpsScope -ModuleName AzOps
                    return $mgDisplayName
                }
                else {
                    Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroupName.NotFound' -StringValues $mgId -FunctionName AzOpsScope -ModuleName AzOps
                    return $mgId
                }
            }
        }
        if ($this.Subscription) {
            foreach ($managementGroup in $script:AzOpsAzManagementGroup) {
                foreach ($child in $managementGroup.Children) {
                    if ($child.DisplayName -eq $this.subscriptionDisplayName) {
                        return $managementGroup.DisplayName
                    }
                }
            }
        }
        return $null
    }
    [string] GetAzOpsSubscriptionPath() {
        $childpath = "{0} ({1})" -f $this.SubscriptionDisplayName, $this.Subscription
        if ($script:AzOpsAzManagementGroup) {
            return (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ($childpath).ToLower())
        }
        else {
            return (Join-Path $this.StateRoot -ChildPath ($childpath).ToLower())
        }
    }
    [string] GetAzOpsResourceGroupPath() {
        return (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ($this.ResourceGroup).ToLower())
    }
    [string] GetSubscription() {
        if ($this.Scope -match $this.regex_subscriptionExtract) {
            $subId = $this.Scope -split $this.regex_subscriptionExtract -split '/' | Where-Object { $_ } | Select-Object -First 1
            $sub = $script:AzOpsSubscriptions | Where-Object subscriptionId -eq $subId
            if ($sub) {
                Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscription.Found' -StringValues $sub.Id -FunctionName AzOpsScope -ModuleName AzOps
                return $sub.subscriptionId
            }
            else {
                Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscription.NotFound' -StringValues $subId -FunctionName AzOpsScope -ModuleName AzOps
                return $subId
            }
        }
        return $null
    }
    [string] GetSubscriptionDisplayName() {
        if ($this.Scope -match $this.regex_subscriptionExtract) {

            $subId = $this.Scope -split $this.regex_subscriptionExtract -split '/' | Where-Object { $_ } | Select-Object -First 1
            $sub = $script:AzOpsSubscriptions | Where-Object subscriptionId -eq $subId
            if ($sub) {
                Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscriptionDisplayName.Found' -StringValues $sub.displayName -FunctionName AzOpsScope -ModuleName AzOps
                return $sub.displayName
            }
            else {
                Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscriptionDisplayName.NotFound' -StringValues $subId -FunctionName AzOpsScope -ModuleName AzOps
                return $subId
            }
        }
        return $null
    }
    [string] GetResourceGroup() {
        if ($this.Scope -match $this.regex_resourceGroupExtract) {
            return ($this.Scope -split $this.regex_resourceGroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1)
        }
        return $null
    }
    [string] GetResource() {

        if ($this.Scope -match $this.regex_managementgroupProvider) {
            return (($this.regex_managementgroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[2]
        }
        if ($this.Scope -match $this.regex_subscriptionProvider) {
            return (($this.regex_subscriptionProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[2]
        }
        if ($this.Scope -match $this.regex_resourceGroupProvider) {
            return (($this.regex_resourceGroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[2]
        }
        return $null
    }

    [string] GetAzOpsResourcePath() {

        Write-PSFMessage -Level Debug -String 'AzOpsScope.GetAzOpsResourcePath.Retrieving' -StringValues $this.Scope -FunctionName AzOpsScope -ModuleName AzOps
        if ($this.Scope -match $this.regex_resourceGroupResource) {
            $rgpath = $this.GetAzOpsResourceGroupPath()
            return (Join-Path (Join-Path $rgpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $this.Name).ToLower())
        }
        elseif ($this.Scope -match $this.regex_subscriptionResource) {
            $subpath = $this.GetAzOpsSubscriptionPath()
            return (Join-Path (Join-Path $subpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $this.Name).ToLower())
        }
        elseif ($this.Scope -match $this.regex_managementgroupResource) {
            $mgmtPath = $this.GetAzOpsManagementGroupPath($this.ManagementGroup)
            return (Join-Path (Join-Path $mgmtPath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $this.Name).ToLower())
        }
        Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.GetAzOpsResourcePath.NotFound' -StringValues $this.Scope -FunctionName AzOpsScope -ModuleName AzOps
        throw "Unable to determine Resource Scope for: $($this.Scope)"
    }
    #endregion Data Accessors
}

function Assert-AzOpsBicepDependency {

    <#
        .SYNOPSIS
            Asserts that - if bicep is installed and in current path
        .DESCRIPTION
            Asserts that - if bicep is installed and in current path
        .PARAMETER Cmdlet
            The $PSCmdlet variable of the calling command.
        .EXAMPLE
            > Assert-AzOpsBicepDependency -Cmdlet $PSCmdlet
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        $Cmdlet
    )

    process {
        Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsBicepDependency.Validating'

        $result = (Invoke-AzOpsNativeCommand -ScriptBlock { bicep --version } -IgnoreExitcode)
        $installed = $result -as [bool]

        if ($installed) {
            Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsBicepDependency.Success'
        }
        else {
            $exception = [System.InvalidOperationException]::new('Unable to locate bicep installation')
            $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
            Write-PSFMessage -Level Warning -String 'Assert-AzOpsBicepDependency.NotFound' -Tag error
            $Cmdlet.ThrowTerminatingError($errorRecord)
        }

    }

}

function Assert-AzOpsInitialization {

    <#
        .SYNOPSIS
            Asserts AzOps has been correctly prepare for execution.
        .DESCRIPTION
            Asserts AzOps has been correctly prepare for execution.
            This boils down to Initialize-AzOpsEnvironment having been executed successfully.
        .PARAMETER Cmdlet
            The $PSCmdlet variable of the calling command.
        .PARAMETER StatePath
            Path to where the AzOps processing state / repository is located at.
        .EXAMPLE
            > Assert-AzOpsInitialization -Cmdlet $PSCmdlet -Statepath $StatePath
            Asserts AzOps has been correctly prepare for execution.
    #>


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

        [Parameter(Mandatory = $true)]
        [string]
        $StatePath
    )

    begin {
        $strings = Get-PSFLocalizedString -Module AzOps
        $invalidPathPattern = [System.IO.Path]::GetInvalidPathChars() -replace '\|', '\|' -join "|"
    }

    process {
        $stateGood = $StatePath -and $StatePath -notmatch $invalidPathPattern
        if (-not $stateGood) {
            Write-PSFMessage -Level Warning -String 'Assert-AzOpsInitialization.StateError' -Tag error
            $exception = [System.InvalidOperationException]::new($strings.'Assert-AzOpsInitialization.StateError')
            $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "BadData", 'InvalidData', $null)
        }
        $cacheBuilt = $script:AzOpsSubscriptions -or $script:AzOpsAzManagementGroup
        if (-not $cacheBuilt) {
            Write-PSFMessage -Level Warning -String 'Assert-AzOpsInitialization.NoCache' -Tag error
            $exception = [System.InvalidOperationException]::new($strings.'Assert-AzOpsInitialization.NoCache')
            $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "NoCache", 'InvalidData', $null)
        }

        if (-not $stateGood -or -not $cacheBuilt) {
            $Cmdlet.ThrowTerminatingError($errorRecord)
        }
    }

}

function Assert-AzOpsJqDependency {

    <#
        .SYNOPSIS
            Asserts that - if jq is installed and in current path
        .DESCRIPTION
            Asserts that - if jq is installed and in current path
        .PARAMETER Cmdlet
            The $PSCmdlet variable of the calling command.
        .EXAMPLE
            > Assert-AzOpsJqDependency -Cmdlet $PSCmdlet
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        $Cmdlet
    )

    process {
        Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsJqDependency.Validating'

        $result = (Invoke-AzOpsNativeCommand -ScriptBlock { jq --version } -IgnoreExitcode)
        $installed = $result -as [bool]

        if ($installed) {
            [double]$version = ($result).Split("-")[1]
            if ($version -ge 1.6) {
                Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsJqDependency.Success'
                return
            }
            else {
                $exception = [System.InvalidOperationException]::new('Unsupported version of jq installed. Please update to a minimum jq version of 1.6')
                $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
                Write-PSFMessage -Level Warning -String 'Assert-AzOpsJqDependency.Failed' -Tag error
                $Cmdlet.ThrowTerminatingError($errorRecord)
            }
        }

        $exception = [System.InvalidOperationException]::new('Unable to locate jq installation')
        $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
        Write-PSFMessage -Level Warning -String 'Assert-AzOpsJqDependency.Failed' -Tag error
        $Cmdlet.ThrowTerminatingError($errorRecord)
    }

}

function Assert-AzOpsWindowsLongPath {

    <#
        .SYNOPSIS
            Asserts that - if on windows - long paths have been enabled.
        .DESCRIPTION
            Asserts that - if on windows - long paths have been enabled.
        .PARAMETER Cmdlet
            The $PSCmdlet variable of the calling command.
        .EXAMPLE
            > Assert-AzOpsWindowsLongPath -Cmdlet $PSCmdlet
            Asserts that - if on windows - long paths have been enabled.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $Cmdlet
    )

    process {
        if (-not $IsWindows) {
            return
        }

        Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsWindowsLongPath.Validating'
        $hasRegKey = 1 -eq (Get-ItemPropertyValue -Path HKLM:SYSTEM\CurrentControlSet\Control\FileSystem -Name LongPathsEnabled)
        $hasGitConfig = (Invoke-AzOpsNativeCommand -ScriptBlock { git config --system -l } -IgnoreExitcode | Select-String 'core.longpaths=true') -as [bool]

        if ($hasGitConfig -and $hasRegKey) {
            return
        }
        if (-not $hasRegKey) {
            Write-PSFMessage -Level Warning -String 'Assert-AzOpsWindowsLongPath.No.Registry'
        }
        if (-not $hasGitConfig) {
            Write-PSFMessage -Level Warning -String 'Assert-AzOpsWindowsLongPath.No.GitCfg'
        }

        $exception = [System.InvalidOperationException]::new('Windows not configured for long paths. Please follow instructions for "Enabling long paths on Windows" on https://aka.ms/es/quickstart.')
        $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
        Write-PSFMessage -Level Warning -String 'Assert-AzOpsWindowsLongPath.Failed' -Tag error
        $Cmdlet.ThrowTerminatingError($errorRecord)
    }

}

function ConvertTo-AzOpsState {

    <#
        .SYNOPSIS
            The cmdlet converts Azure resources (Resources/ResourceGroups/Policy/PolicySet/PolicyAssignments/RoleAssignment/Definition) to the AzOps state format and exports them to the file structure.
        .DESCRIPTION
            The cmdlet converts Azure resources (Resources/ResourceGroups/Policy/PolicySet/PolicyAssignments/RoleAssignment/Definition) to the AzOps state format and exports them to the file structure.
            It is normally executed and orchestrated through the Initialize-AzOpsRepository cmdlet. As most of the AzOps-cmdlets, it is dependant on the AzOpsAzManagementGroup and AzOpsSubscriptions variables.
            Cmdlet will look into jq filter is template directory for the specific one before using the generic one at the root of the module
        .PARAMETER Resource
            Object with resource as input
        .PARAMETER ExportPath
            ExportPath is used if resource needs to be exported to other path than the AzOpsScope path
        .PARAMETER ReturnObject
            Used if to return object in pipeline instead of exporting file
        .PARAMETER ExportRawTemplate
            Used in cases you want to return the template without the custom parameters json schema
        .PARAMETER StatePath
            The root path to where the entire state is being built in.
        .EXAMPLE
            Initialize-AzOpsEnvironment
            $policy = Get-AzPolicyDefinition -Custom | Select-Object -Last 1
            ConvertTo-AzOpsState -Resource $policy
            Export custom policy definition to the AzOps StatePath
        .EXAMPLE
            Initialize-AzOpsEnvironment
            $policy = Get-AzPolicyDefinition -Custom | Select-Object -Last 1
            ConvertTo-AzOpsState -Resource $policy -ReturnObject
            Name Value
            ---- -----
            $schema http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#
            contentVersion 1.0.0.0
            parameters {input}
            Serialize custom policy definition to the AzOps format, return object instead of export file
        .INPUTS
            Resource
        .OUTPUTS
            Resource in AzOpsState json format or object returned as [PSCustomObject] depending on parameters used
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Alias('MG', 'Role', 'Assignment', 'CustomObject', 'ResourceGroup')]
        $Resource,

        [string]
        $ExportPath,

        [switch]
        $ReturnObject,

        [switch]
        $ExportRawTemplate,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $StatePath,

        [string]
        $JqTemplatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.JqTemplatePath')
    )

    begin {
        Write-PSFMessage -Level Debug -String 'ConvertTo-AzOpsState.Starting'
    }

    process {
        Write-PSFMessage -Level Debug -String 'ConvertTo-AzOpsState.Processing' -StringValues $Resource

        if (-not $ExportPath) {
            if ($Resource.Id) {
                $objectFilePath = (New-AzOpsScope -scope $Resource.id -StatePath $StatePath).statepath
            }
            elseif ($Resource.ResourceId) {
                $objectFilePath = (New-AzOpsScope -scope $Resource.ResourceId -StatePath $StatePath).statepath
            }
            else {
                Write-PSFMessage -Level Error -String "ConvertTo-AzOpsState.NoExportPath" -StringValues $Resource.GetType()
            }
        }
        else {
            $objectFilePath = $ExportPath
        }
        # Create folder structure if it doesn't exist
        if (-not (Test-Path -Path $objectFilePath)) {
            Write-PSFMessage -String 'ConvertTo-AzOpsState.File.Create' -StringValues $objectFilePath
            $null = New-Item -Path $objectFilePath -ItemType "file" -Force
        }
        else {
            Write-PSFMessage -String 'ConvertTo-AzOpsState.File.UseExisting' -StringValues $objectFilePath
        }

        # if the export file path ends with parameter
        $generateTemplateParameter = $objectFilePath.EndsWith('.parameters.json') ? $true : $false
        Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplateParameter' -StringValues "$generateTemplateParameter" -FunctionName 'ConvertTo-AzOpsState'

        $resourceType = $null
        switch ($Resource) {
            { $_.ResourceType } {
                Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.ResourceType' -StringValues "$($Resource.ResourceType)" -FunctionName 'ConvertTo-AzOpsState'
                $resourceType = $_.ResourceType
                break
            }
            # Management Groups
            { $_ -is [Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroup] -or
                $_ -is [Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroupChildInfo] } {
                Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState'
                if ($_.Type -eq "/subscriptions") {
                    $resourceType = 'Microsoft.Management/managementGroups/subscriptions'
                    break
                }
                else {
                    $resourceType = 'Microsoft.Management/managementGroups'
                    break
                }
            }
            # Subscriptions
            { $_ -is [Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription] } {
                Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState'
                $resourceType = 'Microsoft.Subscription/subscriptions'
                break
            }
            # Resource Groups
            { $_ -is [Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResourceGroup] } {
                Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState'
                $resourceType = 'Microsoft.Resources/resourceGroups'
                break
            }
            # Resources - Controlled group for raw objects
            { $_ -is [Microsoft.Azure.Commands.Profile.Models.PSAzureTenant] } {
                Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues  "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState'
                break
            }
            Default {
                Write-PSFMessage -Level Warning -String 'ConvertTo-AzOpsState.ObjectType.Resolved.Generic'  -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState'
                break
            }
        }
        if ($resourceType) {
            $providerNamespace = ($resourceType -split '/' | Select-Object -First 1)
            Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ProviderNamespace' -StringValues $providerNamespace -FunctionName 'ConvertTo-AzOpsState'

            if (($resourceType -split '/').Count -eq 2) {
                $resourceTypeName = (($resourceType -split '/', 2) | Select-Object -Last 1)
                Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceTypeName' -StringValues $resourceTypeName -FunctionName 'ConvertTo-AzOpsState'

                $resourceApiTypeName = (($resourceType -split '/', 2) | Select-Object -Last 1)
                Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceApiTypeName' -StringValues $resourceApiTypeName -FunctionName 'ConvertTo-AzOpsState'
            }

            if (($resourceType -split '/').Count -eq 3) {
                $resourceTypeName = ((($resourceType -split '/', 3) | Select-Object -Last 2) -join '/')
                Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceTypeName' -StringValues $resourceTypeName -FunctionName 'ConvertTo-AzOpsState'

                $resourceApiTypeName = (($resourceType -split '/', 3) | Select-Object -Index 1)
                Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceApiTypeName' -StringValues $resourceApiTypeName -FunctionName 'ConvertTo-AzOpsState'
            }

            $jqRemoveTemplate = (
                (Test-Path (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.jq"))) ?
                (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.jq")):
                (Join-Path $JqTemplatePath -ChildPath "generic.jq")
            )
            Write-PSFMessage -String 'ConvertTo-AzOpsState.Jq.Remove' -StringValues $jqRemoveTemplate -FunctionName 'ConvertTo-AzOpsState'
            # If we were able to determine resourceType, apply filter and write template or template parameter files based on output filename.
            $object = $Resource | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqRemoveTemplate | ConvertFrom-Json

            if ($ReturnObject) {
                return $object
            }
            else {
                if ($generateTemplateParameter) {
                    #region Generating Template Parameter
                    Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplateParameter' -FunctionName 'ConvertTo-AzOpsState'
                    $jqJsonTemplate = (Test-Path (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.parameters.jq"))) ?
                    (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.parameters.jq")):
                    (Join-Path $JqTemplatePath -ChildPath "template.parameters.jq")

                    Write-PSFMessage -String 'ConvertTo-AzOpsState.Jq.Template' -StringValues $jqJsonTemplate -FunctionName 'ConvertTo-AzOpsState'
                    $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqJsonTemplate | ConvertFrom-Json)
                    #endregion
                }
                else {
                    #region Generating Template
                    Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate' -StringValues "$true"  -FunctionName 'ConvertTo-AzOpsState'
                    $jqJsonTemplate = (Test-Path (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.template.jq"))) ?
                    (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.template.jq")):
                    (Join-Path $JqTemplatePath -ChildPath "template.jq")

                    Write-PSFMessage -String 'ConvertTo-AzOpsState.Jq.Template' -StringValues $jqJsonTemplate -FunctionName 'ConvertTo-AzOpsState'
                    $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqJsonTemplate | ConvertFrom-Json)
                    #endregion

                    #region Replace Resource Type and API Version
                    if (
                        ($Script:AzOpsResourceProvider | Where-Object { $_.ProviderNamespace -eq $providerNamespace }) -and
                        (($Script:AzOpsResourceProvider | Where-Object { $_.ProviderNamespace -eq $providerNamespace }).ResourceTypes | Where-Object { $_.ResourceTypeName -eq $resourceApiTypeName })
                    ) {
                        $apiVersions = (($Script:AzOpsResourceProvider | Where-Object { $_.ProviderNamespace -eq $providerNamespace }).ResourceTypes | Where-Object { $_.ResourceTypeName -eq $resourceApiTypeName }).ApiVersions[0]
                        Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ApiVersion' -StringValues $resourceType, $apiVersions -FunctionName 'ConvertTo-AzOpsState'

                        $object.resources[0].apiVersion = $apiVersions
                        $object.resources[0].type = $resourceType
                    }
                    else {
                        Write-PSFMessage -Level Warning -String 'ConvertTo-AzOpsState.GenerateTemplate.NoApiVersion' -StringValues $resourceType -FunctionName 'ConvertTo-AzOpsState'
                    }
                    #endregion

                    #region Append Name for child resource
                    # [Patch] Temporary until mangementGroup() is fully implemented
                    if ($resourceType -eq "Microsoft.Management/managementGroups/subscriptions") {
                        $resourceName = (((New-AzOpsScope -Scope $Resource.Id).ManagementGroup) + "/" + $Resource.Name)
                        $object.resources[0].name = $resourceName
                        Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ChildResource' -StringValues $resourceName -FunctionName 'ConvertTo-AzOpsState'
                    }
                    #endregion

                }
                Write-PSFMessage -String 'ConvertTo-AzOpsState.Exporting' -StringValues $objectFilePath -FunctionName 'ConvertTo-AzOpsState'
                ConvertTo-Json -InputObject $object -Depth 100 -EnumsAsStrings | Set-Content -Path ([WildcardPattern]::Escape($objectFilePath)) -Encoding UTF8 -Force
            }
        }
        else {
            Write-PSFMessage -String 'ConvertTo-AzOpsState.Exporting.Default' -StringValues $objectFilePath -FunctionName 'ConvertTo-AzOpsState'
            if ($ReturnObject) { return $Resource }
            else {
                ConvertTo-Json -InputObject $Resource -Depth 100 -EnumsAsStrings | Set-Content -Path ([WildcardPattern]::Escape($objectFilePath)) -Encoding UTF8 -Force
            }
        }
    }
}

function Get-AzOpsManagementGroups {

    <#
        .SYNOPSIS
            The cmdlet will recursively enumerates a management group and returns all children
        .DESCRIPTION
            The cmdlet will recursively enumerates a management group and returns all children mgs.
            If the -PartialDiscovery parameter has been used, it will add all MG's where discovery should initiate to the AzOpsPartialRoot variable.
        .PARAMETER ManagementGroup
            Name of the management group to enumerate
        .PARAMETER PartialDiscovery
            Whether to recursively grab all Management Groups and add them to the partial root cache
        .EXAMPLE
            Get-AzOpsManagementGroups -ManagementGroup Tailspin
            Id : /providers/Microsoft.Management/managementGroups/Tailspin
            Type : /providers/Microsoft.Management/managementGroups
            Name : Tailspin
            TenantId : d4c7591d-9b0c-49a4-9670-5f0349b227f1
            DisplayName : Tailspin
            UpdatedTime : 0001-01-01 00:00:00
            UpdatedBy :
            ParentId : /providers/Microsoft.Management/managementGroups/d4c7591d-9b0c-49a4-9670-5f0349b227f1
            ParentName : d4c7591d-9b0c-49a4-9670-5f0349b227f1
            ParentDisplayName : Tenant Root Group
        .INPUTS
            ManagementGroupName
        .OUTPUTS
            Management Group Object
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript( { Get-AzManagementGroup -GroupId $_ -WarningAction SilentlyContinue })]
        $ManagementGroup,

        [switch]
        $PartialDiscovery
    )

    process {
        $groupObject = Get-AzManagementGroup -GroupId $ManagementGroup -Expand -WarningAction SilentlyContinue
        if ($PartialDiscovery) {
            if ($groupObject.ParentId -and -not (Get-AzManagementGroup -GroupId $groupObject.ParentName -ErrorAction Ignore -WarningAction SilentlyContinue)) {
                $script:AzOpsPartialRoot += $groupObject
            }
            if ($groupObject.Children) {
                $groupObject.Children | Where-Object Type -eq "/providers/Microsoft.Management/managementGroups" | Foreach-Object -Process {
                    Get-AzOpsManagementGroups -ManagementGroup $_.Name -PartialDiscovery:$PartialDiscovery
                }
            }
        }
        $groupObject
    }

}

function Get-AzOpsPolicyAssignment {

    <#
        .SYNOPSIS
            Discover all custom policy assignments at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discover all custom policy assignments at the provided scope (Management Groups, subscriptions or resource groups)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve policyset definitions for.
        .EXAMPLE
            > Get-AzOpsPolicyAssignment -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all custom policy assignments deployed at Management Group scope
    #>


    [OutputType([Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Policy.PsPolicyAssignment])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AzOpsScope]
        $ScopeObject
    )

    process {
        #TODO: Discuss dropping resourcegroups, as no action is taken ever
        if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') {
            return
        }

        switch ($ScopeObject.Type) {
            managementGroups {
                Write-PSFMessage -String 'Get-AzOpsPolicyAssignment.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
            }
            subscriptions {
                Write-PSFMessage -String 'Get-AzOpsPolicyAssignment.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
            }
            resourcegroups {
                Write-PSFMessage -String 'Get-AzOpsPolicyAssignment.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject
            }
        }
        Get-AzPolicyAssignment -Scope $ScopeObject.Scope -WarningAction SilentlyContinue | Where-Object PolicyAssignmentId -match $ScopeObject.scope
    }

}

function Get-AzOpsPolicyDefinition {

    <#
        .SYNOPSIS
            Discover all custom policy definitions at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discover all custom policy definitions at the provided scope (Management Groups, subscriptions or resource groups)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve policy definitions for.
        .EXAMPLE
            > Get-AzOpsPolicyDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all custom policy definitions deployed at Management Group scope
    #>


    [OutputType([Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Policy.PsPolicyDefinition])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AzOpsScope]
        $ScopeObject
    )

    process {
        #TODO: Discuss dropping resourcegroups, as no action is taken ever
        if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') {
            return
        }

        switch ($ScopeObject.Type) {
            managementGroups {
                Write-PSFMessage -String 'Get-AzOpsPolicyDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
                Get-AzPolicyDefinition -Custom -ManagementGroupName $ScopeObject.Name | Where-Object ResourceId -match $ScopeObject.Scope
            }
            subscriptions {
                Write-PSFMessage -String 'Get-AzOpsPolicyDefinition.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
                Get-AzPolicyDefinition -Custom -SubscriptionId $ScopeObject.Scope.Split('/')[2] | Where-Object SubscriptionId -eq $ScopeObject.Name
            }
        }
    }

}

function Get-AzOpsPolicySetDefinition {

    <#
        .SYNOPSIS
            Discover all custom policyset definitions at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discover all custom policyset definitions at the provided scope (Management Groups, subscriptions or resource groups)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve policyset definitions for.
        .EXAMPLE
            > Get-AzOpsPolicySetDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all custom policyset definitions deployed at Management Group scope
    #>


    [OutputType([Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Policy.PsPolicySetDefinition])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AzOpsScope]
        $ScopeObject
    )

    process {
        #TODO: Discuss dropping resourcegroups, as no action is taken ever
        if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') {
            return
        }

        switch ($ScopeObject.Type) {
            managementGroups {
                Write-PSFMessage -String 'Get-AzOpsPolicySetDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
                Get-AzPolicySetDefinition -Custom -ManagementGroupName $ScopeObject.Name | Where-Object ResourceId -match $ScopeObject.Scope
            }
            subscriptions {
                Write-PSFMessage -String 'Get-AzOpsPolicySetDefinition.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
                Get-AzPolicySetDefinition -Custom -SubscriptionId $ScopeObject.Scope.Split('/')[2] | Where-Object SubscriptionId -eq $ScopeObject.Name
            }
        }
    }

}

function Get-AzOpsResourceDefinition {

    <#
        .SYNOPSIS
            This cmdlet recursively discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Policies, Role Assignments) from the provided input scope.
        .DESCRIPTION
            This cmdlet recursively discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Policies, Role Assignments) from the provided input scope.
        .PARAMETER Scope
            Discovery Scope
        .PARAMETER SkipPolicy
            Skip discovery of policies for better performance.
        .PARAMETER SkipRole
            Skip discovery of roles for better performance.
        .PARAMETER SkipResourceGroup
            Skip discovery of resource groups.
        .PARAMETER SkipResource
            Skip discovery of resources inside resource groups.
        .PARAMETER ExportRawTemplate
            Export generic templates without embedding them in the parameter block.
        .PARAMETER StatePath
            The root folder under which to write the resource json.
        .EXAMPLE
            $TenantRootId = '/providers/Microsoft.Management/managementGroups/{0}' -f (Get-AzTenant).Id
            Get-AzOpsResourceDefinition -scope $TenantRootId -Verbose
            Discover all resources from root Management Group
        .EXAMPLE
            Get-AzOpsResourceDefinition -scope /providers/Microsoft.Management/managementGroups/landingzones -SkipPolicy -SkipResourceGroup
            Discover all resources from child Management Group, skip discovery of policies and resource groups
        .EXAMPLE
            Get-AzOpsResourceDefinition -scope /subscriptions/623625ae-cfb0-4d55-b8ab-0bab99cbf45c
            Discover all resources from Subscription level
        .EXAMPLE
            Get-AzOpsResourceDefinition -scope /subscriptions/623625ae-cfb0-4d55-b8ab-0bab99cbf45c/resourceGroups/myresourcegroup
            Discover all resources from resource group level
        .EXAMPLE
            Get-AzOpsResourceDefinition -scope /subscriptions/623625ae-cfb0-4d55-b8ab-0bab99cbf45c/resourceGroups/contoso-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net
            Discover a single resource
    #>


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

        [switch]
        $SkipPolicy = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipPolicy'),

        [switch]
        $SkipRole = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipRole'),

        [switch]
        $SkipResourceGroup = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResourceGroup'),

        [switch]
        $SkipResource = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResource'),

        [switch]
        $ExportRawTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.ExportRawTemplate'),

        [Parameter(Mandatory = $false)]
        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State')
    )

    begin {
        #region Utility Functions
        function ConvertFrom-TypeResource {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                [AzOpsScope]
                $ScopeObject,

                [string]
                $StatePath,

                [switch]
                $ExportRawTemplate
            )

            process {
                $common = @{
                    FunctionName = 'Get-AzOpsResourceDefinition'
                    Target       = $ScopeObject
                }

                Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Resource.Processing' -StringValues $ScopeObject.Resource, $ScopeObject.ResourceGroup
                try {
                    $resource = Get-AzResource -ResourceId $ScopeObject.scope -ErrorAction Stop
                    ConvertTo-AzOpsState -Resource $resource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate
                }
                catch {
                    Write-PSFMessage @common -Level Warning -String 'Get-AzOpsResourceDefinition.Resource.Processing.Failed' -StringValues $ScopeObject.Resource, $ScopeObject.ResourceGroup -ErrorRecord $_
                }
            }
        }

        function ConvertFrom-TypeResourceGroup {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                [AzOpsScope]
                $ScopeObject,

                [switch]
                $SkipResource,

                [string]
                $StatePath,

                [switch]
                $ExportRawTemplate,

                $Context,

                [string]
                $OdataFilter
            )

            process {
                $common = @{
                    FunctionName = 'Get-AzOpsResourceDefinition'
                    Target       = $ScopeObject
                }

                Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing' -StringValues $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription

                try {
                    $resourceGroup = Get-AzResourceGroup -Name $ScopeObject.ResourceGroup -DefaultProfile $Context -ErrorAction Stop
                }
                catch {
                    Write-PSFMessage @common -Level Warning -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Error' -StringValues $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -ErrorRecord $_
                    return
                }
                if ($resourceGroup.ManagedBy) {
                    Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Owned' -StringValues $resourceGroup.ResourceGroupName, $resourceGroup.ManagedBy
                    return
                }
                ConvertTo-AzOpsState -Resource $resourceGroup -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath


                # Get all resources in resource groups
                $paramGetAzResource = @{
                    DefaultProfile    = $Context
                    ResourceGroupName = $resourceGroup.ResourceGroupName
                    ODataQuery        = $OdataFilter
                    ExpandProperties  = $true
                }
                Get-AzResource @paramGetAzResource | ForEach-Object {
                    New-AzOpsScope -Scope $_.ResourceId
                } | ConvertFrom-TypeResource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate
            }
        }

        function ConvertFrom-TypeSubscription {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                [AzOpsScope]
                $ScopeObject,

                [string]
                $StatePath,

                $Context,

                [switch]
                $ExportRawTemplate,

                [switch]
                $SkipResourceGroup,

                [switch]
                $SkipResource,

                [string]
                $ODataFilter )
            begin {
                # Set variables for retry with exponential backoff
                $backoffMultiplier = 2
                $maxRetryCount = 6
            }

            process {
                $common = @{
                    FunctionName = 'Get-AzOpsResourceDefinition'
                    Target       = $ScopeObject
                }

                Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.Processing' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription

                # Skip discovery of resource groups if SkipResourceGroup switch have been used
                # Separate discovery of resource groups in subscriptions to support parallel discovery
                if ($SkipResourceGroup) {
                    Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.SkippingResourceGroup'
                }
                else {
                    # Get all Resource Groups in Subscription
                    # Retry loop with exponential back off implemented to catch errors
                    # Introduced due to error "Your Azure Credentials have not been set up or expired"
                    # https://github.com/Azure/azure-powershell/issues/9448
                    # Define variables used by script
                    if (
                        (((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') | Foreach-Object { $scopeObject.Subscription -like $_ }) -contains $true) -or
                        (((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') | Foreach-Object { $scopeObject.SubscriptionDisplayName -like $_ }) -contains $true)
                    ) {
                        $resourceGroups = Invoke-AzOpsScriptBlock -ArgumentList $Context -ScriptBlock {
                            param ($Context)
                            Get-AzResourceGroup -DefaultProfile ($Context | Write-Output) -ErrorAction Stop | Where-Object { -not $_.ManagedBy }
                        } -RetryCount $maxRetryCount -RetryWait $backoffMultiplier -RetryType Exponential
                        if (-not $resourceGroups) {
                            Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.NoResourceGroup' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription
                        }

                        #region Prepare Input Data for parallel processing
                        $runspaceData = @{
                            AzOpsPath                       = "$($script:ModuleRoot)\AzOps.psd1"
                            StatePath                       = $StatePath
                            ScopeObject                     = $ScopeObject
                            ODataFilter                     = $ODataFilter
                            SkipResource                    = $SkipResource
                            MaxRetryCount                   = $maxRetryCount
                            BackoffMultiplier               = $backoffMultiplier
                            ExportRawTemplate               = $ExportRawTemplate
                            runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup
                            runspace_AzOpsSubscriptions     = $script:AzOpsSubscriptions
                            runspace_AzOpsPartialRoot       = $script:AzOpsPartialRoot
                            runspace_AzOpsResourceProvider  = $script:AzOpsResourceProvider
                        }
                        #endregion Prepare Input Data for parallel processing

                        #region Discover all resource groups in parallel
                        $resourceGroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                            $resourceGroup = $_
                            $runspaceData = $using:runspaceData

                            $msgCommon = @{
                                FunctionName = 'Get-AzOpsResourceDefinition'
                                ModuleName   = 'AzOps'
                            }

                            # region Importing module
                            # We need to import all required modules and declare variables again because of the parallel runspaces
                            # https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-feature/
                            Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                            $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru
                            # endregion Importing module

                            & $azOps {
                                $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup
                                $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions
                                $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot
                                $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider
                            }

                            $context = Get-AzContext -ListAvailable | Where-Object {
                                $_.Subscription.id -eq $runspaceData.ScopeObject.Subscription
                            }

                            Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup
                            & $azOps { ConvertTo-AzOpsState -Resource $resourceGroup -ExportRawTemplate:$runspaceData.ExportRawTemplate -StatePath $runspaceData.Statepath }

                            if (-not $using:SkipResource) {
                                Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup.Resources' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup
                                $resources = & $azOps {
                                    $parameters = @{
                                        DefaultProfile = $Context | Select-Object -First 1
                                        ODataQuery     = $runspaceData.ODataFilter
                                    }
                                    if ($resourceGroup.ResourceGroupName) {
                                        $parameters.ResourceGroupName = $resourceGroup.ResourceGroupName
                                    }
                                    Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock {
                                        param (
                                            $Parameters
                                        )
                                        $param = $Parameters | Write-Output
                                        Get-AzResource @param -ExpandProperties -ErrorAction Stop
                                    } -RetryCount $runspaceData.MaxRetryCount -RetryWait $runspaceData.BackoffMultiplier -RetryType Exponential
                                }
                                if (-not $resources) {
                                    Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup.NoResources' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup
                                }

                                # Loop through resources and convert them to AzOpsState
                                foreach ($resource in $resources) {
                                    # Convert resources to AzOpsState
                                    Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.Resource' -StringValues $resource.Name, $resourceGroup.ResourceGroupName -Target $resource
                                    & $azOps { ConvertTo-AzOpsState -Resource $resource -ExportRawTemplate:$runspaceData.ExportRawTemplate -StatePath $runspaceData.Statepath }
                                }
                            }
                            else {
                                Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.SkippingResources'
                            }
                        }
                        #endregion Discover all resource groups in parallel
                    }
                    else {
                        Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.ExcludeResourceGroup'
                    }
                }
                if ($subscriptionItem = $script:AzOpsAzManagementGroup.children | Where-Object Name -eq $ScopeObject.name) {
                    ConvertTo-AzOpsState -Resource $subscriptionItem -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
                }
            }
        }

        function ConvertFrom-TypeManagementGroup {
            [CmdletBinding()]
            param (
                [Parameter(ValueFromPipeline = $true)]
                [AzOpsScope]
                $ScopeObject,

                [switch]
                $SkipPolicy,

                [switch]
                $SkipRole,

                [switch]
                $SkipResourceGroup,

                [switch]
                $SkipResource,

                [switch]
                $ExportRawTemplate,

                [string]
                $StatePath
            )
            begin {
                $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude ScopeObject
            }
            process {
                $common = @{
                    FunctionName = 'Get-AzOpsResourceDefinition'
                    Target       = $ScopeObject
                }

                Write-PSFMessage -String 'Get-AzOpsResourceDefinition.ManagementGroup.Processing' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup

                $childOfManagementGroups = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $ScopeObject.ManagementGroup).Children

                foreach ($child in $childOfManagementGroups) {

                    if ($child.Type -eq '/subscriptions') {
                        if ($script:AzOpsSubscriptions.id -contains $child.Id) {
                            Get-AzOpsResourceDefinition -Scope $child.Id @parameters
                        } else {
                            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.ManagementGroup.Subscription.NotFound' -StringValues $child.Name
                        }
                    } else {
                        Get-AzOpsResourceDefinition -Scope $child.Id @parameters
                    }
                }
                ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object Name -eq $ScopeObject.ManagementGroup) -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
            }
        }
        #endregion Utility Functions
    }

    process {
        Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing' -StringValues $Scope

        try { $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction Stop }
        catch {
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.NotFound' -StringValues $Scope
            return
        }

        if ($scopeObject.Subscription) {
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Subscription.Found' -StringValues $scopeObject.subscriptionDisplayName, $scopeObject.subscription
            $context = Get-AzContext -ListAvailable | Where-Object { $_.Subscription.id -eq $scopeObject.Subscription }
            $odataFilter = "`$filter=subscriptionId eq '$($scopeObject.subscription)'"
            Write-PSFMessage -Level Debug -String 'Get-AzOpsResourceDefinition.Subscription.OdataFilter' -StringValues $odataFilter
        }

        switch ($scopeObject.Type) {
            resource { ConvertFrom-TypeResource -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate }
            resourcegroups { ConvertFrom-TypeResourceGroup -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -Context $context -SkipResource:$SkipResource -OdataFilter $odataFilter }
            subscriptions {  ConvertFrom-TypeSubscription -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -Context $context -SkipResourceGroup:$SkipResourceGroup -SkipResource:$SkipResource -ODataFilter $odataFilter }
            managementGroups { ConvertFrom-TypeManagementGroup -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -SkipPolicy:$SkipPolicy -SkipRole:$SkipRole -SkipResourceGroup:$SkipResourceGroup -SkipResource:$SkipResource }
        }

        if ($scopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') {
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope
            return
        }

        $serializedPolicyDefinitionsInAzure = @()
        $serializedPolicySetDefinitionsInAzure = @()
        $serializedPolicyAssignmentsInAzure = @()
        $serializedRoleDefinitionsInAzure = @()
        $serializedRoleAssignmentInAzure = @()

        #region Process Policies
        if (-not $SkipPolicy) {
            # Process policy definitions
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Definitions', $scopeObject.Scope
            $policyDefinitions = Get-AzOpsPolicyDefinition -ScopeObject $scopeObject
            $policyDefinitions | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
            $serializedPolicyDefinitionsInAzure = $policyDefinitions | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject

            # Process policyset definitions (initiatives))
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'PolicySet Definitions', $scopeObject.Scope
            $policySetDefinitions = Get-AzOpsPolicySetDefinition -ScopeObject $scopeObject
            $policySetDefinitions | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
            $serializedPolicySetDefinitionsInAzure = $policySetDefinitions | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject

            # Process policy assignments
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Assignments', $scopeObject.Scope
            $policyAssignments = Get-AzOpsPolicyAssignment -ScopeObject $scopeObject
            $policyAssignments | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
            $serializedPolicyAssignmentsInAzure = $policyAssignments | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject
        }
        #endregion Process Policies

        #region Process Roles
        if (-not $SkipRole) {
            # Process role definitions
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Role Definitions', $scopeObject.Scope
            $roleDefinitions = Get-AzOpsRoleDefinition -ScopeObject $scopeObject
            $roleDefinitions | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
            $serializedRoleDefinitionsInAzure = $roleDefinitions | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject

            # Process role assignments
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Role Assignments', $scopeObject.Scope
            $roleAssignments = Get-AzOpsRoleAssignment -ScopeObject $scopeObject
            $roleAssignments | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath
            $serializedRoleAssignmentInAzure = $roleAssignments | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject
        }
        #endregion Process Roles

        if ($scopeObject.Type -notin 'subscriptions', 'managementGroups') {
            Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope
            return
        }

        if (Get-PSFConfigValue -FullName 'AzOps.Core.AllInOneTemplate') {
            #region Add accumulated policy and role data
            # Get statefile from scope
            $parametersJson = Get-Content -Path $scopeObject.StatePath | ConvertFrom-Json -Depth 100
            # Create property bag and add resources at scope
            $propertyBag = [ordered]@{
                'policyDefinitions'    = @($serializedPolicyDefinitionsInAzure)
                'policySetDefinitions' = @($serializedPolicySetDefinitionsInAzure)
                'policyAssignments'    = @($serializedPolicyAssignmentsInAzure)
                'roleDefinitions'      = @($serializedRoleDefinitionsInAzure)
                'roleAssignments'      = @($serializedRoleAssignmentInAzure)
            }
            # Add property bag to parameters json
            $parametersJson.parameters.input.value | Add-Member -Name 'properties' -MemberType NoteProperty -Value $propertyBag -force
            # Export state file with properties at scope
            ConvertTo-AzOpsState -Resource $parametersJson -ExportPath $scopeObject.StatePath -ExportRawTemplate -StatePath $StatePath
            #endregion Add accumulated policy and role data
        }

        Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope
    }

}

function Get-AzOpsRoleAssignment {

    <#
        .SYNOPSIS
            Discovers all custom Role Assignment at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discovers all custom Role Assignment at the provided scope (Management Groups, subscriptions or resource groups)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve role assignments for.
        .EXAMPLE
            > Get-AzOpsRoleAssignment -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all custom role assignments deployed at Management Group scope
    #>


    [OutputType([AzOpsRoleAssignment])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AzOpsScope]
        $ScopeObject
    )

    process {
        Write-PSFMessage -String 'Get-AzOpsRoleAssignment.Processing' -StringValues $ScopeObject -Target $ScopeObject
        foreach ($roleAssignment in Get-AzRoleAssignment -Scope $ScopeObject.Scope | Where-Object Scope -eq $ScopeObject.Scope) {
            Write-PSFMessage -String 'Get-AzOpsRoleAssignment.Assignment' -StringValues $roleAssignment.DisplayName, $roleAssignment.RoleDefinitionName -Target $ScopeObject
            [AzOpsRoleAssignment]::new($roleAssignment)
        }
    }

}

function Get-AzOpsRoleDefinition {

    <#
        .SYNOPSIS
            Discover all custom Role Definition at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discover all custom Role Definition at the provided scope (Management Groups, subscriptions or resource groups)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve role definitions for.
        .EXAMPLE
            > Get-AzOpsRoleDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all custom role definitions deployed at Management Group scope
    #>


    [OutputType([AzOpsRoleDefinition])]
    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AzOpsScope]
        $ScopeObject
    )

    process {
        Write-PSFMessage -String 'Get-AzOpsRoleDefinition.Processing' -StringValues $ScopeObject -Target $ScopeObject
        foreach ($roleDefinition in Get-AzRoleDefinition -Custom -Scope $ScopeObject.Scope -WarningAction SilentlyContinue) {
            #removing trailing '/' if it exists in assignable scopes
            if (($roledefinition.AssignableScopes[0] -replace "[/]$" -replace '') -eq $ScopeObject.Scope) {
                [AzOpsRoleDefinition]::new($roleDefinition)
            }
            else {
                Write-PSFMessage -String 'Get-AzOpsRoleDefinition.NonAuthorative' -StringValues $roledefinition,Id, $ScopeObject.Scope, $roledefinition.AssignableScopes[0] -Target $ScopeObject
            }
        }
    }

}

function Get-AzOpsSubscription {

    <#
        .SYNOPSIS
            Returns a list of applicable subscriptions.
        .DESCRIPTION
            Returns a list of applicable subscriptions.
            "Applicable" generally refers to active, non-trial subscriptions.
        .PARAMETER ExcludedOffers
            Specific offers to exclude (e.g. specific trial offerings)
        .PARAMETER ExcludedStates
            Specific subscription states to ignore (e.g. expired subscriptions)
        .PARAMETER TenantId
            ID of the tenant to search in.
            Must be a connected tenant.
        .PARAMETER ApiVersion
            What version of the AZ Api to communicate with.
        .EXAMPLE
            > Get-AzOpsSubscription -TenantId $TenantId
            Returns active, non-trial subscriptions of the specified tenant.
    #>


    [CmdletBinding()]
    param (
        [string[]]
        $ExcludedOffers = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubOffer'),

        [string[]]
        $ExcludedStates = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubState'),

        [Parameter(Mandatory = $true)]
        [ValidateScript({ $_ -in (Get-AzContext).Tenant.Id })]
        [guid]
        $TenantId,

        [string]
        $ApiVersion = '2020-01-01'
    )

    process {
        Write-PSFMessage -String 'Get-AzOpsSubscription.Excluded.States' -StringValues ($ExcludedStates -join ',')
        Write-PSFMessage -String 'Get-AzOpsSubscription.Excluded.Offers' -StringValues ($ExcludedOffers -join ',')

        $nextLink = "/subscriptions?api-version=$ApiVersion"
        $allSubscriptionsResults = do {
            $allSubscriptionsJson = ((Invoke-AzRestMethod -Path $nextLink -Method GET).Content | ConvertFrom-Json -Depth 100)
            $allSubscriptionsJson.value | Where-Object tenantId -eq $TenantId
            $nextLink = $allSubscriptionsJson.nextLink -replace 'https://management\.azure\.com'
        }
        while ($nextLink)

        $includedSubscriptions = $allSubscriptionsResults | Where-Object {
            $_.state -notin $ExcludedStates -and
            $_.subscriptionPolicies.quotaId -notin $ExcludedOffers
        }
        if (-not $includedSubscriptions) {
            Write-PSFMessage -Level Warning -String 'Get-AzOpsSubscription.NoSubscriptions' -Tag failed
            return
        }

        Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.Found' -StringValues $allSubscriptionsResults.Count
        if ($allSubscriptionsResults.Count -gt $includedSubscriptions.Count) {
            Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.Excluded' -StringValues ($allSubscriptionsResults.Count - $includedSubscriptions.Count)
        }

        if ($includedSubscriptions | Where-Object State -EQ PastDue) {
            Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.PastDue' -StringValues ($includedSubscriptions | Where-Object State -EQ PastDue).Count
        }
        Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.Included' -StringValues $includedSubscriptions.Count
        $includedSubscriptions
    }

}

function Invoke-AzOpsChange {

    <#
        .SYNOPSIS
            Applies a change to Azure from the AzOps configuration.
        .DESCRIPTION
            Applies a change to Azure from the AzOps configuration.
        .PARAMETER ChangeSet
            Set of changes from the last execution that need to be applied.
        .PARAMETER StatePath
            The root path to where the entire state is being built in.
        .PARAMETER AzOpsMainTemplate
            Path to the main template used by AzOps
        .EXAMPLE
            > Invoke-AzOpsChange -ChangeSet changeSet -StatePath $StatePath -AzOpsMainTemplate $templatePath
            Applies a change to Azure from the AzOps configuration.
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]
        $ChangeSet,

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State'),

        [string]
        $AzOpsMainTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate')
    )

    begin {
        #region Utility Functions
        function Resolve-ArmFileAssociation {
            [CmdletBinding()]
            param (
                [AzOpsScope]
                $ScopeObject,

                [string]
                $FilePath,

                [string]
                $AzOpsMainTemplate
            )

            #region Initialization Prep
            $common = @{
                Level        = 'Host'
                Tag          = 'pwsh'
                FunctionName = 'Invoke-AzOpsChange'
                Target       = $ScopeObject
            }

            $result = [PSCustomObject] @{
                TemplateFilePath          = $null
                TemplateParameterFilePath = $null
                DeploymentName            = $null
                ScopeObject               = $ScopeObject
                Scope                     = $ScopeObject.Scope
            }

            $fileItem = Get-Item -Path $FilePath
            if ($fileItem.Extension -notin '.json' , '.bicep') {
                Write-PSFMessage -Level Warning -String 'Invoke-AzOpsChange.Resolve.NoJson' -StringValues $fileItem.FullName -Tag pwsh -FunctionName 'Invoke-AzOpsChange' -Target $ScopeObject
                return
            }
            #endregion Initialization Prep

            #region Case: Parameters File
            if ($fileItem.Name.EndsWith('.parameters.json')) {
                $result.TemplateParameterFilePath = $fileItem.FullName
                $deploymentName = $fileItem.Name -replace (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'), ''
                if ($deploymentName.Length -gt 58) { $deploymentName = $deploymentName.SubString(0, 58) }
                $result.DeploymentName = "AzOps-$deploymentName"

                #region Directly Associated Template file exists
                $templatePath = $fileItem.FullName -replace '.parameters.json', (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')
                if (Test-Path $templatePath) {
                    Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.FoundTemplate' -StringValues $FilePath, $templatePath
                    $result.TemplateFilePath = $templatePath
                    return $result
                }
                #endregion Directly Associated Template file exists

                #region Directly Associated bicep template exists
                $bicepTemplatePath = $fileItem.FullName -replace '.parameters.json', '.bicep'
                if (Test-Path $bicepTemplatePath) {
                    Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.FoundBicepTemplate' -StringValues $FilePath, $bicepTemplatePath
                    $result.TemplateFilePath = $bicepTemplatePath
                    return $result
                }
                #endregion Directly Associated bicep template exists

                #region Check in the main template file for a match
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.NotFoundTemplate' -StringValues $FilePath, $templatePath
                $mainTemplateItem = Get-Item $AzOpsMainTemplate
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.FromMainTemplate' -StringValues $mainTemplateItem.FullName

                # Determine Resource Type in Parameter file
                $templateParameterFileHashtable = Get-Content -Path $fileItem.FullName | ConvertFrom-Json -AsHashtable
                $effectiveResourceType = $null
                if ($templateParameterFileHashtable.Keys -contains "`$schema") {
                    if ($templateParameterFileHashtable.parameters.input.value.Keys -contains "Type") {
                        # ManagementGroup and Subscription
                        $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.Type
                    }
                    elseif ($templateParameterFileHashtable.parameters.input.value.Keys -contains "ResourceType") {
                        # Resource
                        $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.ResourceType
                    }
                }
                # Check if generic template is supporting the resource type for the deployment.
                if ($effectiveResourceType -and
                    (Get-Content $mainTemplateItem.FullName | ConvertFrom-Json -AsHashtable).variables.apiVersionLookup.Keys -contains $effectiveResourceType) {
                    Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.MainTemplate.Supported' -StringValues $effectiveResourceType, $AzOpsMainTemplate.FullName
                    $result.TemplateFilePath = $mainTemplateItem.FullName
                    return $result
                }
                Write-PSFMessage -Level Warning -String 'Invoke-AzOpsChange.Resolve.MainTemplate.NotSupported' -StringValues $effectiveResourceType, $AzOpsMainTemplate.FullName -Tag pwsh -FunctionName 'Invoke-AzOpsChange' -Target $ScopeObject
                return
                #endregion Check in the main template file for a match
                # All Code paths end the command
            }
            #endregion Case: Parameters File

            #region Case: Template File
            $result.TemplateFilePath = $fileItem.FullName
            $parameterPath = Join-Path $fileItem.Directory.FullName -ChildPath ($fileItem.BaseName + '.parameters' + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))
            if (Test-Path -Path $parameterPath) {
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.ParameterFound' -StringValues $FilePath, $parameterPath
                $result.TemplateParameterFilePath = $parameterPath
            }
            else {
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.ParameterNotFound' -StringValues $FilePath, $parameterPath
            }

            $deploymentName = $fileItem.BaseName -replace '\.json$' -replace ' ', '_'
            if ($deploymentName.Length -gt 58) { $deploymentName = $deploymentName.SubString(0, 58) }
            $result.DeploymentName = "AzOps-$deploymentName"

            $result
            #endregion Case: Template File
        }
        #endregion Utility Functions

        $common = @{
            Level = 'Host'
            Tag   = 'git'
        }

        $WhatIfPreferenceState = $WhatIfPreference
        $WhatIfPreference = $false
    }

    process {
        if (-not $ChangeSet) { return }

        #region Categorize Input
        Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deployment.Required'
        $deleteSet = @()
        $addModifySet = foreach ($change in $ChangeSet) {
            $operation, $filename = ($change -split "`t")[0, -1]
            if ($operation -eq 'D') {
                $deleteSet += $filename
                continue
            }
            if ($operation -in 'A', 'M', 'R') { $filename }
        }
        if ($deleteSet) { $deleteSet = $deleteSet | Sort-Object }
        if ($addModifySet) { $addModifySet = $addModifySet | Sort-Object }
        # TODO: Clarify what happens with the deletes - not used after reporting them

        Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.AddModify'
        foreach ($item in $addModifySet) {
            Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.AddModify.File' -StringValues $item
        }
        Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.Delete'
        foreach ($item in $deleteSet) {
            Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.Delete.File' -StringValues $item
        }
        #endregion Categorize Input

        #region Deploy State
        $common.Tag = 'pwsh'
        # Nested Pipeline allows economizing on New-AzOpsStateDeployment having to run its "begin" block once only
        $newStateDeploymentCmd = { New-AzOpsStateDeployment -StatePath $StatePath }.GetSteppablePipeline()
        $newStateDeploymentCmd.Begin($true)
        foreach ($addition in $addModifySet) {
            if ($addition -notmatch '/*.subscription.json$') { continue }
            Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deploy.Subscription' -StringValues $addition -Target $addition
            $newStateDeploymentCmd.Process($addition)
        }
        foreach ($addition in $addModifySet) {
            if ($addition -notmatch '/*.providerfeatures.json$') { continue }
            Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deploy.ProviderFeature' -StringValues $addition -Target $addition
            $newStateDeploymentCmd.Process($addition)
        }
        foreach ($addition in $addModifySet) {
            if ($addition -notmatch '/*.resourceproviders.json$') { continue }
            Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deploy.ResourceProvider' -StringValues $addition -Target $addition
            $newStateDeploymentCmd.Process($addition)
        }
        $newStateDeploymentCmd.End()
        #endregion Deploy State

        $deploymentList = foreach ($addition in $addModifySet | Where-Object { $_ -match ((Get-Item $StatePath).Name) }) {

            # Avoid duplicate entries in the deployment list
            if ($addition.EndsWith(".parameters.json")) {
                if ($addModifySet -contains $addition.Replace(".parameters.json", ".json")) {
                    continue
                }
            }

            # Handle Bicep templates
            if ($addition.EndsWith(".bicep")) {
                Assert-AzOpsBicepDependency -Cmdlet $PSCmdlet
                $transpiledTemplatePath = $addition -replace '.bicep', '.json'
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.ConvertBicepTemplate' -StringValues $addModifySet, $transpiledTemplatePath
                Invoke-AzOpsNativeCommand -ScriptBlock { bicep build $addition --outfile $transpiledTemplatePath }
                $addition = $transpiledTemplatePath
            }

            try { $scopeObject = New-AzOpsScope -Path $addition -StatePath $StatePath -ErrorAction Stop }
            catch {
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Scope.Failed' -StringValues $addition, $StatePath -Target $addition -ErrorRecord $_
                continue
            }
            if (-not $scopeObject) {
                Write-PSFMessage @common -String 'Invoke-AzOpsChange.Scope.NotFound' -StringValues $addition, $StatePath -Target $addition
                continue
            }

            Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $addition -AzOpsMainTemplate $AzOpsMainTemplate
        }

        $WhatIfPreference = $WhatIfPreferenceState

        #Starting Tenant Deployment
        $uniqueProperties = 'Scope', 'DeploymentName', 'TemplateFilePath', 'TemplateParameterFilePath'
        $deploymentList | Select-Object $uniqueProperties -Unique | Sort-Object -Property TemplateParameterFilePath | New-AzOpsDeployment -WhatIf:$WhatIfPreference
    }

}

function Invoke-AzOpsNativeCommand {

    <#
        .SYNOPSIS
            Executes a native command.
        .DESCRIPTION
            Executes a native command.
        .PARAMETER ScriptBlock
            The scriptblock containing the native command to execute.
            Note: Specifying a scriptblock WITHOUT any native command may cause erroneous LASTEXITCODE detection.
        .PARAMETER IgnoreExitcode
            Whether to ignore exitcodes.
        .PARAMETER Quiet
            Quiet mode disables printing error output of a native command.
        .EXAMPLE
            > Invoke-AzOpsNativeCommand -Scriptblock { git config --system -l }
            Executes "git config --system -l"
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $ScriptBlock,

        [switch]
        $IgnoreExitcode,

        [switch]
        $Quiet
    )

    try {
        if ($Quiet) {
            $output = & $ScriptBlock 2>&1
        }
        else { $output = & $ScriptBlock }

        if (-not $Quiet -and $output) {
            $output | Out-String -NoNewLine | ForEach-Object {
                Write-PSFMessage -Level Debug -Message $_
            }
            $output
        }
    }
    catch {
        if (-not $IgnoreExitcode) {
            $caller = Get-PSCallStack -ErrorAction SilentlyContinue
            if ($caller) {
                Stop-PSFFunction -String 'Invoke-AzOpsNativeCommand.Failed.WithCallstack' -StringValues $ScriptBlock, $caller[1].ScriptName, $caller[1].ScriptLineNumber, $LASTEXITCODE -Cmdlet $PSCmdlet -EnableException $true
            }
            Stop-PSFFunction -String 'Invoke-AzOpsNativeCommand.Failed.NoCallstack' -StringValues $ScriptBlock, $LASTEXITCODE -Cmdlet $PSCmdlet -EnableException $true
        }
        $output
    }
}

function Invoke-AzOpsScriptBlock {

    <#
        .SYNOPSIS
            Execute a scriptblock, retry if it fails.
        .DESCRIPTION
            Execute a scriptblock, retry if it fails.
        .PARAMETER ScriptBlock
            The scriptblock to execute.
        .PARAMETER ArgumentList
            Any arguments to pass to the scriptblock.
        .PARAMETER RetryCount
            How often to try again before giving up.
            Default: 0
        .PARAMETER RetryWait
            How long to wait between retries in seconds.
            Default: 3
        .PARAMETER RetryType
            How to wait for a retry?
            Either always the exact time specified in RetryWait as seconds, or exponentially increase the time between waits.
            Assuming a wait time of 2 seconds and three retries, this will result in the following waits between attempts:
            Linear (default): 2, 2, 2
            Exponential: 2, 4, 8
        .EXAMPLE
            > Invoke-AzOpsScriptBlock -ScriptBlock { 1 / 0 }
            Will attempt once to divide by zero.
            Hint: This is unlikely to succeede. Ever.
        .EXAMPLE
            > Invoke-AzOpsScriptBlock -ScriptBlock { 1 / 0 } -RetryCount 3
            Will attempt to divide by zero, retrying up to 3 additional times (for a total of 4 attempts).
            Hint: Trying to divide by zero more than once does not increase your chance of success.
    #>


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

        [object[]]
        $ArgumentList,

        [int]
        $RetryCount = 0,

        [int]
        $RetryWait = 3,

        [ValidateSet('Linear','Exponential')]
        [string]
        $RetryType = 'Linear'
    )

    begin {
        $count = 0
    }

    process {
        $data = @{
            ScriptBlock = $ScriptBlock
            ArgumentList = $ArgumentList
        }
        while ($count -le $RetryCount) {
            $count++
            try {
                if (Test-PSFParameterBinding -ParameterName ArgumentList) { & $ScriptBlock $ArgumentList }
                else { & $ScriptBlock }
                break
            }
            catch {
                if ($count -lt $RetryCount) {
                    Write-PSFMessage -Level Debug -String 'Invoke-AzOpsScriptBlock.Failed.WillRetry' -StringValues $count, $RetryCount -ErrorRecord $_ -Data $data
                    switch ($RetryType) {
                        Linear { Start-Sleep -Seconds $RetryWait }
                        Exponential { Start-Sleep -Seconds ([math]::Pow($RetryWait, $count)) }
                    }
                    continue
                }
                Write-PSFMessage -Level Warning -String 'Invoke-AzOpsScriptBlock.Failed.GivingUp' -StringValues $count, $RetryCount -ErrorRecord $_ -Data $data
                throw
            }
        }
    }

}

function New-AzOpsDeployment {

    <#
        .SYNOPSIS
            Deploys a full state into azure.
        .DESCRIPTION
            Deploys a full state into azure.
        .PARAMETER DeploymentName
            Name under which to deploy the state.
        .PARAMETER TemplateFilePath
            Path where the ARM templates can be found.
        .PARAMETER TemplateParameterFilePath
            Path where the parameters of the ARM templates can be found.
        .PARAMETER Mode
            Mode in which to process the templates.
            Defaults to incremental.
        .PARAMETER StatePath
            The root folder under which to find the resource json.
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
        .EXAMPLE
            > $AzOpsDeploymentList | Select-Object $uniqueProperties -Unique | Sort-Object -Property TemplateParameterFilePath | New-Deployment
            Deploy all unique deployments provided from $AzOpsDeploymentList
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $DeploymentName = "azops-template-deployment",

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]
        $TemplateFilePath = (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate'),

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [AllowNull()]
        [string]
        $TemplateParameterFilePath,

        [string]
        $Mode = "Incremental",

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State')
    )

    process {
        Write-PSFMessage -String 'New-AzOpsDeployment.Processing' -StringValues $DeploymentName, $TemplateFilePath, $TemplateParameterFilePath, $Mode -Target $TemplateFilePath

        #region Resolve Scope
        try {
            if ($TemplateParameterFilePath) {
                $scopeObject = New-AzOpsScope -Path $TemplateParameterFilePath -StatePath $StatePath -ErrorAction Stop -WhatIf:$false
            }
            else {
                $scopeObject = New-AzOpsScope -Path $TemplateFilePath -StatePath $StatePath -ErrorAction Stop -WhatIf:$false
            }
        }
        catch {
            Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Failed' -Target $TemplateFilePath -StringValues $TemplateFilePath, $TemplateParameterFilePath -ErrorRecord $_
            return
        }
        if (-not $scopeObject) {
            Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Empty' -Target $TemplateFilePath -StringValues $TemplateFilePath, $TemplateParameterFilePath
            return
        }
        #endregion Resolve Scope

        #region Parse Content
        $templateContent = Get-Content $TemplateFilePath | ConvertFrom-Json -AsHashtable
        #endregion

        #region Process Scope
        #region Resource Group
        if ($scopeObject.resourcegroup) {
            Set-AzOpsContext -ScopeObject $scopeObject
            if ($templateContent.resources[0].type -eq 'Microsoft.Resources/resourceGroups') {
                # Since this is a deployment for resource group, it must be invoked at subscription scope
                $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion'
                Write-PSFMessage -String 'New-AzOpsDeployment.Subscription.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject

                $parameters = @{
                    'TemplateFile'                = $TemplateFilePath
                    'Location'                    = $defaultDeploymentRegion
                    'SkipTemplateParameterPrompt' = $true
                }
                if ($TemplateParameterFilePath) {
                    $parameters.TemplateParameterFile = $TemplateParameterFilePath
                }

                if ((Get-AzContext).Subscription.Id -ne $scopeObject.subscription) {
                    Set-AzOpsContext -ScopeObject $scopeObject
                }

                # Validate Template
                $results = Get-AzSubscriptionDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError
                if ($resultsError) {
                    Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject
                }
                elseif ($results.Error) {
                    Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject
                    return
                }
                else {
                    Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject
                    Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject
                    $output = 'WhatIf Results:{0}```json{0}{1}{0}```{0}' -f [environment]::NewLine, ($results.Changes | ConvertTo-Json -Depth 5)
                    Set-Content -Path '/tmp/OUTPUT.md' -Value $output -WhatIf:$false
                }

                $parameters.Name = $DeploymentName
                if ($PSCmdlet.ShouldProcess("Start Subscription Deployment?")) {
                    New-AzSubscriptionDeployment @parameters
                }
                else {
                    # Exit deployment
                    Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf'
                }
            }
            else {
                Write-PSFMessage -String 'New-AzOpsDeployment.ResourceGroup.Processing' -StringValues $scopeObject -Target $scopeObject

                $parameters = @{
                    'TemplateFile'                = $TemplateFilePath
                    'SkipTemplateParameterPrompt' = $true
                    'ResourceGroupName'           = $scopeObject.Name
                }
                if ($TemplateParameterFilePath) {
                    $parameters.TemplateParameterFile = $TemplateParameterFilePath
                }

                $results = Get-AzResourceGroupDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError
                if ($resultsError) {
                    Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject
                    if ($resultsError.exception.InnerException.Message -match 'InvalidTemplate') {
                        Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateParameterError' -Target $scopeObject
                        $invalidTemplate = $true
                    }
                }
                elseif ($results.Error) {
                    Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject
                    return
                }
                else {
                    Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject
                    Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject
                    $output = 'WhatIf Results:{0}```json{0}{1}{0}```{0}' -f [environment]::NewLine, ($results.Changes | ConvertTo-Json -Depth 5)
                    Set-Content -Path '/tmp/OUTPUT.md' -Value $output -WhatIf:$false
                }

                $parameters.Name = $DeploymentName
                if ($PSCmdlet.ShouldProcess("Start ResourceGroup Deployment?")) {
                    if (-not $invalidTemplate) {
                        New-AzResourceGroupDeployment @parameters
                    }
                }
                else {
                    # Exit deployment
                    Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf'
                }
            }
        }
        #endregion Resource Group
        #region Subscription
        elseif ($scopeObject.subscription) {
            $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion'
            Write-PSFMessage -String 'New-AzOpsDeployment.Subscription.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject

            if ((Get-AzContext).Subscription.Id -ne $scopeObject.subscription) {
                Set-AzOpsContext -ScopeObject $scopeObject
            }

            $parameters = @{
                'TemplateFile'                = $TemplateFilePath
                'Location'                    = $defaultDeploymentRegion
                'SkipTemplateParameterPrompt' = $true
            }
            if ($TemplateParameterFilePath) {
                $parameters.TemplateParameterFile = $TemplateParameterFilePath
            }

            $results = Get-AzSubscriptionDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError
            if ($resultsError) {
                Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject
                if ($resultsError.exception.InnerException.Message -match 'InvalidTemplate') {
                    $invalidTemplate = $true
                }
            }
            elseif ($results.Error) {
                Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject
                return
            }
            else {
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject
                $output = 'WhatIf Results:{0}```json{0}{1}{0}```{0}' -f [environment]::NewLine, ($results.Changes | ConvertTo-Json -Depth 5)
                Set-Content -Path '/tmp/OUTPUT.md' -Value $output -WhatIf:$false
            }

            $parameters.Name = $DeploymentName
            if ($PSCmdlet.ShouldProcess("Start Subscription Deployment?")) {
                if (-not $invalidTemplate) {
                    New-AzSubscriptionDeployment @parameters
                }
            }
            else {
                # Exit deployment
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf'
            }
        }
        #endregion Subscription
        #region Management Group
        elseif ($scopeObject.managementGroup) {
            $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion'
            Write-PSFMessage -String 'New-AzOpsDeployment.ManagementGroup.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject

            $parameters = @{
                'TemplateFile'                = $TemplateFilePath
                'Location'                    = $defaultDeploymentRegion
                'ManagementGroupId'           = $scopeObject.managementgroup
                'SkipTemplateParameterPrompt' = $true
            }
            if ($TemplateParameterFilePath) {
                $parameters.TemplateParameterFile = $TemplateParameterFilePath
            }

            $results = Get-AzManagementGroupDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError
            if ($resultsError) {
                Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject
                if ($resultsError.exception.InnerException.Message -match 'InvalidTemplate') {
                    $invalidTemplate = $true
                }
            }
            elseif ($results.Error) {
                Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject
                return
            }
            else {
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject
                $output = 'WhatIf Results:{0}```json{0}{1}{0}```{0}' -f [environment]::NewLine, ($results.Changes | ConvertTo-Json -Depth 5)
                Set-Content -Path '/tmp/OUTPUT.md' -Value $output -WhatIf:$false
            }

            $parameters.Name = $DeploymentName
            if ($PSCmdlet.ShouldProcess("Start ManagementGroup Deployment?")) {
                if (-not $invalidTemplate) {
                    New-AzManagementGroupDeployment @parameters
                }
            }
            else {
                # Exit deployment
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf'
            }
        }
        #endregion Management Group
        #region Root
        elseif ($scopeObject.type -eq 'root' -and $scopeObject.scope -eq '/') {
            $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion'
            Write-PSFMessage -String 'New-AzOpsDeployment.Root.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject

            $parameters = @{
                'TemplateFile'                = $TemplateFilePath
                'location'                    = $defaultDeploymentRegion
                'SkipTemplateParameterPrompt' = $true
            }
            if ($TemplateParameterFilePath) {
                $parameters.TemplateParameterFile = $TemplateParameterFilePath
            }

            $results = Get-AzTenantDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError
            if ($resultsError) {
                Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject
            }
            elseif ($results.Error) {
                Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject
                return
            }
            else {
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject
                $output = 'WhatIf Results:{0}```json{0}{1}{0}```{0}' -f [environment]::NewLine, ($results.Changes | ConvertTo-Json -Depth 5)
                Set-Content -Path '/tmp/OUTPUT.md' -Value $output -WhatIf:$false
            }

            $parameters.Name = $DeploymentName
            if ($PSCmdlet.ShouldProcess("Start Tenant Deployment?")) {
                New-AzTenantDeployment @parameters
            }
            else {
                # Exit deployment
                Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf'
            }
        }
        #endregion Root
        #region Unidentified
        else {
            Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Unidentified' -Target $scopeObject -StringValues $scopeObject
        }
        #endregion Unidentified
        #endregion Process Scope
    }
}

function New-AzOpsScope {

    <#
        .SYNOPSIS
            Returns an AzOpsScope for a path or for a scope
        .DESCRIPTION
            Returns an AzOpsScope for a path or for a scope
        .PARAMETER Scope
            The scope for which to return a scope object.
        .PARAMETER Path
            The path from which to build a scope.
        .PARAMETER StatePath
            The root path to where the entire state is being built in.
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
        .EXAMPLE
            > New-AzOpsScope -Scope "/providers/Microsoft.Management/managementGroups/3fc1081d-6105-4e19-b60c-1ec1252cf560"
            Return AzOpsScope for a root Management Group scope scope in Azure:
            scope : /providers/Microsoft.Management/managementGroups/3fc1081d-6105-4e19-b60c-1ec1252cf560
            type : managementGroups
            name : 3fc1081d-6105-4e19-b60c-1ec1252cf560
            statepath : C:\git\cet-northstar\azops\3fc1081d-6105-4e19-b60c-1ec1252cf560\.AzState\Microsoft.Management_managementGroups-3fc1081d-6105-4e19-b60c-1ec1252cf560.parameters.json
            managementgroup : 3fc1081d-6105-4e19-b60c-1ec1252cf560
            managementgroupDisplayName : 3fc1081d-6105-4e19-b60c-1ec1252cf560
            subscription :
            subscriptionDisplayName :
            resourcegroup :
            resourceprovider :
            resource :
        .EXAMPLE
            > New-AzOpsScope -path "C:\Users\jodahlbo\git\CET-NorthStar\azops\Tenant Root Group\Non-Production Subscriptions\Dalle MSDN MVP\365lab-dcs"
            Return AzOpsScope for a filepath
        .INPUTS
            Scope
        .INPUTS
            Path
        .OUTPUTS
            [AzOpsScope]
    #>


    #[OutputType([AzOpsScope])]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ParameterSetName = "scope")]
        [string]
        [ValidateScript( { $null -ne $script:AzOpsAzManagementGroup -or $script:AzOpsSubscription })]
        $Scope,

        [Parameter(ParameterSetName = "pathfile", ValueFromPipeline = $true)]
        [string]
        $Path,

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State')
    )
    process {
        Write-PSFMessage -Level Debug -String 'New-AzOpsScope.Starting'

        switch ($PSCmdlet.ParameterSetName) {
            scope {
                Invoke-PSFProtectedCommand -ActionString 'New-AzOpsScope.Creating.FromScope' -ActionStringValues $Scope -Target $Scope -ScriptBlock {
                    [AzOpsScope]::new($Scope, $StatePath)
                } -EnableException $true -PSCmdlet $PSCmdlet
            }
            pathfile {
                if (-not (Test-Path $Path)) {
                    Stop-PSFFunction -String 'New-AzOpsScope.Path.NotFound' -StringValues $Path -EnableException $true -Cmdlet $PSCmdlet
                }
                $Path = Resolve-PSFPath -Path $Path -SingleItem -Provider FileSystem
                $StatePathValidator = Resolve-PSFPath -Path $StatePath -SingleItem -Provider FileSystem
                if (-not $Path.StartsWith($StatePathValidator)) {
                    Stop-PSFFunction -String 'New-AzOpsScope.Path.InvalidRoot' -StringValues $Path, $StatePath -EnableException $true -Cmdlet $PSCmdlet
                }
                Invoke-PSFProtectedCommand -ActionString 'New-AzOpsScope.Creating.FromFile' -Target $Path -ScriptBlock {
                    [AzOpsScope]::new($(Get-Item -Path $Path), $StatePath)
                } -EnableException $true -PSCmdlet $PSCmdlet
            }
        }
    }
}


function New-AzOpsStateDeployment {

    <#
        .SYNOPSIS
            Deploys a set of ARM templates into Azure.
        .DESCRIPTION
            Deploys a set of ARM templates into Azure.
            Define the state using Initialize-AzOpsRepository and maintain it via:
            - Invoke-AzOpsGitPull
            - Invoke-AzOpsGitPush
        .PARAMETER FileName
            Root path from which to deploy.
        .PARAMETER StatePath
            The overall path of the state to deploy.
        .EXAMPLE
            > New-StateDeployment -FileName $fileName -StatePath $StatePath
            Deploys the specified set of ARM templates into Azure.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({ Test-Path $_ })]
        $FileName,

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State')
    )

    begin {
        $subscriptions = Get-AzSubscription
        $enrollmentAccounts = Get-AzEnrollmentAccount
    }

    process {
        Write-PSFMessage -String 'New-AzOpsStateDeployment.Processing' -StringValues $FileName
        $scopeObject = New-AzOpsScope -Path (Get-Item -Path $FileName).FullName -StatePath $StatePath

        if (-not $scopeObject.Type) {
            Write-PSFMessage -Level Warning -String 'New-AzOpsStateDeployment.InvalidScope' -StringValues $FileName -Target $scopeObject
            return
        }
        #TODO: Clarify whether this exclusion was intentional
        if ($scopeObject.Type -ne 'subscriptions') { return }

        #region Process Subscriptions
        if ($FileName -match '/*.subscription.json$') {
            Write-PSFMessage -String 'New-AzOpsStateDeployment.Subscription' -StringValues $FileName -Target $scopeObject
            $subscription = $subscriptions | Where-Object Name -EQ $scopeObject.subscriptionDisplayName

            #region Subscription needs to be created
            if (-not $subscription) {
                Write-PSFMessage -String 'New-AzOpsStateDeployment.Subscription.New' -StringValues $FileName -Target $scopeObject

                if (-not $enrollmentAccounts) {
                    Write-PSFMessage -Level Error -String 'New-AzOpsStateDeployment.NoEnrollmentAccount' -Target $scopeObject
                    Write-PSFMessage -Level Error -String 'New-AzOpsStateDeployment.NoEnrollmentAccount.Solution' -Target $scopeObject
                    return
                }

                if ($cfgEnrollmentAccount = Get-PSFConfigValue -FullName 'AzOps.Core.EnrollmentAccountPrincipalName') {
                    Write-PSFMessage -String 'New-AzOpsStateDeployment.EnrollmentAccount.Selected' -StringValues $cfgEnrollmentAccount
                    $enrollmentAccountObjectId = ($enrollmentAccounts | Where-Object PrincipalName -eq $cfgEnrollmentAccount).ObjectId
                }
                else {
                    Write-PSFMessage -String 'New-AzOpsStateDeployment.EnrollmentAccount.First' -StringValues @($enrollmentAccounts)[0].PrincipalName
                    $enrollmentAccountObjectId = @($enrollmentAccounts)[0].ObjectId
                }

                Invoke-PSFProtectedCommand -ActionString 'New-AzOpsStateDeployment.Subscription.Creating' -ActionStringValues $scopeObject.Name -ScriptBlock {
                    $subscription = New-AzSubscription -Name $scopeObject.Name -OfferType (Get-PSFConfigValue -FullName 'AzOps.Core.OfferType') -EnrollmentAccountObjectId $enrollmentAccountObjectId -ErrorAction Stop
                    $subscriptions = @($subscriptions) + @($subscription)
                } -Target $scopeObject -EnableException $true -PSCmdlet $PSCmdlet

                Invoke-PSFProtectedCommand -ActionString 'New-AzOpsStateDeployment.Subscription.AssignManagementGroup' -ActionStringValues $subscription.Name, $scopeObject.ManagementGroupDisplayName -ScriptBlock {
                    New-AzManagementGroupSubscription -GroupName $scopeObject.ManagementGroup -SubscriptionId $subscription.SubscriptionId -ErrorAction Stop
                } -Target $scopeObject -EnableException $true -PSCmdlet $PSCmdlet
            }
            #endregion Subscription needs to be created
            #region Subscription exists already
            else {
                Write-PSFMessage -String 'New-AzOpsStateDeployment.Subscription.Exists' -StringValues $subscription.Name, $subscription.Id -Target $scopeObject
                Invoke-PSFProtectedCommand -ActionString 'New-AzOpsStateDeployment.Subscription.AssignManagementGroup' -ActionStringValues $subscription.Name, $scopeObject.ManagementGroupDisplayName -ScriptBlock {
                    New-AzManagementGroupSubscription -GroupName $scopeObject.ManagementGroup -SubscriptionId $subscription.SubscriptionId -ErrorAction Stop
                } -Target $scopeObject -EnableException $true -PSCmdlet $PSCmdlet
            }
            #endregion Subscription exists already
        }
        if ($FileName -match '/*.providerfeatures.json$') {
            Register-AzOpsProviderFeature -FileName $FileName -ScopeObject $scopeObject
        }
        if ($FileName -match '/*.resourceproviders.json$') {
            Register-AzOpsResourceProvider -FileName $FileName -ScopeObject $scopeObject
        }
        #endregion Process Subscriptions
    }

}

function Register-AzOpsProviderFeature {

    <#
        .SYNOPSIS
            Registers a provider feature from ARM.
        .DESCRIPTION
            Registers a provider feature from ARM.
        .PARAMETER FileName
            Path to the ARM template file representing a provider feature.
        .PARAMETER ScopeObject
            The current AzOps scope.
        .EXAMPLE
            PS C:\> Register-ProviderFeature -FileName $file -ScopeObject $scopeObject
            Registers a provider feature from ARM.
    #>


    [CmdletBinding()]
    param (
        [string]
        $FileName,

        [AzOpsScope]
        $ScopeObject
    )

    process {
        #TODO: Clarify original function design intent

        # Get Subscription ID from scope (since Subscription ID is not available for Resource Groups and Resources)
        Write-PSFMessage -String 'Register-AzOpsProviderFeature.Processing' -StringValues $ScopeObject, $FileName -Target $ScopeObject
        $currentContext = Get-AzContext
        if ($ScopeObject.Subscription -and $currentContext.Subscription.Id -ne $ScopeObject.Subscription) {
            Write-PSFMessage -String 'Register-AzOpsProviderFeature.Context.Switching' -StringValues $currentContext.Subscription.Name, $CurrentAzContext.Subscription.Id, $ScopeObject.Subscription, $ScopeObject.Name -Target $ScopeObject
            try {
                $null = Set-AzContext -SubscriptionId $ScopeObject.Subscription -ErrorAction Stop
            }
            catch {
                Stop-PSFFunction -String 'Register-AzOpsProviderFeature.Context.Failed' -StringValues $ScopeObject.SubscriptionDisplayName -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet -Target $ScopeObject
                throw "Couldn't switch context $_"
            }
        }

        $providerFeatures = Get-Content  $FileName | ConvertFrom-Json
        foreach ($providerFeature in $providerFeatures) {
            if ($ProviderFeature.FeatureName -and $ProviderFeature.ProviderName) {
                Write-PSFMessage -String 'Register-AzOpsProviderFeature.Provider.Feature' -StringValues $ProviderFeature.FeatureName, $ProviderFeature.ProviderName -Target $ScopeObject
                Register-AzProviderFeature -Confirm:$false -ProviderNamespace $ProviderFeature.ProviderName -FeatureName $ProviderFeature.FeatureName
            }
        }
    }

}

function Register-AzOpsResourceProvider {

    <#
        .SYNOPSIS
            Registers an azure resource provider.
        .DESCRIPTION
            Registers an azure resource provider.
            Assumes an ARM definition of a resource provider as input.
        .PARAMETER FileName
            The path to the file containing an ARM template defining a resource provider.
        .PARAMETER ScopeObject
            The current AzOps scope.
        .EXAMPLE
            PS C:\> Register-ResourceProvider -FileName $fileName -ScopeObject $scopeObject
            Registers an azure resource provider.
    #>


    [CmdletBinding()]
    param (
        [string]
        $FileName,

        [AzOpsScope]
        $ScopeObject
    )

    process {
        Write-PSFMessage -String 'Register-AzOpsResourceProvider.Processing' -StringValues $ScopeObject, $FileName -Target $ScopeObject
        $currentContext = Get-AzContext
        if ($ScopeObject.Subscription -and $currentContext.Subscription.Id -ne $ScopeObject.Subscription) {
            Write-PSFMessage -String 'Register-AzOpsResourceProvider.Context.Switching' -StringValues $currentContext.Subscription.Name, $CurrentAzContext.Subscription.Id, $ScopeObject.Subscription, $ScopeObject.Name -Target $ScopeObject
            try {
                $null = Set-AzContext -SubscriptionId $ScopeObject.Subscription -ErrorAction Stop
            }
            catch {
                Stop-PSFFunction -String 'Register-AzOpsResourceProvider.Context.Failed' -StringValues $ScopeObject.SubscriptionDisplayName -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet -Target $ScopeObject
                throw "Couldn't switch context $_"
            }
        }

        $resourceproviders = Get-Content  $FileName | ConvertFrom-Json
        foreach ($resourceprovider  in $resourceproviders | Where-Object RegistrationState -eq 'Registered') {
            if (-not $resourceprovider.ProviderNamespace) { continue }

            Write-PSFMessage -String 'Register-AzOpsResourceProvider.Provider.Register' -StringValues $resourceprovider.ProviderNamespace
            Write-AzOpsLog -Level Verbose -Topic "Register-AzOpsResourceProvider" -Message "Registering Provider $($resourceprovider.ProviderNamespace)"
            Register-AzResourceProvider -Confirm:$false -Pre -ProviderNamespace $resourceprovider.ProviderNamespace
        }
    }

}

function Save-AzOpsManagementGroupChildren {

    <#
        .SYNOPSIS
            Recursively build/change Management Group hierarchy in file system from provided scope.
        .DESCRIPTION
            Recursively build/change Management Group hierarchy in file system from provided scope.
        .PARAMETER Scope
            Scope to discover - assumes [AzOpsScope] object
        .PARAMETER StatePath
            The root path to where the entire state is being built in.
        .PARAMETER Confirm
            If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
        .PARAMETER WhatIf
            If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
        .EXAMPLE
            > Save-AzOpsManagementGroupChildren -Scope (New-AzOpsScope -scope /providers/Microsoft.Management/managementGroups/contoso)
            Discover Management Group hierarchy from scope
        .INPUTS
            AzOpsScope
        .OUTPUTS
            Management Group hierarchy in file system
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType()]
    param (
        [Parameter(Mandatory = $true)]
        $Scope,

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State')
    )

    process {
        Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Starting'
        Invoke-PSFProtectedCommand -ActionString 'Save-AzOpsManagementGroupChildren.Creating.Scope' -Target $Scope -ScriptBlock {
            $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction SilentlyContinue -Confirm:$false
        } -EnableException $true -PSCmdlet $PSCmdlet
        if (-not $scopeObject) { return } # In case -WhatIf is used

        Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Processing' -StringValues $scopeObject.Scope

        # Construct all file paths for scope
        $scopeStatepath = $scopeObject.StatePath
        $statepathFileName = [IO.Path]::GetFileName($scopeStatepath)
        $statepathDirectory = [IO.Path]::GetDirectoryName($scopeStatepath)
        $statepathScopeDirectory = [IO.Directory]::GetParent($statepathDirectory).ToString()
        $statepathScopeDirectoryParent = [IO.Directory]::GetParent($statepathScopeDirectory).ToString()

        Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.StatePath' -StringValues $scopeStatepath
        Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.FileName' -StringValues $statepathFileName
        Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.Directory' -StringValues $statepathDirectory
        Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectory' -StringValues $statepathScopeDirectory
        Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectoryParent' -StringValues $statepathScopeDirectoryParent

        # If file is found anywhere in "AzOps.Core.State", ensure that it is at the right scope or else it doesn't matter
        if (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName) {
            # If the file is found in AzOps State
            $exisitingScopePath = (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName).Directory

            # Looking at parent of parent if AutoGeneratedTemplateFolderPath is sub-directory, looking for parent (scope folder) of parent (actual parent in Azure)
            if ( ((Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -notin '.') -and
                $exisitingScopePath.Parent.Parent.FullName -ne $statepathScopeDirectoryParent) {
                if ($exisitingScopePath.Parent.FullName -ne $statepathScopeDirectoryParent) {
                    Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Moving.Source' -StringValues $exisitingScopePath
                    Move-Item -Path $exisitingScopePath.Parent -Destination $statepathScopeDirectoryParent -Force
                    Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Moving.Destination' -StringValues $statepathScopeDirectoryParent
                }
            }

            # Files might be at the right scope but not in right AutoGeneratedTemplateFolderPath e.g. when AutoGeneratedTemplateFolderPath is changed.
            if (-not (Test-Path $statepathDirectory)) {
                New-Item -Path $statepathDirectory -ItemType Directory -Force | out-null
            }
            # For all the files in AutoGeneratedTemplateFolderPath directory, only moving files that are auto generated
            Get-ChildItem -Path (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName).Directory -File -Filter 'Microsoft.*' | Move-Item -Destination $statepathDirectory -Force

        }
        switch ($scopeObject.Type) {
            managementGroups {
                ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object { $_.Name -eq $scopeObject.managementgroup }) -ExportPath $scopeObject.statepath -StatePath $StatePath
                foreach ($child in $script:AzOpsAzManagementGroup.Where{ $_.Name -eq $scopeObject.managementgroup }.Children) {
                    if ($child.Type -eq '/subscriptions') {
                        if ($script:AzOpsSubscriptions.id -contains $child.Id) {
                            Save-AzOpsManagementGroupChildren -Scope $child.Id -StatePath $StatePath
                        }
                        else {
                            Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Subscription.NotFound' -StringValues $child.Name
                        }
                    }
                    else {
                        Save-AzOpsManagementGroupChildren -Scope $child.Id -StatePath $StatePath
                    }
                }
            }
            subscriptions {
                ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup.children | Where-Object Name -eq $scopeObject.name) -ExportPath $scopeObject.statepath -StatePath $StatePath
            }
        }
    }

}


function Set-AzOpsContext {

    <#
        .SYNOPSIS
            Changes the currently active azure context to the subscription of the specified scope object.
        .DESCRIPTION
            Changes the currently active azure context to the subscription of the specified scope object.
        .PARAMETER ScopeObject
            The scope object into which context to change.
        .EXAMPLE
            > Set-AzOpsContext -ScopeObject $scopeObject
            Changes the current context to the subscription of $scopeObject.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [AzOpsScope]
        $ScopeObject
    )

    begin {
        $context = Get-AzContext
    }

    process {
        if (-not $ScopeObject.Subscription) { return }

        if ($context.Subscription.Id -ne $ScopeObject.Subscription) {
            Write-PSFMessage -String 'Set-AzOpsContext.Change' -StringValues $context.Subscription.Name, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription
            Set-AzContext -SubscriptionId $scopeObject.Subscription
        }
    }
}

function Initialize-AzOpsEnvironment {

    <#
        .SYNOPSIS
            Initializes the execution context of the module.
        .DESCRIPTION
            Initializes the execution context of the module.
            This is used by all other commands.
            It prepares / caches tenant, subscription and management group data.
        .PARAMETER IgnoreContextCheck
            Whether it should validate the azure contexts available or not.
        .PARAMETER InvalidateCache
            If data was already cached from a previous execution, execute again anyway?
        .PARAMETER ExcludedSubOffer
            Subscription filter.
            Subscriptions from the listed offerings will be ignored.
            Generally used to prevent using trial subscriptions, but can be adapted for other limitations.
        .PARAMETER ExcludedSubState
            Subscription filter.
            Subscriptions in the listed states will be ignored.
            For example, by default, disabled subscriptions will not be processed.
        .PARAMETER PartialMgDiscoveryRoot
            Custom search roots under which to detect management groups.
            Used for partial management group discovery.
            Must be used in combination with -PartialMgDiscovery
        .EXAMPLE
            > Initialize-AzOpsEnvironment
            Initializes the default execution context of the module.
    #>


    [CmdletBinding()]
    param (
        [switch]
        $IgnoreContextCheck = (Get-PSFConfigValue -FullName 'AzOps.Core.IgnoreContextCheck'),

        [switch]
        $InvalidateCache = (Get-PSFConfigValue -FullName 'AzOps.Core.InvalidateCache'),

        [string[]]
        $ExcludedSubOffer = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubOffer'),

        [string[]]
        $ExcludedSubState = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubState'),

        [string[]]
        $PartialMgDiscoveryRoot = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot')
    )

    begin {
        Assert-AzOpsWindowsLongPath -Cmdlet $PSCmdlet
        Assert-AzOpsJqDependency -Cmdlet $PSCmdlet

        $allAzContext = Get-AzContext -ListAvailable
        if (-not $allAzContext) {
            Stop-PSFFunction -String 'Initialize-AzOpsEnvironment.AzureContext.No' -EnableException $true -Cmdlet $PSCmdlet
        }
        $azContextTenants = @($AllAzContext.Tenant.Id | Sort-Object -Unique)
        if (-not $IgnoreContextCheck -and $azContextTenants.Count -gt 1) {
            Stop-PSFFunction -String 'Initialize-AzOpsEnvironment.AzureContext.TooMany' -StringValues $azContextTenants.Count, ($azContextTenants -join ',') -EnableException $true -Cmdlet $PSCmdlet
        }
    }

    process {
        # If data exists and we don't want to rebuild the data cache, no point in continuing
        if (-not $InvalidateCache -and $script:AzOpsAzManagementGroup -and $script:AzOpsSubscriptions) {
            Write-PSFMessage -String 'Initialize-AzOpsEnvironment.UsingCache'
            return
        }

        #region Initialize & Prepare
        Write-PSFMessage -String 'Initialize-AzOpsEnvironment.Processing'
        $currentAzContext = Get-AzContext
        $tenantId = $currentAzContext.Tenant.Id
        $rootScope = '/providers/Microsoft.Management/managementGroups/{0}' -f $tenantId

        Write-PSFMessage -String 'Initialize-AzOpsEnvironment.Initializing'
        if (-not (Test-Path -Path (Get-PSFConfigValue -FullName 'AzOps.Core.State'))) {
            $null = New-Item -path (Get-PSFConfigValue -FullName 'AzOps.Core.State') -Force -ItemType directory
        }
        $script:AzOpsSubscriptions = Get-AzOpsSubscription -ExcludedOffers $ExcludedSubOffer -ExcludedStates $ExcludedSubState -TenantId $tenantId
        $script:AzOpsResourceProvider = Get-AzResourceProvider
        $script:AzOpsAzManagementGroup = @()
        $script:AzOpsPartialRoot = @()
        #endregion Initialize & Prepare

        #region Management Group Processing
        try {
            $managementGroups = Get-AzManagementGroup -ErrorAction Stop
        }
        catch {
            Write-PSFMessage -Level Warning -String 'Initialize-AzOpsEnvironment.ManagementGroup.NoManagementGroupAccess'
            return
        }

        #region Validate root '/' permissions
        if ($currentAzContext.Account.Type -eq "User") {
            $rootPermissions = Get-AzRoleAssignment -UserPrincipalName $currentAzContext.Account.Id -Scope "/" -ErrorAction SilentlyContinue
        }
        else {
            $rootPermissions = Get-AzRoleAssignment -ObjectId (Get-AzADServicePrincipal -ApplicationId $currentAzContext.Account.Id).Id -Scope "/" -ErrorAction SilentlyContinue
        }
        if (-not $rootPermissions) {
            Write-PSFMessage -String 'Initialize-AzOpsEnvironment.ManagementGroup.NoRootPermissions' -StringValues $currentAzContext.Account.Id
            $PartialMgDiscovery = $true
        }
        else {
            $PartialMgDiscovery = $false
        }
        #endregion Validate root '/' permissions

        #region Partial Discovery
        if ($PartialMgDiscoveryRoot) {
            Write-PSFMessage -String 'Initialize-AzOpsEnvironment.ManagementGroup.PartialDiscovery'
            $PartialMgDiscovery = $true
            $managementGroups = @()
            foreach ($managementRoot in $PartialMgDiscoveryRoot) {
                $managementGroups += [pscustomobject]@{ Name = $managementRoot }
                $script:AzOpsPartialRoot += Get-AzManagementGroup -GroupId $managementRoot -Recurse -Expand -WarningAction SilentlyContinue
            }
        }
        #endregion Partial Discovery

        #region Management Group Resolution
        Write-PSFMessage -String 'Initialize-AzOpsEnvironment.ManagementGroup.Resolution' -StringValues $managementGroups.Count
        $tempResolved = foreach ($mgmtGroup in $managementGroups) {
            Write-PSFMessage -String 'Initialize-AzOpsEnvironment.ManagementGroup.Expanding' -StringValues $mgmtGroup.Name
            Get-AzOpsManagementGroups -ManagementGroup $mgmtGroup.Name -PartialDiscovery:$PartialMgDiscovery
        }
        $script:AzOpsAzManagementGroup = $tempResolved | Sort-Object -Property Id -Unique
        #endregion Management Group Resolution
        #endregion Management Group Processing

        Write-PSFMessage -String 'Initialize-AzOpsEnvironment.Processing.Completed'
    }

}


function Initialize-AzOpsRepository {

    <#
        .SYNOPSIS
            Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment.
        .DESCRIPTION
            Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment.
        .PARAMETER SkipPolicy
            Skip discovery of policies for better performance.
        .PARAMETER SkipRole
            Skip discovery of role.
        .PARAMETER SkipResourceGroup
            Skip discovery of resource groups
        .PARAMETER SkipResource
            Skip discovery of resources inside resource groups.
        .PARAMETER InvalidateCache
            Invalidate cached subscriptions and Management Groups and do a full discovery.
        .PARAMETER ExportRawTemplate
            Export generic templates without embedding them in the parameter block.
        .PARAMETER Rebuild
            Delete all AutoGeneratedTemplateFolderPath folders inside AzOpsState directory.
        .PARAMETER Force
            Delete $script:AzOpsState directory.
        .PARAMETER PartialMgDiscoveryRoot
            The subset of management groups in the entire hierarchy with which to work.
            Needed when lacking root access.
        .PARAMETER StatePath
            The root folder under which to write the resource json.
        .EXAMPLE
            > Initialize-AzOpsRepository
            Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment.
    #>


    [CmdletBinding()]
    param (
        [switch]
        $SkipPolicy = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipPolicy'),

        [switch]
        $SkipRole = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipRole'),

        [switch]
        $SkipResourceGroup = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResourceGroup'),

        [switch]
        $SkipResource = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResource'),

        [switch]
        $InvalidateCache = (Get-PSFConfigValue -FullName 'AzOps.Core.InvalidateCache'),

        [switch]
        $ExportRawTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.ExportRawTemplate'),

        [switch]
        $Rebuild,

        [switch]
        $Force,

        [string[]]
        $PartialMgDiscoveryRoot = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot'),

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State'),

        [string]
        $TemplateParameterFileSuffix = (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')
    )

    begin {
        #region Initialize & Prepare
        Write-PSFMessage -String 'Initialize-AzOpsRepository.Initialization.Starting'
        if (-not $SkipRole) {
            try {
                Write-PSFMessage -String 'Initialize-AzOpsRepository.Validating.UserRole'
                Get-AzADUser -First 1 -ErrorAction Stop
                Write-PSFMessage -String 'Initialize-AzOpsRepository.Validating.UserRole.Success'
            }
            catch {
                Write-PSFMessage -Level Warning -String 'Initialize-AzOpsRepository.Validating.UserRole.Failed'
                $SkipRole = $true
            }
        }

        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include InvalidateCache, PartialMgDiscovery, PartialMgDiscoveryRoot
        Initialize-AzOpsEnvironment @parameters

        Assert-AzOpsInitialization -Cmdlet $PSCmdlet -StatePath $StatePath

        $tenantId = (Get-AzContext).Tenant.Id
        Write-PSFMessage -String 'Initialize-AzOpsRepository.Tenant' -StringValues $tenantId
        Write-PSFMessage -String 'Initialize-AzOpsRepository.TemplateParameterFileSuffix' -StringValues (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')

        Write-PSFMessage -String 'Initialize-AzOpsRepository.Initialization.Completed'
        $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
        #endregion Initialize & Prepare
    }

    process {
        #region Existing Content
        if (Test-Path $StatePath) {
            $migrationRequired = (Get-ChildItem -Recurse -Force -Path $StatePath -File | Where-Object {
                    $_.Name -like $("Microsoft.Management_managementGroups-" + $tenantId + $TemplateParameterFileSuffix)
                } | Select-Object -ExpandProperty FullName -First 1) -notmatch '\((.*)\)'
            if ($migrationRequired) {
                Write-PSFMessage -String 'Initialize-AzOpsRepository.Migration.Required'
            }

            if ($Force -or $migrationRequired) {
                Invoke-PSFProtectedCommand -ActionString 'Initialize-AzOpsRepository.Deleting.State' -ActionStringValues $StatePath -Target $StatePath -ScriptBlock {
                    Remove-Item -Path $StatePath -Recurse -Force -Confirm:$false -ErrorAction Stop
                } -EnableException $true -PSCmdlet $PSCmdlet
            }
            if ($Rebuild) {
                Invoke-PSFProtectedCommand -ActionString 'Initialize-AzOpsRepository.Rebuilding.State' -ActionStringValues $StatePath -Target $StatePath -ScriptBlock {
                    Get-ChildItem -Path $StatePath  -File -Recurse -Force -Filter 'Microsoft.*_*.json' | Remove-Item -Force -Recurse -Confirm:$false -ErrorAction Stop
                } -EnableException $true -PSCmdlet $PSCmdlet
            }
        }
        #endregion Existing Content

        #region Root Scopes
        $rootScope = '/providers/Microsoft.Management/managementGroups/{0}' -f $tenantId
        if ($script:AzOpsPartialRoot.id) {
            $rootScope = $script:AzOpsPartialRoot.id | Sort-Object -Unique
        }

        if ($rootScope -and $script:AzOpsAzManagementGroup) {
            foreach ($root in $rootScope) {
                # Create AzOpsState Structure recursively
                Save-AzOpsManagementGroupChildren -Scope $root -StatePath $StatePath

                # Discover Resource at scope recursively
                $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include SkipPolicy, SkipRole, SkipResourceGroup, SkipResource, ExportRawTemplate, StatePath
                Get-AzOpsResourceDefinition -Scope $root @parameters
            }
        }
        else {
            # If no management groups are found, iterate through each subscription
            foreach ($subscription in $script:AzOpsSubscriptions) {
                $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include SkipPolicy, SkipRole, SkipResourceGroup, SkipResource, ExportRawTemplate, StatePath
                Get-AzOpsResourceDefinition -Scope $subscription.id @parameters
            }

        }
        #endregion Root Scopes
    }

    end {
        $stopWatch.Stop()
        Write-PSFMessage -String 'Initialize-AzOpsRepository.Duration' -StringValues $stopWatch.Elapsed -Data @{ Elapsed = $stopWatch.Elapsed }
    }

}

Register-PSFConfigValidation -Name "stringorempty" -ScriptBlock {

    param (
        $Value
    )

    $Result = New-Object PSObject -Property @{
        Success = $True
        Value   = $null
        Message = ""
    }

    try {
        [string]$data = $Value
    }
    catch {
        $Result.Message = "Not a string: $Value"
        $Result.Success = $False
        return $Result
    }

    if ([string]::IsNullOrEmpty($data)) {
        $data = ""
    }

    if ($data -eq $Value.GetType().FullName) {
        $Result.Message = "Is an object with no proper string representation: $Value"
        $Result.Success = $False
        return $Result
    }

    $Result.Value = $data

    return $Result

}

Set-PSFConfig -Module AzOps -Name Core.AllInOneTemplate -Value $false -Initialize -Validation bool -Description '-'
Set-PSFConfig -Module AzOps -Name Core.AutoGeneratedTemplateFolderPath -Value "." -Initialize -Validation string -Description 'Auto-Genereated Template Folder Path i.e. ./Az'
Set-PSFConfig -Module AzOps -Name Core.AutoInitialize -Value $false -Initialize -Validation bool -Description '-'
Set-PSFConfig -Module AzOps -Name Core.DefaultDeploymentRegion -Value northeurope -Initialize -Validation string -Description 'Default deployment region for state deployments (ARM region, not region where a resource is deployed)'
Set-PSFConfig -Module AzOps -Name Core.EnrollmentAccountPrincipalName -Value '' -Initialize -Validation stringorempty -Description '-'
Set-PSFConfig -Module AzOps -Name Core.ExcludedSubOffer -Value 'AzurePass_2014-09-01', 'FreeTrial_2014-09-01', 'AAD_2015-09-01' -Initialize -Validation stringarray -Description 'Excluded QuotaID'
Set-PSFConfig -Module AzOps -Name Core.ExcludedSubState -Value 'Disabled', 'Deleted', 'Warned', 'Expired' -Initialize -Validation stringarray -Description 'Excluded subscription states'
Set-PSFConfig -Module AzOps -Name Core.ExportRawTemplate -Value $false -Initialize -Validation bool -Description '-'
Set-PSFConfig -Module AzOps -Name Core.IgnoreContextCheck -Value $false -Initialize -Validation bool -Description 'If set to $true, skip AAD tenant validation == 1'
Set-PSFConfig -Module AzOps -Name Core.InvalidateCache -Value $true -Initialize -Validation bool -Description 'Invalidates cache and ensures that Management Groups and Subscriptions are re-discovered'
Set-PSFConfig -Module AzOps -Name Core.JqTemplatePath -Value "$script:ModuleRoot\data\template" -Initialize -Validation string -Description 'default path to search for jq template'
Set-PSFConfig -Module AzOps -Name Core.MainTemplate -Value "$script:ModuleRoot\data\template\template.json" -Initialize -Validation string -Description 'Main template json'
Set-PSFConfig -Module AzOps -Name Core.OfferType -Value 'MS-AZR-0017P' -Initialize -Validation string -Description '-'
Set-PSFConfig -Module AzOps -Name Core.PartialMgDiscoveryRoot -Value @() -Initialize -Validation stringarray -Description 'Used in combination with AZOPS_SUPPORT_PARTIAL_MG_DISCOVERY, example value: "Contoso","Tailspin","Management"'
Set-PSFConfig -Module AzOps -Name Core.SkipPolicy -Value $false -Initialize -Validation bool -Description '-'
Set-PSFConfig -Module AzOps -Name Core.SkipResource -Value $true -Initialize -Validation bool -Description 'Global flag to indicate whether resource should be discovered or not. Requires SkipResourceGroup to be false.'
Set-PSFConfig -Module AzOps -Name Core.SkipResourceGroup -Value $false -Initialize -Validation bool -Description 'Global flag to indicate whether resource group should be discovered or not'
Set-PSFConfig -Module AzOps -Name Core.SkipRole -Value $false -Initialize -Validation bool -Description '-'
Set-PSFConfig -Module AzOps -Name Core.State -Value (Join-Path $pwd -ChildPath "root") -Initialize -Validation string -Description 'Folder to store AzOpsState artefact'
Set-PSFConfig -Module AzOps -Name Core.SubscriptionsToIncludeResourceGroups -Value @('*') -Initialize -Validation stringarray -Description 'Requires SkipResourceGroup to be false. Subscription ID or Display Name that matches the filter. Powershell filter that matches with like operator is supported.'
Set-PSFConfig -Module AzOps -Name Core.TemplateParameterFileSuffix -Value '.json' -Initialize -Validation string -Description 'parameter file suffix to look for'
Set-PSFConfig -Module AzOps -Name Core.ThrottleLimit -Value 10 -Initialize -Validation integer -Description 'Throttle limit used in Foreach-Object -Parallel for resource/subscription discovery'

<#
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 'AzOps.ScriptBlockName' -Scriptblock {

}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "AzOps.Test" -ScriptBlock { 'Test' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Test -Parameter Type -Name AzOps.x
#>


# Module Cache for Subscriptions accessible for the current account
$script:AzOpsSubscriptions = @()
# Module Cache for Management Groups that are in scope for this module
$script:AzOpsAzManagementGroup = @()
# Module Cache for Management Group Roots that are in scope for this module, when accepting partial processing
$script:AzOpsPartialRoot = @()
# Module cache to load resource provider version
$script:AzOpsResourceProvider = $null

Set-PSFFeature -Name PSFramework.Stop-PSFFunction.ShowWarning -Value $true -ModuleName AzOps

if (Get-PSFConfigValue -FullName AzOps.Core.AutoInitialize) {
    if ([runspace]::DefaultRunspace.Id -eq 1) {
        Initialize-AzOpsEnvironment
    }
}


New-PSFLicense -Product 'AzOps' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-11-07") -Text @"
Copyright (c) 2020 Friedrich Weinmann

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.
"@

#endregion Load compiled code