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 AzOpsRoleEligibilityScheduleRequest {
    [string]$ResourceType
    [string]$Name
    [string]$Id
    [hashtable]$Properties

    AzOpsRoleEligibilityScheduleRequest($roleEligibilitySchedule, $roleEligibilityScheduleRequest) {
        $this.Properties = [ordered]@{
            Condition = $roleEligibilitySchedule.Condition
            ConditionVersion = $roleEligibilitySchedule.ConditionVersion
            PrincipalId = $roleEligibilitySchedule.PrincipalId
            RoleDefinitionId = $roleEligibilitySchedule.RoleDefinitionId
            RequestType = $roleEligibilityScheduleRequest.RequestType.ToString()
            ScheduleInfo = [ordered]@{
                Expiration = [ordered]@{
                    EndDateTime = $roleEligibilitySchedule.EndDateTime
                    Duration = $roleEligibilitySchedule.ExpirationDuration
                    ExpirationType = if ($roleEligibilitySchedule.ExpirationType) {$roleEligibilitySchedule.ExpirationType.ToString()}
                }
                StartDateTime  = $roleEligibilitySchedule.StartDateTime
            }
        }
        $this.Id = $roleEligibilitySchedule.RequestId
        $this.Name = $roleEligibilitySchedule.Name
        $this.ResourceType = $roleEligibilityScheduleRequest.Type
    }

    AzOpsRoleEligibilityScheduleRequest($roleEligibilitySchedule) {
        $this.Properties = [ordered]@{
            Condition = $roleEligibilitySchedule.Condition
            ConditionVersion = $roleEligibilitySchedule.ConditionVersion
            PrincipalId = $roleEligibilitySchedule.PrincipalId
            RoleDefinitionId = $roleEligibilitySchedule.RoleDefinitionId
            RequestType = "AdminAssign"
            ScheduleInfo = [ordered]@{
                Expiration = [ordered]@{
                    EndDateTime = $roleEligibilitySchedule.EndDateTime
                    Duration = $roleEligibilitySchedule.ExpirationDuration
                    ExpirationType = if ($roleEligibilitySchedule.ExpirationType) {$roleEligibilitySchedule.ExpirationType.ToString()}
                }
                StartDateTime  = $roleEligibilitySchedule.StartDateTime
            }
        }
        $this.Id = $roleEligibilitySchedule.RequestId
        $this.Name = $roleEligibilitySchedule.Name
        $this.ResourceType = "Microsoft.Authorization/roleEligibilityScheduleRequests"
    }
}

class AzOpsScope {

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

    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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.Constructor' -LogStringValues $scope -FunctionName "AzOpsScope" -ModuleName "AzOps"
        $this.StateRoot = $StateRoot
        if (Test-Path -Path $scope) {
            if ((Get-Item $scope -Force).GetType().ToString() -eq 'System.IO.FileInfo') {
                #Strong confidence based on content - file
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile' -LogStringValues $scope -FunctionName "AzOpsScope" -ModuleName "AzOps"
                $this.InitializeMemberVariablesFromFile($Scope)
            }
            else {
                # Weak confidence based on metadata at scope - directory
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromDirectory' -LogStringValues $scope -FunctionName "AzOpsScope" -ModuleName "AzOps"
                $this.InitializeMemberVariablesFromDirectory($Scope)
            }
        }
        else {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariables' -LogStringValues $scope -FunctionName "AzOpsScope" -ModuleName "AzOps"
            $this.InitializeMemberVariables($Scope)
        }
    }
    # Overridden Constructor used for Child Resource
    AzOpsScope ([string]$Scope, [hashtable]$ChildResource, [string]$StateRoot) {
        <#
            .SYNOPSIS
                Creates an StatePath of Child Resource based on the specified resource ID of ResourceGroup, Resource provider of Resource and Resource name
            .DESCRIPTION
                Creates an StatePath of Child Resource based on the specified resource ID of ResourceGroup, Resource provider of Resource and Resource name
            .PARAMETER Scope
                Scope == ResourceID of Parent resource
            .PARAMETER ChildResource
                The ChildResource contains details of the child resource
            .INPUTS
                None. You cannot pipe objects to Add-Extension.
            .OUTPUTS
                Creates an StatePath of Child Resource
            .EXAMPLE
                New-AzOpsScope -Scope "/subscriptions/7d57452c-d765-4fc6-87ec-6649c37f0a0a/resourceGroups/resourcegroup" -ResourceProvider "Microsoft.Network/virtualHubs/hubRouteTables" -ResourceName "hubroutetable1"
                Using Parent Resource id , Resource provider and Resource name it generates a statepath to place the Child Resource file and parent scope Object
        #>

        $this.StateRoot = $StateRoot
        $this.ChildResource = $ChildResource.resourceProvider + '-' + $ChildResource.resourceName
        # Check and update generated name for invalid filesystem characters and exceeding maximum length
        $this.ChildResource = $this.ChildResource | Remove-AzOpsInvalidCharacter | Set-AzOpsStringLength
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.ChildResource.InitializeMemberVariables' -LogStringValues $ChildResource.ResourceProvider, $ChildResource.ResourceName, $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 -Force).FullName) {
            # Root tenant path
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromDirectory.RootTenant' -LogStringValues $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')".ToLower()
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromDirectory.AutoGeneratedFolderPath' -LogStringValues $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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.Input.FromFileName.ManagementGroup' -LogStringValues $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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.Input.FromFileName.Subscription' -LogStringValues $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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromDirectory.ParentSubscription' -LogStringValues $Path.Parent -FunctionName "InitializeMemberVariablesFromDirectory" -ModuleName "AzOps"

            if ($(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -match $Path.Name) {
                $parent = New-AzOpsScope -Path ($Path.Parent.Parent)
                $rgName = $Path.Parent.Name
            }
            else {
                $parent = New-AzOpsScope -Path ($Path.Parent)
                $rgName = $Path.Name
            }

            $this.InitializeMemberVariables($("/subscriptions/{0}/resourceGroups/{1}" -f $parent.Subscription, $rgName))
        }
        else {
            #Error
            Write-AzOpsMessage -LogLevel Warning -LogString 'AzOpsScope.Input.BadData.UnknownType' -LogStringValues $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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.NotJson' -LogStringValues $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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceId' -LogStringValues $($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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.Id' -LogStringValues $($resourcePath.parameters.input.value.Id) -FunctionName "InitializeMemberVariablesFromFile" -ModuleName "AzOps"
                    $this.InitializeMemberVariables($resourcePath.parameters.input.value.Id)
                    break
                }
                { $_.parameters.input.value.Keys -ccontains "Type" } {
                    # Parameter Files - Determine Resource Type and Name (Management group)
                    # Management group resource id do contain '/provider'
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.Type' -LogStringValues ("$($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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceType' -LogStringValues ($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
                }
                { $_.parameters.input.value.Keys -ccontains "type" } {
                    # Parameter Files - Determine resource type and name (Any ResourceType except management group)
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceType' -LogStringValues ($resourcePath.parameters.input.value.type) -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.type)/$($resourcePath.parameters.input.value.name)")
                    break
                }
                { $_.resources -and
                    $_.resources[0].type -eq 'Microsoft.Management/managementGroups' } {
                    # Template - Management Group
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.managementgroups' -LogStringValues ($_.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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.subscriptions' -LogStringValues ($_.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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.resourceGroups' -LogStringValues ($_.resources[0].name) -FunctionName "InitializeMemberVariablesFromFile" -ModuleName "AzOps"

                    if ($(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -match $Path.Directory.Name) {
                        $currentScope = New-AzOpsScope -Path ($Path.Directory.Parent)
                    }
                    else {
                        $currentScope = New-AzOpsScope -Path ($Path.Directory)
                    }

                    $this.InitializeMemberVariables("$($currentScope.scope)")
                    break
                }
                { $_.resources } {
                    # Template - 1st resource
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromFile.resource' -LogStringValues ($_.resources[0].type), ($_.resources[0].name) -FunctionName "InitializeMemberVariablesFromFile" -ModuleName "AzOps"

                    if ($(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -match $Path.Directory.Name) {
                        $currentScope = New-AzOpsScope -Path ($Path.Directory.Parent)
                    }
                    else {
                        $currentScope = New-AzOpsScope -Path ($Path.Directory)
                    }

                    $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($_.resources[0].type)/$($_.resources[0].name)")
                    break
                }
                Default {
                    # Only show warning about parameter file if parameter file doesn't have deploymentParameters schema defined
                    if ($resourcePath.'$schema' -notmatch 'deploymentParameters.json') {
                        Write-AzOpsMessage -LogLevel Warning -LogString 'AzOpsScope.Input.BadData.TemplateParameterFile' -LogStringValues $Path -FunctionName "AzOpsScope" -ModuleName "AzOps"
                    }
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariablesFromDirectory' -LogStringValues $Path -FunctionName "AzOpsScope" -ModuleName "AzOps"
                    $this.InitializeMemberVariablesFromDirectory($Path.Directory)
                }
            }
        }
    }
    #endregion Constructors

    hidden [void] InitializeMemberVariables([string]$Scope) {
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariables.Start' -LogStringValues ($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.TemplateParameterFileSuffix') -notcontains 'parameters.json' -and
                ("$($this.ResourceProvider)/$($this.Resource)" -in 'Microsoft.Authorization/policyDefinitions', 'Microsoft.Authorization/policySetDefinitions')
            ) {
                $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 ($this.ChildResource -and (-not(Get-PSFConfigValue -FullName AzOps.Core.SkipChildResource))) {
                $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\$($this.ChildResource).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()
            }
            $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 = if($this.Name) { ($script:AzOpsAzManagementGroup | Where-Object Name -eq $this.Name).DisplayName }
            $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-AzOpsMessage -LogLevel InternalComment -LogString 'AzOpsScope.InitializeMemberVariables.End' -LogStringValues ($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.Name -eq $this.GetManagementGroupName()) {
                    $private:mgmtHit = $true
                    return $mgmt.Name
                }
            }
            if (-not $private:mgmtHit) {
                $mgId = $this.Scope -split $this.regex_managementgroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1
                Write-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetManagementGroup.NotFound' -LogStringValues $mgId -FunctionName "AzOpsScope" -ModuleName "AzOps"
                return $mgId
            }
        }
        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 | Remove-AzOpsInvalidCharacter
                return Join-Path $parentPath -ChildPath ($childPath.ToLower())
            }
            else {
                $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name | Remove-AzOpsInvalidCharacter
                return Join-Path $this.StateRoot -ChildPath ($childPath.ToLower())
            }
        }
        else {
            Write-AzOpsMessage -LogLevel Warning -LogString 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' -LogStringValues $managementgroupName -FunctionName "AzOpsScope" -ModuleName "AzOps"
            $assumeNewResource = "azopsscope-assume-new-resource_$managementgroupName"
            return $assumeNewResource.ToLower()
        }
    }

    [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) {
                $mgName = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $mgId).Name
                if ($mgName) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetManagementGroupName.Found.Azure' -LogStringValues $mgName -FunctionName "AzOpsScope" -ModuleName "AzOps"
                    return $mgName
                }
                else {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetManagementGroupName.NotFound' -LogStringValues $mgId -FunctionName "AzOpsScope" -ModuleName "AzOps"
                    return $mgId
                }
            }
        }
        if ($this.Subscription) {
            foreach ($managementGroup in $script:AzOpsAzManagementGroup) {
                foreach ($child in $managementGroup.Children) {
                    if ($child.Type -eq '/subscriptions' -and $child.DisplayName -eq $this.subscriptionDisplayName) {
                        return $managementGroup.Name
                    }
                }
            }
        }
        return $null
    }
    [string] GetAzOpsSubscriptionPath() {
        $childpath = "{0} ({1})" -f $this.SubscriptionDisplayName, $this.Subscription | Remove-AzOpsInvalidCharacter
        if ($script:AzOpsAzManagementGroup) {
            return (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ($childpath).ToLower())
        }
        else {
            return (Join-Path $this.StateRoot -ChildPath ($childpath).ToLower())
        }
    }
    [string] GetAzOpsResourceGroupPath() {
        $childpath = $this.ResourceGroup | Remove-AzOpsInvalidCharacter
        return (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ($childpath).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-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetSubscription.Found' -LogStringValues $sub.Id -FunctionName "AzOpsScope" -ModuleName "AzOps"
                return $sub.subscriptionId
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetSubscription.NotFound' -LogStringValues $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-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetSubscriptionDisplayName.Found' -LogStringValues $sub.displayName -FunctionName "AzOpsScope" -ModuleName "AzOps"
                return $sub.displayName
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetSubscriptionDisplayName.NotFound' -LogStringValues $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-AzOpsMessage -LogLevel Debug -LogString 'AzOpsScope.GetAzOpsResourcePath.Retrieving' -LogStringValues $this.Scope -FunctionName "AzOpsScope" -ModuleName "AzOps"
        $childpath = $this.Name  | Remove-AzOpsInvalidCharacter
        if ($this.Scope -match $this.regex_resourceGroupResource) {
            $rgpath = $this.GetAzOpsResourceGroupPath()
            return (Join-Path (Join-Path $rgpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')".ToLower()) -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $childpath).ToLower())
        }
        elseif ($this.Scope -match $this.regex_subscriptionResource) {
            $subpath = $this.GetAzOpsSubscriptionPath()
            return (Join-Path (Join-Path $subpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')".ToLower()) -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $childpath).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')".ToLower()) -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $childpath).ToLower())
        }
        Write-AzOpsMessage -LogLevel Warning -LogString 'AzOpsScope.GetAzOpsResourcePath.NotFound' -LogStringValues $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-AzOpsMessage -LogLevel InternalComment -LogString 'Assert-AzOpsBicepDependency.Validating'

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

        if ($installed) {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Assert-AzOpsBicepDependency.Success'
        }
        else {
            $exception = [System.InvalidOperationException]::new('Unable to locate bicep installation')
            $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
            Write-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsBicepDependency.NotFound'
            $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-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsInitialization.StateError'
            $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-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsInitialization.NoCache'
            $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-AzOpsMessage -LogLevel InternalComment -LogString 'Assert-AzOpsJqDependency.Validating'
        $minVersion = New-Object System.Version("1.6")
        $result = (Invoke-AzOpsNativeCommand -ScriptBlock { jq --version } -IgnoreExitcode)
        $installed = $result -as [bool]

        if ($installed) {
            $version = New-Object System.Version(($result).Split("-")[1])
            if ($version -ge $minVersion) {
                Write-AzOpsMessage -LogLevel InternalComment -LogString '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-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsJqDependency.Failed'
                $Cmdlet.ThrowTerminatingError($errorRecord)
            }
        }

        $exception = [System.InvalidOperationException]::new('Unable to locate jq installation')
        $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
        Write-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsJqDependency.Failed'
        $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-AzOpsMessage -LogLevel InternalComment -LogString '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 (-not $hasGitConfig) {
            # Check global git config if setting not found in system settings
            $hasGitConfig = (Invoke-AzOpsNativeCommand -ScriptBlock { git config --global -l } -IgnoreExitcode | Select-String 'core.longpaths=true') -as [bool]
        }

        if ($hasGitConfig -and $hasRegKey) {
            return
        }
        if (-not $hasRegKey) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsWindowsLongPath.No.Registry'
        }
        if (-not $hasGitConfig) {
            Write-AzOpsMessage -LogLevel Warning -LogString '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://github.com/azure/azops/wiki/troubleshooting#windows.')
        $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null)
        Write-AzOpsMessage -LogLevel Warning -LogString 'Assert-AzOpsWindowsLongPath.Failed'
        $Cmdlet.ThrowTerminatingError($errorRecord)
    }

}

function ConvertFrom-AzOpsBicepTemplate {
    <#
        .SYNOPSIS
            Transpiles bicep template and associated bicepparam to Azure Resource Manager (ARM) template.
            The json file will be created in the same folder as the bicep file.
        .PARAMETER BicepTemplatePath
            BicepTemplatePath.
        .PARAMETER BicepParamTemplatePath
            BicepParamTemplatePath, when provided function does not attempt default parameter file discovery.
        .PARAMETER SkipParam
            Switch when set will avoid parameter file discovery.
        .PARAMETER ConvertedTemplate
            Array of strings, already converted base template, if file is on list skip conversion.
        .PARAMETER ConvertedParameter
            Array of strings, already converted parameter, if file is on list skip conversion.
        .EXAMPLE
            ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath "root/tenant root group (xxxx-xxxx-xxxx-xxxx-xxxx)/es (es)/subscription (xxxx-xxxx-xxxx-xxxx)/resource-rg/main.bicep"
            transpiledTemplatePath : root/tenant root group (xxxx-xxxx-xxxx-xxxx-xxxx)/es (es)/subscription (xxxx-xxxx-xxxx-xxxx)/resource-rg/main.json
            transpiledTemplateNew : True
            transpiledParametersPath : root/tenant root group (xxxx-xxxx-xxxx-xxxx-xxxx)/es (es)/subscription (xxxx-xxxx-xxxx-xxxx)/resource-rg/main.parameters.json
            transpiledParametersNew : True
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $BicepTemplatePath,
        [string]
        $BicepParamTemplatePath,
        [switch]
        $SkipParam,
        [string[]]
        $ConvertedTemplate,
        [string[]]
        $ConvertedParameter,
        [switch]
        $CompareDeploymentToDeletion
    )

    begin {
        # Assert bicep binaries
        Assert-AzOpsBicepDependency -Cmdlet $PSCmdlet
        # Default transpiled values to false
        $transpiledTemplateNew = $false
        $transpiledParametersNew = $false
    }
    process {
        if ($CompareDeploymentToDeletion) {
            # Avoid adding files destined for deletion to a deployment list
            if ($BicepTemplatePath -in $deleteSet -or $BicepTemplatePath -in ($deleteSet | Resolve-Path).Path) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.DeployDeletionOverlap' -LogStringValues $BicepTemplatePath
                continue
            }
        }
        $transpiledTemplatePath = [IO.Path]::GetFullPath("$($BicepTemplatePath -replace '\.bicep$', '.json')")
        if ($transpiledTemplatePath -notin $ConvertedTemplate) {
            # Convert bicep template
            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate' -LogStringValues $BicepTemplatePath, $transpiledTemplatePath
            Invoke-AzOpsNativeCommand -ScriptBlock { bicep build $bicepTemplatePath --outfile $transpiledTemplatePath }
            $transpiledTemplateNew = $true
            # Check if bicep build created (ARM) template
            if (-not (Test-Path $transpiledTemplatePath)) {
                # If bicep build did not produce file exit with error
                Write-AzOpsMessage -LogLevel Error -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepTemplate.Error' -LogStringValues $BicepTemplatePath
                throw
            }
        }
        if (-not $SkipParam) {
            if (-not $BicepParamTemplatePath) {
                # Check if bicep template has associated bicepparam file
                $bicepParametersPath = $BicepTemplatePath -replace '\.bicep$', '.bicepparam'
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.BicepParam' -LogStringValues $BicepTemplatePath, $bicepParametersPath
            }
            elseif ($BicepParamTemplatePath) {
                # BicepParamTemplatePath path provided as input
                $bicepParametersPath = $BicepParamTemplatePath
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.BicepParam' -LogStringValues $BicepTemplatePath, $bicepParametersPath
            }
            if ($CompareDeploymentToDeletion) {
                # Avoid adding files destined for deletion to a deployment list
                if ($bicepParametersPath -in $deleteSet -or $bicepParametersPath -in ($deleteSet | Resolve-Path).Path) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.DeployDeletionOverlap' -LogStringValues $bicepParametersPath
                    $skipParameters = $true
                }
            }
            if (-not $skipParameters -and $bicepParametersPath -and (Test-Path $bicepParametersPath)) {
                $transpiledParametersPath = [IO.Path]::GetFullPath("$($bicepParametersPath -replace '\.bicepparam$', '.parameters.json')")
                if ($transpiledParametersPath -notin $ConvertedParameter) {
                    # Convert bicepparam to ARM parameter file
                    Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepParam' -LogStringValues $bicepParametersPath, $transpiledParametersPath
                    Invoke-AzOpsNativeCommand -ScriptBlock { bicep build-params $bicepParametersPath --outfile $transpiledParametersPath }
                    $transpiledParametersNew = $true
                    # Check if bicep build-params created (ARM) parameters
                    if (-not (Test-Path $transpiledParametersPath)) {
                        # If bicep build-params did not produce file exit with error
                        Write-AzOpsMessage -LogLevel Error -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.ConvertBicepParam.Error' -LogStringValues $bicepParametersPath
                        throw
                    }
                }
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertFrom-AzOpsBicepTemplate.Resolve.BicepParam.NotFound' -LogStringValues $BicepTemplatePath
            }
        }
        # Return transpiled (ARM) template paths
        $return = [PSCustomObject]@{
            transpiledTemplatePath   = $transpiledTemplatePath
            transpiledTemplateNew    = $transpiledTemplateNew
            transpiledParametersPath = $transpiledParametersPath
            transpiledParametersNew  = $transpiledParametersNew
        }
        return $return
    }
}

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 Invoke-AzOpsPull 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 ChildResource
            The ChildResource contains details of the child resource
        .PARAMETER StatePath
            The root path to where the entire state is being built in.
        .EXAMPLE
            $policy = Get-AzPolicyDefinition -Custom | Select-Object -Last 1
            ConvertTo-AzOpsState -Resource $policy
            Export custom policy definition to the AzOps StatePath
        .EXAMPLE
            $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)]
        $Resource,

        [string]
        $ExportPath,

        [switch]
        $ReturnObject,

        [hashtable]
        $ChildResource,

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

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

    begin {
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'ConvertTo-AzOpsState.Starting'
    }

    process {
        if ($ChildResource) {
            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Processing' -LogStringValues $ChildResource.resourceName

            $objectFilePath = (New-AzOpsScope -scope $ChildResource.parentResourceId -ChildResource $ChildResource -StatePath $Statepath).statepath

            $jqJsonTemplate = Get-AzOpsTemplateFile -File "templateChildResource.jq"

            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Subscription.ChildResource.Jq.Template' -LogStringValues $jqJsonTemplate
            $object = ($Resource | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r '--sort-keys' | jq -r -f $jqJsonTemplate | ConvertFrom-Json)

            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Subscription.ChildResource.Exporting' -LogStringValues $objectFilePath
            ConvertTo-Json -InputObject $object -Depth 100 -EnumsAsStrings | Set-Content -Path $objectFilePath -Encoding UTF8 -Force
            return
        }
        else {
            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Processing' -LogStringValues $Resource.id
        }

        if (-not $ExportPath) {
            if ($Resource.Id) {
                # Handle subscription-only scenarios without managementGroup access
                if ($Resource -is [Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription]) {
                    $objectFilePath = (New-AzOpsScope -scope "/subscriptions/$($Resource.id)" -StatePath $StatePath).statepath
                }
                else {
                    $objectFilePath = (New-AzOpsScope -scope $Resource.id -StatePath $StatePath).statepath
                }
            }
            elseif ($Resource.ResourceId) {
                $objectFilePath = (New-AzOpsScope -scope $Resource.ResourceId -StatePath $StatePath).statepath
            }
            else {
                Write-AzOpsMessage -LogLevel Error -LogString 'ConvertTo-AzOpsState.NoExportPath' -LogStringValues $Resource.GetType()
            }
        }
        else {
            $objectFilePath = $ExportPath
        }
        # Create folder structure if it doesn't exist
        if (-not (Test-Path -Path $objectFilePath)) {
            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.File.Create' -LogStringValues $objectFilePath
            $null = New-Item -Path $objectFilePath -ItemType "file" -Force
        }
        else {
            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.File.UseExisting' -LogStringValues $objectFilePath
        }

        # If export file path ends with parameter
        $generateTemplateParameter = $objectFilePath.EndsWith('.parameters.json') ? $true : $false
        Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplateParameter' -LogStringValues "$generateTemplateParameter"

        $resourceType = $null
        switch ($Resource) {
            { $_.ResourceType } {
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.ResourceType' -LogStringValues "$($Resource.ResourceType)"
                $resourceType = $_.ResourceType
                break
            }
            # Management Groups
            { $_ -is [Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroup] -or
                $_ -is [Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroupChildInfo] } {
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -LogStringValues "$($_.GetType())"
                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-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -LogStringValues "$($_.GetType())"
                $resourceType = 'Microsoft.Subscription/subscriptions'
                if (-not $Resource.Type) {
                    $Resource | Add-Member -NotePropertyName Type -NotePropertyValue $resourceType
                }
                break
            }
            # Resource Groups
            { $_ -is [Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResourceGroup] } {
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -LogStringValues "$($_.GetType())"
                $resourceType = 'Microsoft.Resources/resourceGroups'
                break
            }
            # Resources - Controlled group for raw objects
            { $_ -is [Microsoft.Azure.Commands.Profile.Models.PSAzureTenant] } {
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -LogStringValues "$($_.GetType())"
                break
            }
            { $_.type } {
                if ( $_.type -eq 'Microsoft.Resources/subscriptions/resourceGroups') {
                    $resourceType = 'Microsoft.Resources/resourceGroups'
                }
                else {
                    $resourceType = $_.type
                }
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.ResourceType' -LogStringValues $resourceType
                break
            }
            Default {
                Write-AzOpsMessage -LogLevel Warning -LogString 'ConvertTo-AzOpsState.ObjectType.Resolved.Generic' -LogStringValues "$($_.GetType())"
                break
            }
        }
        if ($resourceType) {
            $providerNamespace = ($resourceType -split '/' | Select-Object -First 1)
            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ProviderNamespace' -LogStringValues $providerNamespace

            if (($resourceType -split '/').Count -eq 2) {
                $resourceTypeName = (($resourceType -split '/', 2) | Select-Object -Last 1)
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ResourceTypeName' -LogStringValues $resourceTypeName

                $resourceApiTypeName = (($resourceType -split '/', 2) | Select-Object -Last 1)
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ResourceApiTypeName' -LogStringValues $resourceApiTypeName
            }

            if (($resourceType -split '/').Count -eq 3) {
                $resourceTypeName = ((($resourceType -split '/', 3) | Select-Object -Last 2) -join '/')
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ResourceTypeName' -LogStringValues $resourceTypeName

                $resourceApiTypeName = (($resourceType -split '/', 3) | Select-Object -Index 1)
                Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ResourceApiTypeName' -LogStringValues $resourceApiTypeName
            }

            $jqRemoveTemplate = Get-AzOpsTemplateFile -File (Join-Path $providerNamespace -ChildPath "$resourceTypeName.jq") -Fallback "generic.jq"

            Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Jq.Remove' -LogStringValues $jqRemoveTemplate
            # 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-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplateParameter'

                    $jqJsonTemplate = Get-AzOpsTemplateFile -File (Join-Path $providerNamespace -ChildPath "$resourceTypeName.parameters.jq") -Fallback "template.parameters.jq"

                    Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Jq.Template' -LogStringValues $jqJsonTemplate
                    $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r '--sort-keys' | jq -r -f $jqJsonTemplate | ConvertFrom-Json)
                    #endregion
                }
                else {
                    #region Generating Template
                    Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate' -LogStringValues "$true"
                    $jqJsonTemplate = Get-AzOpsTemplateFile -File (Join-Path $providerNamespace -ChildPath "$resourceTypeName.template.jq") -Fallback "template.jq"
                    Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.Jq.Template' -LogStringValues $jqJsonTemplate
                    $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r '--sort-keys' | 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

                        # Handle GA/Preview API versions
                        $gaApiVersion = $apiVersions | Where-Object {$_ -match '^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$'} | Sort-Object -Descending
                        $preApiVersion = $apiVersions | Where-Object {$_ -notmatch '^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$'} | Sort-Object -Descending

                        if ($null -eq $gaApiVersion) {
                            $apiVersion = $preApiVersion | Select-Object -First 1
                        } else {
                            $apiVersion = $gaApiVersion | Select-Object -First 1
                        }
                        Write-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ApiVersion' -LogStringValues $resourceType, $apiVersion
                        $object.resources[0].apiVersion = $apiVersion
                        $object.resources[0].type = $resourceType
                    }
                    else {
                        Write-AzOpsMessage -LogLevel Warning -LogString 'ConvertTo-AzOpsState.GenerateTemplate.NoApiVersion' -LogStringValues $resourceType
                    }
                    #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-AzOpsMessage -LogLevel Debug -LogString 'ConvertTo-AzOpsState.GenerateTemplate.ChildResource' -LogStringValues $resourceName
                    }
                    #endregion

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

function Get-AzOpsCurrentPrincipal {
    <#
        .SYNOPSIS
            Gets the objectid/clientid from the current Azure context
        .DESCRIPTION
            Gets the objectid/clientid from the current Azure context
        .PARAMETER AzContext
            The AzContext used when pulling the information.
        .EXAMPLE
            > Get-AzOpsCurrentPrincipal -AzContext $AzContext
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        $AzContext = (Get-AzContext)
    )

    process {
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsCurrentPrincipal.AccountType' -LogStringValues $AzContext.Account.Type

        switch ($AzContext.Account.Type) {
            'User' {
                $restMethodResult = Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/me -ErrorAction Stop
                if ($restMethodResult) {
                    $principalObject = $restMethodResult.Content | ConvertFrom-Json -ErrorAction Stop
                }
            }
            'ManagedService' {
                # Get managed identity application id via IMDS (https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token)
                $restMethodResult = Invoke-RestMethod -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2021-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F" -Headers @{ Metadata = $true } -ErrorAction Stop
                if ($restMethodResult.client_id) {
                    $principalObject = Get-AzADServicePrincipal -ApplicationId $restMethodResult.client_id -ErrorAction Stop
                }
            }
            default {
                $principalObject = Get-AzADServicePrincipal -ApplicationId $AzContext.Account.Id -ErrorAction Stop
            }
        }
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsCurrentPrincipal.PrincipalId' -LogStringValues $principalObject.Id
        return $principalObject
    }
}

function Get-AzOpsManagementGroup {

    <#
        .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-AzOpsManagementGroup -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)]
        $ManagementGroup,

        [switch]
        $PartialDiscovery
    )

    process {
        try {
            $groupObject = Get-AzManagementGroup -GroupId $ManagementGroup -Expand -WarningAction SilentlyContinue
        }
        catch {
            Write-AzOpsMessage -LogLevel Error -LogString 'Get-AzOpsManagementGroup.Failed' -LogStringValues $ManagementGroup
            throw
        }
        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 "Microsoft.Management/managementGroups" | Foreach-Object -Process {
                    Get-AzOpsManagementGroup -ManagementGroup $_.Name -PartialDiscovery:$PartialDiscovery
                }
            }
        }
        return $groupObject
    }

}

function Get-AzOpsNestedSubscription {
    <#
        .SYNOPSIS
            Create a list of subscriptionId's nested at ManagementGroup Scope
        .PARAMETER Scope
            ManagementGroup Name
        .EXAMPLE
            > Get-AzOpsNestedSubscription -Scope 5663f39e-feb1-4303-a1f9-cf20b702de61
            Discover subscriptions at Management Group scope and below
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $Scope
    )

    process {
        $children = ($script:AzOpsAzManagementGroup | Where-Object {$_.Name -eq $Scope}).Children
        if ($children) {
            $subscriptionIds = @()
            foreach ($child in $children) {
                if (($child.Type -eq '/subscriptions') -and ($script:AzOpsSubscriptions.id -contains $child.Id)) {
                    $subscriptionIds += [PSCustomObject] @{
                        Name = $child.DisplayName
                        Id = $child.Name
                        Type = $child.Type
                    }
                }
                else {
                    $subscriptionIds += Get-AzOpsNestedSubscription -Scope $child.Name
                }
            }
            if ($subscriptionIds) {
                return $subscriptionIds
            }
        }
    }
}

function Get-AzOpsPim {
    <#
        .SYNOPSIS
            Get Privileged Identity Management objects from provided scope
        .PARAMETER ScopeObject
            ScopeObject
        .PARAMETER StatePath
            StatePath
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject,
        [Parameter(Mandatory = $true)]
        $StatePath
    )

    process {
        # Process RoleEligibilitySchedule
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'RoleEligibilitySchedule', $scopeObject.Scope
        $roleEligibilityScheduleRequest = Get-AzOpsRoleEligibilityScheduleRequest -ScopeObject $ScopeObject
        if ($roleEligibilityScheduleRequest) {
            $roleEligibilityScheduleRequest | ConvertTo-AzOpsState -StatePath $StatePath
        }
    }
}

function Get-AzOpsPolicy {

    <#
        .SYNOPSIS
            Get policy objects from provided scope
        .PARAMETER ScopeObject
            ScopeObject
        .PARAMETER StatePath
            StatePath
        .PARAMETER Subscription
            Complete Subscription list
        .PARAMETER SubscriptionsToIncludeResourceGroups
            Scoped Subscription list
        .PARAMETER ResourceGroup
            ResourceGroup switch indicating desired scope condition
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $ScopeObject,
        [Parameter(Mandatory = $true)]
        $StatePath,
        [Parameter(Mandatory = $false)]
        [object]
        $Subscription,
        [Parameter(Mandatory = $false)]
        [object]
        $SubscriptionsToIncludeResourceGroups,
        [Parameter(Mandatory = $false)]
        [switch]
        $ResourceGroup
    )

    process {
        if (-not $ResourceGroup) {
            # Process policy definitions
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'Policy Definitions', $scopeObject.Scope
            $policyDefinitions = Get-AzOpsPolicyDefinition -ScopeObject $ScopeObject -Subscription $Subscription
            $policyDefinitionsClean = @()
            foreach ($policyDefinition in $policyDefinitions) {
                $policyDefinitionClean = $policyDefinition | ConvertTo-Json -Depth 100
                $policyDefinitionsClean += $policyDefinitionClean -replace 'T00:00:00Z' | ConvertFrom-Json -Depth 100
            }
            $policyDefinitionsClean | ConvertTo-AzOpsState -StatePath $StatePath

            # Process policy set definitions (initiatives)
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'Policy Set Definitions', $ScopeObject.Scope
            $policySetDefinitions = Get-AzOpsPolicySetDefinition -ScopeObject $ScopeObject -Subscription $Subscription
            $policySetDefinitions | ConvertTo-AzOpsState -StatePath $StatePath
        }
        # Process policy assignments
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'Policy Assignments', $ScopeObject.Scope
        $policyAssignments = Get-AzOpsPolicyAssignment -ScopeObject $ScopeObject -Subscription $Subscription -SubscriptionsToIncludeResourceGroups $SubscriptionsToIncludeResourceGroups -ResourceGroup $ResourceGroup
        $policyAssignments | ConvertTo-AzOpsState -StatePath $StatePath
        # Process policy exemptions
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'Policy Exemptions', $ScopeObject.Scope
        $policyExemptions = Get-AzOpsPolicyExemption -ScopeObject $ScopeObject -Subscription $Subscription -SubscriptionsToIncludeResourceGroups $SubscriptionsToIncludeResourceGroups -ResourceGroup $ResourceGroup
        $policyExemptions | ConvertTo-AzOpsState -StatePath $StatePath
    }

}

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.
        .PARAMETER Subscription
            Complete Subscription list
        .PARAMETER SubscriptionsToIncludeResourceGroups
            Scoped Subscription list
        .PARAMETER ResourceGroup
            ResourceGroup switch indicating desired scope condition
        .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.PowerShell.Cmdlets.Policy.Models.IPolicyAssignment])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $ScopeObject,
        [Parameter(Mandatory = $false)]
        [object]
        $Subscription,
        [Parameter(Mandatory = $false)]
        [object]
        $SubscriptionsToIncludeResourceGroups,
        [Parameter(Mandatory = $false)]
        [bool]
        $ResourceGroup
    )

    process {
        if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions', 'managementGroups') {
            return
        }
        if ($ScopeObject.Type -eq 'managementGroups') {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyAssignment.ManagementGroup' -LogStringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
            if ((-not $SubscriptionsToIncludeResourceGroups) -or (-not $ResourceGroups)) {
                $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup == '' and subscriptionId == '' | order by ['id'] asc"
                Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop
            }
        }
        if ($Subscription) {
            if ($SubscriptionsToIncludeResourceGroups -and $ResourceGroup) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyAssignment.Subscription' -LogStringValues $SubscriptionsToIncludeResourceGroups.count -Target $ScopeObject
                $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup != '' | order by ['id'] asc"
                Search-AzOpsAzGraph -Subscription $SubscriptionsToIncludeResourceGroups -Query $query -ErrorAction Stop
            }
            elseif ($ResourceGroup) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyAssignment.ResourceGroup' -LogStringValues $Subscription.count -Target $ScopeObject
                $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup != '' | order by ['id'] asc"
                Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyAssignment.Subscription' -LogStringValues $Subscription.count -Target $ScopeObject
                $query = "policyresources | where type == 'microsoft.authorization/policyassignments' and resourceGroup == '' | order by ['id'] asc"
                Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop
            }
        }
    }

}

function Get-AzOpsPolicyDefinition {

    <#
        .SYNOPSIS
            Discover all custom policy definitions at the provided scope (Management Groups or subscriptions)
        .DESCRIPTION
            Discover all custom policy definitions at the provided scope (Management Groups or subscriptions)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve policy definitions for.
        .PARAMETER Subscription
            Complete Subscription list
        .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.PowerShell.Cmdlets.Policy.Models.IPolicyDefinition])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $ScopeObject,
        [Parameter(Mandatory = $false)]
        [object]
        $Subscription
    )

    process {
        if ($ScopeObject.Type -notin 'subscriptions', 'managementGroups') {
            return
        }
        if ($ScopeObject.Type -eq 'managementGroups') {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyDefinition.ManagementGroup' -LogStringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
            $query = "policyresources | where type == 'microsoft.authorization/policydefinitions' and properties.policyType == 'Custom' and subscriptionId == '' | order by ['id'] asc"
            Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop
        }
        if ($Subscription) {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyDefinition.Subscription' -LogStringValues $Subscription.count -Target $ScopeObject
            $query = "policyresources | where type == 'microsoft.authorization/policydefinitions' and properties.policyType == 'Custom' | order by ['id'] asc"
            Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop
        }
    }

}

function Get-AzOpsPolicyExemption {

    <#
        .SYNOPSIS
            Discover all custom policy exemptions at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discover all custom policy exemptions at the provided scope (Management Groups, subscriptions or resource groups)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve excemptions for.
        .PARAMETER Subscription
            Complete Subscription list
        .PARAMETER SubscriptionsToIncludeResourceGroups
            Scoped Subscription list
        .PARAMETER ResourceGroup
            ResourceGroup switch indicating desired scope condition
        .EXAMPLE
            > Get-AzOpsPolicyExemption -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all custom policy exemptions deployed at Management Group scope
    #>


    [OutputType([Microsoft.Azure.PowerShell.Cmdlets.Policy.Models.IPolicyExemption])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject,
        [Parameter(Mandatory = $false)]
        [object]
        $Subscription,
        [Parameter(Mandatory = $false)]
        [object]
        $SubscriptionsToIncludeResourceGroups,
        [Parameter(Mandatory = $false)]
        [bool]
        $ResourceGroup
    )

    process {
        if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions', 'managementGroups') {
            return
        }
        if ($ScopeObject.Type -eq 'managementGroups') {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyExemption.ManagementGroup' -LogStringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
            if ((-not $SubscriptionsToIncludeResourceGroups) -or (-not $ResourceGroups)) {
                $query = "policyresources | where type == 'microsoft.authorization/policyexemptions' and resourceGroup == '' and subscriptionId == '' | order by ['id'] asc"
                Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop
            }
        }
        if ($Subscription) {
            if ($SubscriptionsToIncludeResourceGroups -and $ResourceGroup) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyExemption.Subscription' -LogStringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
                $query = "policyresources | where type == 'microsoft.authorization/policyexemptions' and resourceGroup != '' | order by ['id'] asc"
                Search-AzOpsAzGraph -Subscription $SubscriptionsToIncludeResourceGroups -Query $query -ErrorAction Stop
            }
            elseif ($ResourceGroup) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyExemption.ResourceGroup' -LogStringValues $ScopeObject.ResourceGroup -Target $ScopeObject
                $query = "policyresources | where type == 'microsoft.authorization/policyexemptions' and resourceGroup != '' | order by ['id'] asc"
                Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicyExemption.Subscription' -LogStringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
                $query = "policyresources | where type == 'microsoft.authorization/policyexemptions' and resourceGroup == '' | order by ['id'] asc"
                Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop
            }
        }
    }

}

function Get-AzOpsPolicySetDefinition {

    <#
        .SYNOPSIS
            Discover all custom policyset definitions at the provided scope (Management Groups or subscriptions)
        .DESCRIPTION
            Discover all custom policyset definitions at the provided scope (Management Groups or subscriptions)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve policyset definitions for.
        .PARAMETER Subscription
            Complete Subscription list
        .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.PowerShell.Cmdlets.Policy.Models.IPolicySetDefinition])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $ScopeObject,
        [Parameter(Mandatory = $false)]
        [object]
        $Subscription
    )

    process {
        if ($ScopeObject.Type -notin 'subscriptions', 'managementGroups') {
            return
        }
        if ($ScopeObject.Type -eq 'managementGroups') {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicySetDefinition.ManagementGroup' -LogStringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
            $query = "policyresources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' and subscriptionId == '' | order by ['id'] asc"
            Search-AzOpsAzGraph -ManagementGroupName $ScopeObject.Name -Query $query -ErrorAction Stop
        }
        if ($Subscription) {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsPolicySetDefinition.Subscription' -LogStringValues $Subscription.count -Target $ScopeObject
            $query = "policyresources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' | order by ['id'] asc"
            Search-AzOpsAzGraph -Subscription $Subscription -Query $query -ErrorAction Stop
        }
    }

}

function Get-AzOpsResource {

    <#
        .SYNOPSIS
            Check if the Azure resource exists.
        .DESCRIPTION
            Check if the Azure resource exists.
        .PARAMETER ScopeObject
            The Resource to check.
        .EXAMPLE
            > Get-AzOpsResource -ScopeObject $ScopeObject
    #>


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

    process {
        Set-AzOpsContext -ScopeObject $ScopeObject
        try {
            switch ($ScopeObject.Resource) {
                # Check if the resource exist
                'locks' {
                    $resource = Get-AzResourceLock -Scope "/subscriptions/$($ScopeObject.Subscription)" -ErrorAction SilentlyContinue | Where-Object { $_.ResourceID -eq $ScopeObject.Scope }
                }
                'policyAssignments' {
                    $resource = Get-AzPolicyAssignment -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                }
                'policyDefinitions' {
                    $resource = Get-AzPolicyDefinition -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                }
                'policyExemptions' {
                    $resource = Get-AzPolicyExemption -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                }
                'policySetDefinitions' {
                    $resource = Get-AzPolicySetDefinition -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                }
                'roleAssignments' {
                    $resource = Invoke-AzRestMethod -Path "$($scopeObject.Scope)?api-version=2022-04-01" | Where-Object { $_.StatusCode -eq 200 }
                }
                'resourceGroups' {
                    $resource = Get-AzResourceGroup -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                }
                default {
                    $resource = Get-AzResource -ResourceId $ScopeObject.Scope -ErrorAction SilentlyContinue
                }
            }
        }
        catch {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsResource.Failed' -LogStringValues $_
            return
        }
        if ($resource) {
            return $resource
        }
    }
}

function Get-AzOpsResourceDefinition {

    <#
        .SYNOPSIS
            This cmdlet discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Privileged Identity Management resources, Policies, Role Assignments) from the provided input scope.
        .DESCRIPTION
            This cmdlet discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Privileged Identity Management resources, Policies, Role Assignments) from the provided input scope.
        .PARAMETER Scope
            Discovery Scope
        .PARAMETER IncludeResourcesInResourceGroup
            Discover only resources in these resource groups.
        .PARAMETER IncludeResourceType
            Discover only specific resource types.
        .PARAMETER SkipChildResource
            Skip childResource discovery.
        .PARAMETER SkipPim
            Skip discovery of Privileged Identity Management.
        .PARAMETER SkipLock
            Skip discovery of resourceLock.
        .PARAMETER SkipPolicy
            Skip discovery of policies.
        .PARAMETER SkipResource
            Skip discovery of resources inside resource groups.
        .PARAMETER SkipResourceGroup
            Skip discovery of resource groups.
        .PARAMETER SkipResourceType
            Skip discovery of specific resource types.
        .PARAMETER SkipRole
            Skip discovery of roles for better performance.
        .PARAMETER StatePath
            The root folder under which to write the resource json.
        .PARAMETER SubscriptionsToIncludeChildResource
            Filter which Subscription IDs should include child resources in pull.
        .PARAMETER SubscriptionsToIncludeResourceGroups
            Filter which Subscription IDs should include Resource Groups in pull.
        .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,

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

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

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

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

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

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

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

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

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

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

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

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

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

    begin {
        # Set variables for retry with exponential backoff
        $backoffMultiplier = 2
        $maxRetryCount = 3
        # Prepare Input Data for parallel processing
        $runspaceData = @{
            AzOpsPath                       = "$($script:ModuleRoot)\AzOps.psd1"
            StatePath                       = $StatePath
            runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup
            runspace_AzOpsSubscriptions     = $script:AzOpsSubscriptions
            runspace_AzOpsPartialRoot       = $script:AzOpsPartialRoot
            runspace_AzOpsResourceProvider  = $script:AzOpsResourceProvider
            BackoffMultiplier               = $backoffMultiplier
            MaxRetryCount                   = $maxRetryCount
        }
    }

    process {
        Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsResourceDefinition.Processing' -LogStringValues $Scope
        try {
            $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsResourceDefinition.Processing.NotFound' -LogStringValues $Scope
            return
        }
        if ($scopeObject.Type -notin 'subscriptions', 'managementGroups') {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Finished' -LogStringValues $scopeObject.Scope
            return
        }
        switch ($scopeObject.Type) {
            subscriptions {
                Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsResourceDefinition.Subscription.Processing' -LogStringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
                $subscriptions = Get-AzSubscription -SubscriptionId $scopeObject.Name | Where-Object { "/subscriptions/$($_.Id)" -in $script:AzOpsSubscriptions.id }
            }
            managementGroups {
                Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsResourceDefinition.ManagementGroup.Processing' -LogStringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject
                $query = "resourcecontainers | where type == 'microsoft.management/managementgroups' | order by ['id'] asc"
                $managementgroups = Search-AzOpsAzGraph -ManagementGroupName $scopeObject.Name -Query $query -ErrorAction Stop | Where-Object { $_.id -in $script:AzOpsAzManagementGroup.Id }
                $subscriptions = Get-AzOpsNestedSubscription -Scope $scopeObject.Name
                if ($managementgroups) {
                    # Process managementGroup scope in parallel
                    $managementgroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                        $managementgroup = $_
                        $runspaceData = $using:runspaceData

                        Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                        $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru

                        & $azOps {
                            $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup
                            $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions
                            $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot
                            $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider
                        }
                        # Process Privileged Identity Management resources and Roles at managementGroup scope
                        if ((-not $using:SkipPim) -or (-not $using:SkipRole)) {
                            & $azOps {
                                $ScopeObject = New-AzOpsScope -Scope $managementgroup.id -StatePath $runspaceData.Statepath -ErrorAction Stop
                                if (-not $using:SkipPim) {
                                    Get-AzOpsPim -ScopeObject $ScopeObject -StatePath $runspaceData.Statepath
                                }
                                if (-not $using:SkipRole) {
                                    Get-AzOpsRole -ScopeObject $ScopeObject -StatePath $runspaceData.Statepath
                                }
                            }
                        }
                    }
                    Clear-PSFMessage
                }
            }
        }
        #region Process Policies at $scopeObject
        if (-not $SkipPolicy) {
            Get-AzOpsPolicy -ScopeObject $scopeObject -Subscription $subscriptions -StatePath $StatePath
        }
        #endregion Process Policies at $scopeObject

        #region Process subscription scope in parallel
        if ($subscriptions) {
            $subscriptions | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                $subscription = $_
                $runspaceData = $using:runspaceData

                Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru

                & $azOps {
                    $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup
                    $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions
                    $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot
                    $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider
                }
                # Process Privileged Identity Management resources, Locks and Roles at subscription scope
                if ((-not $using:SkipPim) -or (-not $using:SkipLock) -or (-not $using:SkipRole)) {
                    & $azOps {
                        $scopeObject = New-AzOpsScope -Scope ($subscription.Type + '/' + $subscription.Id) -StatePath $runspaceData.Statepath -ErrorAction Stop
                        if (-not $using:SkipPim) {
                            Get-AzOpsPim -ScopeObject $scopeObject -StatePath $runspaceData.Statepath
                        }
                        if (-not $using:SkipLock) {
                            Get-AzOpsResourceLock -ScopeObject $scopeObject -StatePath $runspaceData.Statepath
                        }
                        if (-not $using:SkipRole) {
                            Get-AzOpsRole -ScopeObject $scopeObject -StatePath $runspaceData.Statepath
                        }
                    }
                }
            }
            Clear-PSFMessage
        }
        #endregion Process subscription scope in parallel

        #region Process Resource Groups
        if ($SkipResourceGroup -or (-not $subscriptions)) {
            if ($SkipResourceGroup) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.SkippingResourceGroup' -Target $ScopeObject
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.Subscription.NotFound' -Target $ScopeObject
            }
        }
        else {
            $query = "resourcecontainers | where type == 'microsoft.resources/subscriptions/resourcegroups' | where managedBy == '' | order by ['id'] asc"
            if ($SubscriptionsToIncludeResourceGroups -ne '*') {
                $newSubscriptionsToIncludeResourceGroups = $subscriptions | Where-Object { $_.Id -in $SubscriptionsToIncludeResourceGroups }
                if ($newSubscriptionsToIncludeResourceGroups) {
                    $resourceGroups = Search-AzOpsAzGraph -Subscription $newSubscriptionsToIncludeResourceGroups -Query $query -ErrorAction Stop
                }
                else {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.Subscription.NotFound' -Target $ScopeObject
                }
            }
            else {
                $resourceGroups = Search-AzOpsAzGraph -Subscription $subscriptions -Query $query -ErrorAction Stop
            }
            if ($resourceGroups) {
                # Process Resource Groups in parallel
                $resourceGroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                    $resourceGroup = $_
                    $runspaceData = $using:runspaceData

                    Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                    $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru

                    & $azOps {
                        $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup
                        $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions
                        $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot
                        $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider
                    }
                    # Create Resource Group in file system
                    & $azOps {
                        ConvertTo-AzOpsState -Resource $resourceGroup -StatePath $runspaceData.Statepath
                    }
                    # Process Privileged Identity Management resources, Locks and Roles at resource group scope
                    if ((-not $using:SkipPim) -or (-not $using:SkipRole) -or (-not $using:SkipLock)) {
                        & $azOps {
                            $rgScopeObject = New-AzOpsScope -Scope $resourceGroup.id -StatePath $runspaceData.Statepath -ErrorAction Stop
                            if (-not $using:SkipLock) {
                                Get-AzOpsResourceLock -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath
                            }
                            if (-not $using:SkipPim) {
                                Get-AzOpsPim -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath
                            }
                            if (-not $using:SkipRole) {
                                Get-AzOpsRole -ScopeObject $rgScopeObject -StatePath $runspaceData.Statepath
                            }
                        }
                    }
                }
                Clear-PSFMessage
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.NoResourceGroup' -LogStringValues $scopeObject.Name -Target $ScopeObject
            }
            # Process Policies at Resource Group scope
            if (-not $SkipPolicy) {
                if ($newSubscriptionsToIncludeResourceGroups) {
                    Get-AzOpsPolicy -ScopeObject $scopeObject -Subscription $subscriptions -SubscriptionsToIncludeResourceGroups $newSubscriptionsToIncludeResourceGroups -ResourceGroup -StatePath $StatePath
                }
                else {
                    Get-AzOpsPolicy -ScopeObject $scopeObject -Subscription $subscriptions -ResourceGroup -StatePath $StatePath
                }
            }
            # Process Resources at Resource Group scope
            if (-not $SkipResource) {
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Resource.Discovery' -LogStringValues $scopeObject.Name -Target $ScopeObject
                try {
                    $SkipResourceType | ForEach-Object { $skipResourceTypes += ($(if($skipResourceTypes){","}) + "'" + $_  + "'") }
                    $query = "resources | where type !in~ ($skipResourceTypes)"
                    if ($IncludeResourceType -ne "*") {
                        $IncludeResourceType | ForEach-Object { $includeResourceTypes += ($(if($includeResourceTypes){","}) + "'" + $_  + "'") }
                        $query = $query + " and type in~ ($includeResourceTypes)"
                    }
                    if ($IncludeResourcesInResourceGroup -ne "*") {
                        $IncludeResourcesInResourceGroup | ForEach-Object { $includeResourcesInResourceGroups += ($(if($includeResourcesInResourceGroups){","}) + "'" + $_  + "'") }
                        $query = $query + " and resourceGroup in~ ($includeResourcesInResourceGroups)"
                    }
                    $query = $query + " | order by ['id'] asc"
                    $resourcesBase = Search-AzOpsAzGraph -Subscription $subscriptions -Query $query -ErrorAction Stop
                }
                catch {
                    Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsResourceDefinition.Processing.Resource.Warning' -LogStringValues $scopeObject.Name -Target $ScopeObject
                }
                if ($resourcesBase) {
                    $resources = @()
                    foreach ($resource in $resourcesBase) {
                        if ($resourceGroups | Where-Object { $_.name -eq $resource.resourceGroup -and $_.subscriptionId -eq $resource.subscriptionId }) {
                            Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Resource' -LogStringValues $resource.name, $resource.resourcegroup -Target $resource
                            $resources += $resource
                            ConvertTo-AzOpsState -Resource $resource -StatePath $Statepath
                        }
                    }
                }
                else {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.Processing.Resource.Discovery.NotFound' -LogStringValues $scopeObject.Name -Target $ScopeObject
                }
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.SkippingResources' -Target $ScopeObject
            }
            # Process Child resources at resource scope in parallel
            if (-not $SkipResource -and -not $SkipChildResource) {
                if ($SubscriptionsToIncludeChildResource -ne '*') {
                    $resources = $resources | Where-Object { $_.subscriptionId -in $SubscriptionsToIncludeChildResource }
                }
                $resources | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                    $resource = $_
                    $runspaceData = $using:runspaceData

                    Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                    $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru

                    & $azOps {
                        $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup
                        $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions
                        $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot
                        $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider
                    }
                    $context = Get-AzContext
                    $context.Subscription.Id = $resource.subscriptionId
                    $tempExportPath = [System.IO.Path]::GetTempPath() + (New-Guid).ToString() + '.json'
                    try {
                        & $azOps {
                            $exportParameters = @{
                                Resource                = $resource.id
                                ResourceGroupName       = $resource.resourceGroup
                                SkipAllParameterization = $true
                                Path                    = $tempExportPath
                                DefaultProfile          = $context | Select-Object -First 1
                            }
                            Invoke-AzOpsScriptBlock -ArgumentList $exportParameters -ScriptBlock {
                                param (
                                    $ExportParameters
                                )
                                $param = $ExportParameters | Write-Output
                                Export-AzResourceGroup @param -Confirm:$false -Force -ErrorAction Stop | Out-Null
                            } -RetryCount $runspaceData.MaxRetryCount -RetryWait $runspaceData.BackoffMultiplier -RetryType Exponential
                        }
                        $exportResources = (Get-Content -Path $tempExportPath | ConvertFrom-Json).resources
                        $resourceGroup = $using:resourceGroups | Where-Object {$_.subscriptionId -eq $resource.subscriptionId -and $_.name -eq $resource.resourceGroup}
                        foreach ($exportResource in $exportResources) {
                            if (-not(($resource.name -eq $exportResource.name) -and ($resource.type -eq $exportResource.type))) {
                                & $azOps {
                                    Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.ChildResource' -LogStringValues $exportResource.name, $resource.resourceGroup -FunctionName "Get-AzOpsResourceDefinition" -ModuleName "AzOps" -Target $exportResource
                                }
                                $ChildResource = @{
                                    resourceProvider = $exportResource.type -replace '/', '_'
                                    resourceName     = $exportResource.name -replace '/', '_'
                                    parentResourceId = $resourceGroup.id
                                }
                                if (Get-Member -InputObject $exportResource -name 'dependsOn') {
                                    $exportResource.PsObject.Members.Remove('dependsOn')
                                }
                                $resourceHash = @{resources = @($exportResource) }
                                & $azOps {
                                    ConvertTo-AzOpsState -Resource $resourceHash -ChildResource $ChildResource -StatePath $runspaceData.Statepath
                                }
                            }
                        }
                    }
                    catch {
                        & $azOps {
                            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsResourceDefinition.ChildResource.Warning' -LogStringValues $resource.resourceGroup, $_ -FunctionName "Get-AzOpsResourceDefinition" -ModuleName "AzOps"
                        }
                    }
                    if (Test-Path -Path $tempExportPath) {
                        Remove-Item -Path $tempExportPath
                    }
                }
                Clear-PSFMessage
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.SkippingChildResources' -Target $ScopeObject
            }
        }
        #endregion Process Resource Groups
        Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsResourceDefinition.Finished' -LogStringValues $scopeObject.Scope -Target $ScopeObject
    }
}

function Get-AzOpsResourceLock {

    <#
        .SYNOPSIS
            Discover resource locks at the provided scope (Subscription or resource group)
        .DESCRIPTION
            Discover resource locks at the provided scope (Subscription or resource group)
        .PARAMETER ScopeObject
            The scope object representing the azure entity to retrieve resource locks from.
        .PARAMETER StatePath
            StatePath
        .EXAMPLE
            > Get-AzOpsResourceLock -ScopeObject xxx
            Discover all resource locks deployed at resource group scope
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject,
        [Parameter(Mandatory = $true)]
        $StatePath
    )

    process {
        if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions') {
            return
        }
        switch ($ScopeObject.Type) {
            subscriptions {
                # ScopeObject is a subscription
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceLock.Subscription' -LogStringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject
            }
            resourcegroups {
                # ScopeObject is a resourcegroup
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceLock.ResourceGroup' -LogStringValues $ScopeObject.ResourceGroup -Target $ScopeObject
            }
        }
        try {
            $parameters = @{
                Scope = $ScopeObject.Scope
            }
            # Gather resource locks at scopeObject with retry and backoff support from Invoke-AzOpsScriptBlock
            $resourceLocks = Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock {
                Get-AzResourceLock @parameters -AtScope -ErrorAction Stop | Where-Object {$($_.ResourceID.Substring(0, $_.ResourceId.LastIndexOf('/'))) -Like ("$($parameters.Scope)/providers/Microsoft.Authorization/locks")}
            } -RetryCount 3 -RetryWait 5 -RetryType Exponential -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsResourceLock.Failed' -LogStringValues $_
        }
        if ($resourceLocks) {
            # Process each resource lock
            foreach ($lock in $resourceLocks) {
                $lock | ConvertTo-AzOpsState -StatePath $StatePath
            }
        }
    }
}

function Get-AzOpsRole {
    <#
        .SYNOPSIS
            Get role objects from provided scope
        .PARAMETER ScopeObject
            ScopeObject
        .PARAMETER StatePath
            StatePath
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject,
        [Parameter(Mandatory = $true)]
        $StatePath
    )

    process {
        # Process role definitions
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'Role Definitions', $ScopeObject.Scope
        $roleDefinitions = Get-AzOpsRoleDefinition -ScopeObject $ScopeObject
        if ($roleDefinitions) {
            $roleDefinitions | ConvertTo-AzOpsState -StatePath $StatePath
        }

        # Process role assignments
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsResourceDefinition.Processing.Detail' -LogStringValues 'Role Assignments', $ScopeObject.Scope
        $roleAssignments = Get-AzOpsRoleAssignment -ScopeObject $ScopeObject
        if ($roleAssignments) {
            $roleAssignments | ConvertTo-AzOpsState -StatePath $StatePath
        }
    }
}

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
    #>


    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject
    )

    process {
        Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsRoleAssignment.Processing' -LogStringValues $ScopeObject -Target $ScopeObject
        $apiVersion = (($script:AzOpsResourceProvider | Where-Object {$_.ProviderNamespace -eq 'Microsoft.Authorization'}).ResourceTypes | Where-Object {$_.ResourceTypeName -eq 'roleAssignments'}).ApiVersions | Select-Object -First 1
        $path = "$($scopeObject.Scope)/providers/Microsoft.Authorization/roleAssignments?api-version=$apiVersion&`$filter=atScope()"
        try {
            $parameters = @{
                Path = $path
                Method = 'GET'
            }
            # Gather roleAssignment with retry and backoff support from Invoke-AzOpsScriptBlock
            $roleAssignments = Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock {
                Invoke-AzOpsRestMethod @parameters -ErrorAction Stop
            } -RetryCount 3 -RetryWait 5 -RetryType Exponential -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsRoleAssignment.Processing.Failed' -LogStringValues $_
        }
        if ($roleAssignments) {
            $roleAssignmentMatch = @()
            foreach ($roleAssignment in $roleAssignments) {
                if ($roleAssignment.properties.scope -eq $ScopeObject.Scope) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsRoleAssignment.Assignment' -LogStringValues $roleAssignment.id, $roleAssignment.properties.roleDefinitionId -Target $ScopeObject
                    $roleAssignmentMatch += [PSCustomObject]@{
                        id = $roleAssignment.id
                        name = $roleAssignment.name
                        properties = $roleAssignment.properties
                        type = $roleAssignment.type
                     }
                }
            }
            if ($roleAssignmentMatch) {
                return $roleAssignmentMatch
            }
        }
    }

}

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
    #>


    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject
    )

    process {
        Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsRoleDefinition.Processing' -LogStringValues $ScopeObject -Target $ScopeObject
        $apiVersion = (($script:AzOpsResourceProvider | Where-Object {$_.ProviderNamespace -eq 'Microsoft.Authorization'}).ResourceTypes | Where-Object {$_.ResourceTypeName -eq 'roleDefinitions'}).ApiVersions | Select-Object -First 1
        $path = "$($scopeObject.Scope)/providers/Microsoft.Authorization/roleDefinitions?api-version=$apiVersion&`$filter=type+eq+'CustomRole'"
        try {
            $parameters = @{
                Path = $path
                Method = 'GET'
            }
            # Gather roleDefinitions with retry and backoff support from Invoke-AzOpsScriptBlock
            $roleDefinitions = Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock {
                Invoke-AzOpsRestMethod @parameters -ErrorAction Stop
            } -RetryCount 3 -RetryWait 5 -RetryType Exponential -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsRoleDefinition.Processing.Failed' -LogStringValues $_
        }
        if ($roleDefinitions) {
            $roleDefinitionsMatch = @()
            foreach ($roleDefinition in $roleDefinitions) {
                if ($roleDefinition.properties.assignableScopes -eq $ScopeObject.Scope) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsRoleDefinition.Definition' -LogStringValues $roleDefinition.id -Target $ScopeObject
                    $roleDefinitionsMatch += [PSCustomObject]@{
                        # Removing the Trailing slash to ensure that '/' is not appended twice when adding '/providers/xxx'.
                        # Example: '/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/' is a valid assignment scope.
                        id = '/' + $roleDefinition.properties.assignableScopes[0].Trim('/') + '/providers/Microsoft.Authorization/roleDefinitions/' + $roleDefinition.id
                        name = $roleDefinition.Name
                        properties = $roleDefinition.properties
                        type = $roleDefinition.type
                     }
                }
            }
            if ($roleDefinitionsMatch) {
                return $roleDefinitionsMatch
            }
        }
    }

}

function Get-AzOpsRoleEligibilityScheduleRequest {

    <#
        .SYNOPSIS
            Discover all Privileged Identity Management RoleEligibilityScheduleRequest at the provided scope (Management Groups, subscriptions or resource groups)
        .DESCRIPTION
            Discover all Privileged Identity Management RoleEligibilityScheduleRequest 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-AzOpsRoleEligibilityScheduleRequest -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath)
            Discover all Privileged Identity Management RoleEligibilityScheduleRequest at Management Group scope
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $ScopeObject
    )

    process {
        if ($ScopeObject.Type -notin 'resourceGroups', 'subscriptions', 'managementGroups') {
            return
        }

        # Process RoleEligibilitySchedule which is used to construct AzOpsRoleEligibilityScheduleRequest
        Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsRoleEligibilityScheduleRequest.Processing' -LogStringValues $ScopeObject.Scope -Target $ScopeObject
        try {
            $parameters = @{
                Scope = $ScopeObject.Scope
            }
            $roleEligibilitySchedules = Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock {
                Get-AzRoleEligibilitySchedule @parameters -WarningAction SilentlyContinue -ErrorAction Stop | Where-Object { $_.Scope -eq $parameters.Scope }
            } -RetryCount 3 -RetryWait 5 -RetryType Exponential -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsRoleEligibilityScheduleRequest.Processing.Failed' -LogStringValues $_
            return
        }
        if ($roleEligibilitySchedules) {
            foreach ($roleEligibilitySchedule in $roleEligibilitySchedules) {
                # Process roleEligibilitySchedule together with RoleEligibilityScheduleRequest
                $parameters = @{
                    Scope = $ScopeObject.Scope
                    Name = $roleEligibilitySchedule.Name
                }
                $roleEligibilityScheduleRequest = $null
                $roleEligibilityScheduleRequest = Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock {
                    Get-AzRoleEligibilityScheduleRequest @parameters -ErrorAction SilentlyContinue
                } -RetryCount 3 -RetryWait 5 -RetryType Exponential -ErrorAction SilentlyContinue
                if ($roleEligibilityScheduleRequest) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsRoleEligibilityScheduleRequest.Assignment' -LogStringValues $roleEligibilitySchedule.Name -Target $ScopeObject
                    # Construct AzOpsRoleEligibilityScheduleRequest by combining information from roleEligibilitySchedule and roleEligibilityScheduleRequest
                    [AzOpsRoleEligibilityScheduleRequest]::new($roleEligibilitySchedule, $roleEligibilityScheduleRequest)
                }
                else {
                    Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsRoleEligibilityScheduleRequest.Processing.NotFound' -LogStringValues $ScopeObject.Scope, $roleEligibilitySchedule.Name -Target $ScopeObject
                    # Construct AzOpsRoleEligibilityScheduleRequest from roleEligibilitySchedule since no AzRoleEligibilityScheduleRequest was found
                    [AzOpsRoleEligibilityScheduleRequest]::new($roleEligibilitySchedule)
                }
            }
        }
    }
}

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 = '2022-12-01'
    )

    process {
        Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsSubscription.Excluded.States' -LogStringValues ($ExcludedStates -join ',')
        Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsSubscription.Excluded.Offers' -LogStringValues ($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-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsSubscription.NoSubscriptions'
            return
        }

        Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsSubscription.Subscriptions.Found' -LogStringValues $allSubscriptionsResults.Count
        if ($allSubscriptionsResults.Count -gt $includedSubscriptions.Count) {
            Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsSubscription.Subscriptions.Excluded' -LogStringValues ($allSubscriptionsResults.Count - $includedSubscriptions.Count)
        }

        if ($includedSubscriptions | Where-Object State -EQ PastDue) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsSubscription.Subscriptions.PastDue' -LogStringValues ($includedSubscriptions | Where-Object State -EQ PastDue).Count
        }
        Write-AzOpsMessage -LogLevel Important -LogString 'Get-AzOpsSubscription.Subscriptions.Included' -LogStringValues $includedSubscriptions.Count -Metric $includedSubscriptions.Count -MetricName 'Subscription Count'
        $includedSubscriptions
    }

}

function Get-AzOpsTemplateFile {

    <#
        .SYNOPSIS
            Takes file name input and returns first match, looks at Core.SkipCustomJqTemplate, Core.CustomJqTemplatePath followed by Core.JqTemplatePath.
        .DESCRIPTION
            Takes file name input and returns first match, looks at Core.SkipCustomJqTemplate, Core.CustomJqTemplatePath followed by Core.JqTemplatePath.
        .PARAMETER File
            Filename of template file to look for.
        .PARAMETER Fallback
            Fallback filename to look for if parameter:file is not found.
        .EXAMPLE
            > Get-AzOpsTemplateFile -File "templateChildResource.jq"
            Returns the following:
            /workspaces/AzOps/src/data/template/templateChildResource.jq
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $File,

        [string]
        $Fallback,

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

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

        [bool]
        $SkipCustomJqTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipCustomJqTemplate')
    )

    process {
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing' -LogStringValues $File
        # Evaluate JqTemplate Conditions
        if ($SkipCustomJqTemplate) {
            # Use default module templates only
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing.Path' -LogStringValues $File, $JqTemplatePath
            if ($Fallback) {
                # Process with Fallback
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing.Fallback' -LogStringValues $File, $Fallback
                $return = (Test-Path -Path (Join-Path $JqTemplatePath -ChildPath $File) -PathType Leaf) ?
                (Get-Item -Path (Join-Path $JqTemplatePath -ChildPath $File) -ErrorAction SilentlyContinue):
                (Get-Item -Path (Join-Path $JqTemplatePath -ChildPath $Fallback) -ErrorAction SilentlyContinue)
            }
            else {
                # Process without Fallback
                if (Test-Path -Path (Join-Path $JqTemplatePath -ChildPath $File) -PathType Leaf) {
                    $return = (Get-Item -Path (Join-Path $JqTemplatePath -ChildPath $File) -ErrorAction SilentlyContinue)
                }
            }
        }
        else {
            # Use custom templates
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing.Path' -LogStringValues $File, $CustomJqTemplatePath
            if ($Fallback) {
                # Process with Fallback
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing.Fallback' -LogStringValues $File, $Fallback
                $return = (Test-Path -Path (Join-Path $CustomJqTemplatePath -ChildPath $File) -PathType Leaf) ?
                (Get-Item -Path (Join-Path $CustomJqTemplatePath -ChildPath $File) -ErrorAction SilentlyContinue):
                (Get-Item -Path (Join-Path $CustomJqTemplatePath -ChildPath $Fallback) -ErrorAction SilentlyContinue)
                if (-not $return) {
                    # Use default templates since no custom templates was found
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing.Path' -LogStringValues $File, $JqTemplatePath
                    $return = (Test-Path -Path (Join-Path $JqTemplatePath -ChildPath $File) -PathType Leaf) ?
                    (Get-Item -Path (Join-Path $JqTemplatePath -ChildPath $File) -ErrorAction SilentlyContinue):
                    (Get-Item -Path (Join-Path $JqTemplatePath -ChildPath $Fallback) -ErrorAction SilentlyContinue)
                }
            }
            else {
                # Process without Fallback
                if (Test-Path -Path (Join-Path $CustomJqTemplatePath -ChildPath $File) -PathType Leaf) {
                    $return = (Get-Item -Path (Join-Path $CustomJqTemplatePath -ChildPath $File) -ErrorAction SilentlyContinue)
                }
                if (-not $return) {
                    # Use default templates since no custom templates was found
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'Get-AzOpsTemplateFile.Processing.Path' -LogStringValues $File, $JqTemplatePath
                    if (Test-Path -Path (Join-Path $JqTemplatePath -ChildPath $File) -PathType Leaf) {
                        $return = (Get-Item -Path (Join-Path $JqTemplatePath -ChildPath $File) -ErrorAction SilentlyContinue)
                    }
                }
            }
        }
        if ($return) {
            # Template file found
            $return = ($return | Select-Object -First 1).VersionInfo.FileName
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsTemplateFile.Processing.Found' -LogStringValues $return
            return $return
        }
        else {
            # No template file found, throw
            Write-AzOpsMessage -LogLevel Error -LogString 'Get-AzOpsTemplateFile.Processing.NotFound' -LogStringValues $File
            throw
        }
    }
}

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-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsNativeCommand' -LogStringValues $ScriptBlock, $_
            }
            $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-AzOpsRestMethod {
    <#
        .SYNOPSIS
            Process Path with given Method and manage paging of results and returns value's
        .PARAMETER Path
            Path
        .PARAMETER Method
            Method
        .EXAMPLE
            > Invoke-AzOpsRestMethod -Path "/subscriptions/{subscription}/resourcegroups/{resourcegroup}/providers/microsoft.operationalinsights/workspaces/{workspace}?api-version={API}" -Method GET
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Object]
        $Path,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $Method
    )

    process {
        # Process Path with given Method
        Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsRestMethod.Processing' -LogStringValues $Path
        $allresults = do {
            try {
                $results = ((Invoke-AzRestMethod -Path $Path -Method $Method -ErrorAction Stop).Content | ConvertFrom-Json -Depth 100)
                $results.value
                $path = $results.nextLink -replace 'https://management\.azure\.com'
                if ($results.StatusCode -eq '429' -or $results.StatusCode -like '5*') {
                    $results.Headers.GetEnumerator() | ForEach-Object {
                        if ($_.key -eq 'Retry-After') {
                            Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsRestMethod.Processing.RateLimit' -LogStringValues $Path, $_.value
                            Start-Sleep -Seconds $_.value
                        }
                    }
                }
            }
            catch {
                Write-AzOpsMessage -LogLevel Error -LogString 'Invoke-AzOpsRestMethod.Processing.Error' -LogStringValues $_, $Path
            }
        }
        while ($path)
        if ($allresults) {
            return $allresults
        }
    }
}

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-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsScriptBlock.Failed.WillRetry' -LogStringValues $ScriptBlock, $count, $RetryCount -ErrorRecord $_ -Data $data
                    switch ($RetryType) {
                        Linear { Start-Sleep -Seconds $RetryWait }
                        Exponential { Start-Sleep -Seconds ([math]::Pow($RetryWait, $count)) }
                    }
                    continue
                }
                Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsScriptBlock.Failed.GivingUp' -LogStringValues $ScriptBlock, $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 TemplateObject
            TemplateObject where the templates content is stored in-memory.
        .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 WhatifExcludedChangeTypes
            Exclude specific change types from WhatIf operations.
        .PARAMETER WhatIfResultFormat
            Accepts ResourceIdOnly or FullResourcePayloads.
        .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
            Name Value
            ---- -----
            filePath /root/managementgroup/subscription/resourcegroup/template.json
            parameterFilePath /root/managementgroup/subscription/resourcegroup/template.parameters.json
            results Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments.PSWhatIfOperationResult
    #>


    [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)]
        [hashtable]
        $TemplateObject,

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

        [string]
        $Mode = "Incremental",

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

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

        [string]
        [ValidateSet("ResourceIdOnly","FullResourcePayloads")]
        $WhatIfResultFormat

    )

    process {
        Write-AzOpsMessage -LogLevel Important -LogString 'New-AzOpsDeployment.Processing' -LogStringValues $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
            }
            $scopeFound = $true
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.Scope.Failed' -LogStringValues $TemplateFilePath, $TemplateParameterFilePath -ErrorRecord $_
            return
        }
        if (-not $scopeObject) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.Scope.Empty' -LogStringValues $TemplateFilePath, $TemplateParameterFilePath
            return
        }
        #endregion Resolve Scope

        #region Parse Content
        if ($null -eq $TemplateObject) {
            $TemplateFileContent = [System.IO.File]::ReadAllText($TemplateFilePath)
            $TemplateObject = ConvertFrom-Json $TemplateFileContent -AsHashtable
        }
        if ($TemplateObject.metadata._generator.name -eq 'bicep') {
            # Detect bicep templates
            $bicepTemplate = $true
        }
        #endregion

        #region Process Scope
        # Configure variables/parameters and the WhatIf/Deployment cmdlets to be used per scope
        $defaultDeploymentRegion = (Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion')
        $parameters = @{
            'TemplateObject'              = $TemplateObject
            'SkipTemplateParameterPrompt' = $true
            'Location'                    = $defaultDeploymentRegion
        }
        if ($WhatIfResultFormat) {
            $parameters.ResultFormat = $WhatIfResultFormat
        }
        # Resource Groups excluding Microsoft.Resources/resourceGroups that needs to be submitted at subscription scope
        if ($scopeObject.resourcegroup -and $TemplateObject.resources[0].type -ne 'Microsoft.Resources/resourceGroups') {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.ResourceGroup.Processing' -LogStringValues $scopeObject -Target $scopeObject
            Set-AzOpsContext -ScopeObject $scopeObject
            $whatIfCommand = 'Get-AzResourceGroupDeploymentWhatIfResult'
            $deploymentCommand = 'New-AzResourceGroupDeployment'
            $parameters.ResourceGroupName = $scopeObject.resourcegroup
            $parameters.Remove('Location')
        }
        # Subscriptions
        elseif ($scopeObject.subscription) {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Subscription.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject
            Set-AzOpsContext -ScopeObject $scopeObject
            $whatIfCommand = 'Get-AzSubscriptionDeploymentWhatIfResult'
            $deploymentCommand = 'New-AzDeployment'
        }
        # Management Groups
        elseif ($scopeObject.managementGroup -and (-not ($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_'))) {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.ManagementGroup.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject
            $parameters.ManagementGroupId = $scopeObject.managementgroup
            $whatIfCommand = 'Get-AzManagementGroupDeploymentWhatIfResult'
            $deploymentCommand = 'New-AzManagementGroupDeployment'
        }
        # Tenant deployments
        elseif ($scopeObject.type -eq 'root' -and $scopeObject.scope -eq '/') {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Root.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject
            $whatIfCommand = 'Get-AzTenantDeploymentWhatIfResult'
            $deploymentCommand = 'New-AzTenantDeployment'
        }
        # If Management Group resource was not found, validate and prepare for first time deployment of resource
        elseif ($scopeObject.managementGroup -and (($scopeObject.StatePath).StartsWith('azopsscope-assume-new-resource_'))) {
            $resourceScopeFileContent = Get-Content -Path $addition | ConvertFrom-Json -Depth 100
            $resource = ($resourceScopeFileContent.resources | Where-Object {$_.type -eq 'Microsoft.Management/managementGroups'} | Select-Object -First 1)
            $pathDir = (Get-Item -Path $addition).Directory | Resolve-Path -Relative
            if ((Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -ne '.') {
                $pathDir = Split-Path -Path $pathDir -Parent
            }
            $parentDirScopeObject = New-AzOpsScope -Path (Split-Path -Path $pathDir -Parent) -WhatIf:$false | Where-Object {(-not ($_.StatePath).StartsWith('azopsscope-assume-new-resource_'))}
            $parentIdScope = New-AzOpsScope -Scope (($resource).properties.details.parent.id) -WhatIf:$false | Where-Object {(-not ($_.StatePath).StartsWith('azopsscope-assume-new-resource_'))}
            # Validate parent existence with content parent scope, statepath and name match, determines file location match deployment scope
            if ($parentDirScopeObject -and $parentIdScope -and $parentDirScopeObject.Scope -eq $parentIdScope.Scope -and $parentDirScopeObject.StatePath -eq $parentIdScope.StatePath -and $parentDirScopeObject.Name -eq $parentIdScope.Name) {
                # Validate directory name match resource information
                if ((Get-Item -Path $pathDir).Name -eq "$($resource.properties.displayName) ($($resource.name))") {
                    Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.Root.Processing' -LogStringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject
                    $whatIfCommand = 'Get-AzTenantDeploymentWhatIfResult'
                    $deploymentCommand = 'New-AzTenantDeployment'
                }
                # Invalid directory name
                else {
                    Write-AzOpsMessage -LogLevel Error -LogString 'New-AzOpsDeployment.Directory.NotFound' -LogStringValues (Get-Item -Path $pathDir).Name, "$($resource.properties.displayName) ($($resource.name))"
                    throw
                }
            }
            # Parent missing
            else {
                Write-AzOpsMessage -LogLevel Error -LogString 'New-AzOpsDeployment.Parent.NotFound' -LogStringValues $addition
                throw
            }
        }
        else {
            Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.Scope.Unidentified' -LogStringValues $scopeObject
            $scopeFound = $false
        }
        # Proceed with WhatIf or Deployment if scope was found
        if ($scopeFound) {
            $deploymentResult = [PSCustomObject]@{
                filePath    = ''
                parameterFilePath = ''
                results = ''
                deployment = ''
            }
            if ($TemplateParameterFilePath) {
                $parameters.TemplateParameterFile = $TemplateParameterFilePath
            }
            if ($WhatifExcludedChangeTypes) {
                $parameters.ExcludeChangeType = $WhatifExcludedChangeTypes
            }
            # Get predictive deployment results from WhatIf API
            $results = & $whatIfCommand @parameters -ErrorAction Continue -ErrorVariable resultsError
            if ($resultsError) {
                $resultsErrorMessage = $resultsError.exception.InnerException.Message
                # Ignore errors for bicep modules
                if ($resultsErrorMessage -match 'https://aka.ms/resource-manager-parameter-files' -and $true -eq $bicepTemplate) {
                    Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.TemplateParameterError' -Target $scopeObject
                    $invalidTemplate = $true
                }
                # Handle WhatIf prediction errors
                elseif ($resultsErrorMessage -match 'DeploymentWhatIfResourceError' -and $resultsErrorMessage -match "The request to predict template deployment") {
                    Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.WhatIfWarning' -LogStringValues $resultsErrorMessage -Target $scopeObject
                    if ($parameters.TemplateParameterFile) {
                        $deploymentResult.filePath = $parameters.TemplateFile
                        $deploymentResult.parameterFilePath = $parameters.TemplateParameterFile
                        $deploymentResult.results = ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage)
                    }
                    else {
                        $deploymentResult.filePath = $parameters.TemplateFile
                        $deploymentResult.results = ('{0}WhatIf prediction failed with error - validate changes manually before merging:{0}{1}' -f [environment]::NewLine, $resultsErrorMessage)
                    }
                }
                else {
                    Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.WhatIfWarning' -LogStringValues $resultsErrorMessage -Target $scopeObject
                    throw $resultsErrorMessage
                }
            }
            elseif ($results.Error) {
                Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsDeployment.TemplateError' -LogStringValues $TemplateFilePath -Target $scopeObject
                return
            }
            else {
                Write-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsDeployment.WhatIfResults' -LogStringValues ($results | Out-String) -Target $scopeObject
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'New-AzOpsDeployment.WhatIfFile' -Target $scopeObject
                if ($parameters.TemplateParameterFile) {
                    $deploymentResult.filePath = $TemplateFilePath
                    $deploymentResult.parameterFilePath = $parameters.TemplateParameterFile
                    $deploymentResult.results = $results
                }
                else {
                    $deploymentResult.filePath = $TemplateFilePath
                    $deploymentResult.results = $results
                }
            }
            # Remove ExcludeChangeType parameter as it doesn't exist for deployment cmdlets
            if ($parameters.ExcludeChangeType) {
                $parameters.Remove('ExcludeChangeType')
            }
            $parameters.Name = $DeploymentName
            if ($PSCmdlet.ShouldProcess("Start $($scopeObject.type) Deployment with $deploymentCommand?")) {
                if (-not $invalidTemplate) {
                    $deploymentResult.deployment = & $deploymentCommand @parameters
                }
            }
            else {
                # Exit deployment
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'New-AzOpsDeployment.SkipDueToWhatIf'
            }
        }
        #Return
        if ($deploymentResult) {
            return $deploymentResult
        }
    }
}

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 ChildResource
            The ChildResource contains details of the child resource.
        .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]
    #>


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

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

        [hashtable]
        $ChildResource,

        [string]
        $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State')
    )
    process {
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'New-AzOpsScope.Starting'

        switch ($PSCmdlet.ParameterSetName) {
            scope {
                if (($ChildResource) -and (-not(Get-PSFConfigValue -FullName AzOps.Core.SkipChildResource))) {
                    Invoke-PSFProtectedCommand -ActionString 'New-AzOpsScope.Creating.FromParentScope' -ActionStringValues $Scope -Target $Scope -ScriptBlock {
                        [AzOpsScope]::new($Scope, $ChildResource, $StatePath)
                    } -EnableException $true -PSCmdlet $PSCmdlet
                }
                else {
                    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' -ActionStringValues $Path -Target $Path -ScriptBlock {
                    [AzOpsScope]::new($(Get-Item -Path $Path -Force), $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 Invoke-AzOpsPull 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-AzOpsMessage -LogLevel Important -LogString 'New-AzOpsStateDeployment.Processing' -LogStringValues $FileName
        $scopeObject = New-AzOpsScope -Path (Get-Item -Path $FileName).FullName -StatePath $StatePath

        if (-not $scopeObject.Type) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'New-AzOpsStateDeployment.InvalidScope' -LogStringValues $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-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsStateDeployment.Subscription' -LogStringValues $FileName -Target $scopeObject
            $subscription = $subscriptions | Where-Object Name -EQ $scopeObject.subscriptionDisplayName

            #region Subscription needs to be created
            if (-not $subscription) {
                Write-AzOpsMessage -LogLevel Important -LogString 'New-AzOpsStateDeployment.Subscription.New' -LogStringValues $FileName -Target $scopeObject

                if (-not $enrollmentAccounts) {
                    Write-AzOpsMessage -LogLevel Error -LogString 'New-AzOpsStateDeployment.NoEnrollmentAccount' -Target $scopeObject
                    Write-AzOpsMessage -LogLevel Error -LogString 'New-AzOpsStateDeployment.NoEnrollmentAccount.Solution' -Target $scopeObject
                    return
                }

                if ($cfgEnrollmentAccount = Get-PSFConfigValue -FullName 'AzOps.Core.EnrollmentAccountPrincipalName') {
                    Write-AzOpsMessage -LogLevel Important -LogString 'New-AzOpsStateDeployment.EnrollmentAccount.Selected' -LogStringValues $cfgEnrollmentAccount
                    $enrollmentAccountObjectId = ($enrollmentAccounts | Where-Object PrincipalName -eq $cfgEnrollmentAccount).ObjectId
                }
                else {
                    Write-AzOpsMessage -LogLevel Important -LogString 'New-AzOpsStateDeployment.EnrollmentAccount.First' -LogStringValues @($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-AzOpsMessage -LogLevel Verbose -LogString 'New-AzOpsStateDeployment.Subscription.Exists' -LogStringValues $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-AzOpsMessage -LogLevel Verbose -LogString 'Register-AzOpsProviderFeature.Processing' -LogStringValues $ScopeObject, $FileName -Target $scopeObject
        $currentContext = Get-AzContext
        if ($ScopeObject.Subscription -and $currentContext.Subscription.Id -ne $ScopeObject.Subscription) {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Register-AzOpsProviderFeature.Context.Switching' -LogStringValues $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-AzOpsMessage -LogLevel Verbose -LogString 'Register-AzOpsProviderFeature.Provider.Feature' -LogStringValues $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-AzOpsMessage -LogLevel Verbose -LogString 'Register-AzOpsResourceProvider.Processing' -LogStringValues $ScopeObject, $FileName -Target $ScopeObject
        $currentContext = Get-AzContext
        if ($ScopeObject.Subscription -and $currentContext.Subscription.Id -ne $ScopeObject.Subscription) {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Register-AzOpsResourceProvider.Context.Switching' -LogStringValues $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-AzOpsMessage -LogLevel Important -LogString 'Register-AzOpsResourceProvider.Provider.Register' -LogStringValues $resourceprovider.ProviderNamespace
            Register-AzResourceProvider -Confirm:$false -Pre -ProviderNamespace $resourceprovider.ProviderNamespace
        }
    }

}

function Remove-AzOpsDeployment {

    <#
        .SYNOPSIS
            Deletion of supported resource types AzOps.Core.DeletionSupportedResourceType and custom templates.
        .DESCRIPTION
            Deletion of supported resource types AzOps.Core.DeletionSupportedResourceType and custom templates.
        .PARAMETER CustomTemplateResourceDeletion
            Enable or disable, deletion of resources in custom templates.
        .PARAMETER DeploymentName
            Dummy name used to run Azure WhatIf deployment.
        .PARAMETER TemplateFilePath
            Path where the ARM templates can be found.
        .PARAMETER TemplateParameterFilePath
            Path where the ARM parameters templates can be found.
        .PARAMETER StatePath
            The root folder under which to find the resource json.
        .PARAMETER DeletionSupportedResourceType
            Supported resource types for deletion of AzOps generated file.
        .PARAMETER DeleteSet
            String of file names to validate deletion.
        .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
            > $AzOpsRemovalList | Select-Object $uniqueProperties -Unique | Remove-AzOpsDeployment
            Remove all unique deployments provided from $AzOpsRemovalList
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [bool]
        $CustomTemplateResourceDeletion = (Get-PSFConfigValue -FullName 'AzOps.Core.CustomTemplateResourceDeletion'),

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

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

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

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

        [object[]]
        $DeletionSupportedResourceType = (Get-PSFConfigValue -FullName 'AzOps.Core.DeletionSupportedResourceType'),

        [string[]]
        $DeleteSet
    )

    process {
        function Get-AzLocksDeletionDependency {
            param (
                $resourceToDelete
            )
            $dependency = @()
            if ($resourceToDelete.Type -in $DeletionSupportedResourceType) {
                $subPattern = '^/subscriptions/([0-9a-fA-F-]{36})'
                $rgPattern = '^/subscriptions/([0-9a-fA-F-]{36})/resourceGroups/([^/]+)'
                if ($resourceToDelete.Id -match $subPattern) {
                    $deletionScope = $matches[0]
                    $depLock = Get-AzResourceLock -Scope $deletionScope
                    if ($depLock) {
                        foreach ($lock in $depLock) {
                            #Filter through each return and validate if resource has rg and is not at child resource scope
                            if ($lock.ResourceId -match $rgPattern) {
                                if ($resourceToDelete.Id.StartsWith($matches[0]) -and $lock.ResourceId -notlike '*/resourcegroups/*/providers/*/providers/*') {
                                    $dependency += [PSCustomObject]@{
                                        Type = 'locks'
                                        Id = $lock.ResourceId
                                    }
                                }
                            }
                            elseif ($lock.ResourceId -notlike '*/resourcegroups/*') {
                                $dependency += [PSCustomObject]@{
                                    Type = 'locks'
                                    Id = $lock.ResourceId
                                }
                            }
                        }
                        if ($dependency) {
                            $dependency = $dependency | Sort-Object Id -Unique | Where-Object {$_.Id -ne $resourceToDelete.Id}
                            return $dependency
                        }
                    }
                }
            }
        }
        function Get-AzPolicyAssignmentDeletionDependency {
            param (
                $resourceToDelete
            )
            $dependency = @()
            if ($resourceToDelete.Type -in $DeletionSupportedResourceType) {
                switch ($resourceToDelete.Type) {
                    'Microsoft.Authorization/policyAssignments' {
                        $depPolicyAssignment = $resourceToDelete
                    }
                    'Microsoft.Authorization/policyDefinitions' {
                        $depPolicyAssignment = Get-AzPolicyAssignment -PolicyDefinitionId $resourceToDelete.Id -ErrorAction SilentlyContinue
                    }
                    'Microsoft.Authorization/policySetDefinitions' {
                        $query = "PolicyResources | where type == 'microsoft.authorization/policyassignments' and properties.policyDefinitionId == '$($resourceToDelete.Id)' | order by id asc"
                        $depPolicyAssignment = Search-AzGraphDeletionDependency -query $query
                        if ($depPolicyAssignment) {
                            #Loop through each return from graph cache and validate resource is still present in Azure
                            $depPolicyAssignment = foreach ($policyAssignment in $depPolicyAssignment) {Get-AzPolicyAssignment -Id $policyAssignment.Id -ErrorAction SilentlyContinue}
                        }
                    }
                }
            }
            if ($depPolicyAssignment) {
                foreach ($policyAssignment in $depPolicyAssignment) {
                    $dependency += [PSCustomObject]@{
                        Type = $policyAssignment.Type
                        Id = $policyAssignment.Id
                    }
                    if ($policyAssignment.IdentityType -eq 'SystemAssigned') {
                        $depSystemAssignedRoleAssignment = $null
                        $depSystemAssignedRoleAssignment = Get-AzRoleAssignment -ObjectId $policyAssignment.IdentityPrincipalId -Scope $policyAssignment.Scope
                        if ($depSystemAssignedRoleAssignment) {
                            foreach ($roleAssignmentId in $depSystemAssignedRoleAssignment.RoleAssignmentId) {
                                #Filter through each return and validate resource is not at child resource scope
                                if ($roleAssignmentId -notlike '*/resourcegroups/*/providers/*/providers/*') {
                                    $dependency += [PSCustomObject]@{
                                        Type = 'roleAssignments'
                                        Id = $roleAssignmentId
                                    }
                                }
                                else {
                                    Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.ResourceDependencyNested' -LogStringValues $roleAssignmentId, $policyAssignment.Id
                                }
                            }
                        }
                    }
                }
            }
            if ($dependency) {
                $dependency = $dependency | Sort-Object Id -Unique | Where-Object {$_.Id -ne $resourceToDelete.Id}
                return $dependency
            }
        }
        function Get-AzPolicyDefinitionDeletionDependency {
            param (
                $resourceToDelete
            )
            if ($resourceToDelete.Type -eq 'Microsoft.Authorization/policyDefinitions') {
                $dependency = @()
                $query = "PolicyResources | where type == 'microsoft.authorization/policysetdefinitions' and properties.policyType == 'Custom' | project id, type, policyDefinitions = (properties.policyDefinitions) | mv-expand policyDefinitions | project id, type, policyDefinitionId = tostring(policyDefinitions.policyDefinitionId) | where policyDefinitionId == '$($resourceToDelete.Id)' | order by policyDefinitionId asc | order by id asc"
                $depPolicySetDefinition = Search-AzGraphDeletionDependency -query $query
                if ($depPolicySetDefinition) {
                    $depPolicySetDefinition = foreach ($policySetDefinition in $depPolicySetDefinition) {
                        #Loop through each return from graph cache and validate resource is still present in Azure
                        $policy = Get-AzPolicySetDefinition -Id $policySetDefinition.Id -ErrorAction SilentlyContinue
                        if ($policy) {
                            $dependency += [PSCustomObject]@{
                                Type = $policy.Type
                                Id = $policy.Id
                            }
                            $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $policy
                        }
                    }
                }
                if ($dependency) {
                    $dependency = $dependency | Sort-Object Id -Unique | Where-Object {$_.Id -ne $resourceToDelete.Id}
                    return $dependency
                }
            }
        }
        function Search-AzGraphDeletionDependency {
            param (
                $query,

                $PartialMgDiscoveryRoot = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot')
            )
            $results = @()
            if ($PartialMgDiscoveryRoot) {
                foreach ($managementRoot in $PartialMgDiscoveryRoot) {
                    $subscriptions = Get-AzOpsNestedSubscription -Scope $managementRoot
                    $results += Search-AzOpsAzGraph -ManagementGroupName $managementRoot -Query $query -ErrorAction Stop
                    if ($subscriptions) {
                        $results += Search-AzOpsAzGraph -Subscription $subscriptions -Query $query -ErrorAction Stop
                    }
                }
            }
            else {
                $results = Search-AzOpsAzGraph -Query $query -UseTenantScope -ErrorAction Stop
            }
            if ($results) {
                $results = $results | Sort-Object Id -Unique
                return $results
            }
        }

        $dependencyMissing = $null
        #Adjust TemplateParameterFilePath to compensate for policyDefinitions and policySetDefinitions usage of parameters.json
        if ($TemplateParameterFilePath -and $TemplateFilePath -eq (Resolve-Path (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate')).Path) {
            $TemplateFilePath = $TemplateParameterFilePath
        }
        #Deployment Name
        $fileItem = Get-Item -Path $TemplateFilePath
        $removeJobName = $fileItem.BaseName -replace '\.json$' -replace ' ', '_'
        $removeJobName = "AzOps-RemoveResource-$removeJobName"
        Write-AzOpsMessage -LogLevel Important -LogString 'Remove-AzOpsDeployment.Processing' -LogStringValues $removeJobName, $TemplateFilePath

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

        #region Validate template type AzOps generated or not
        $schemavalue = '$schema'
        $customDeletion = $false
        if ($templateContent.metadata._generator.name -eq "AzOps" -or $templateContent.$schemavalue -like "*deploymentParameters.json#") {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Remove-AzOpsDeployment.Metadata.AzOps' -LogStringValues $TemplateFilePath
        }
        elseif ($true -eq $CustomTemplateResourceDeletion) {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Remove-AzOpsDeployment.Metadata.Custom' -LogStringValues $TemplateFilePath
            $customDeletion = $true
        }
        else {
            Write-AzOpsMessage -LogLevel Error -LogString 'Remove-AzOpsDeployment.Metadata.Failed' -LogStringValues $TemplateFilePath
            return
        }
        #endregion Validate template type AzOps generated or not

        #region Resolve Scope
        try {
            $scopeObject = New-AzOpsScope -Path $TemplateFilePath -StatePath $StatePath -ErrorAction Stop -WhatIf:$false
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.Scope.Failed' -LogStringValues $TemplateFilePath -ErrorRecord $_
            return
        }
        if (-not $scopeObject) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.Scope.Empty' -LogStringValues $TemplateFilePath
            return
        }
        #endregion Resolve Scope

        #region SetContext
        Set-AzOpsContext -ScopeObject $scopeObject
        #endregion SetContext

        #region remove resources
        if ($customDeletion -eq $false -and $scopeObject.Resource -in $DeletionSupportedResourceType) {
            $dependency = @()
            switch ($scopeObject.Resource) {
                # Check resource existance through optimal path
                'locks' {
                    $resourceToDelete = Get-AzResourceLock -Scope "/subscriptions/$($ScopeObject.Subscription)" -ErrorAction SilentlyContinue | Where-Object { $_.ResourceID -eq $ScopeObject.Scope }
                }
                'policyAssignments' {
                    $resourceToDelete = Get-AzPolicyAssignment -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                    if ($resourceToDelete) {
                        $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete
                        $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete
                    }
                }
                'policyDefinitions' {
                    $resourceToDelete = Get-AzPolicyDefinition -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                    if ($resourceToDelete) {
                        $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete
                        $dependency += Get-AzPolicyDefinitionDeletionDependency -resourceToDelete $resourceToDelete
                        $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete
                    }
                }
                'policyExemptions' {
                    $resourceToDelete = Get-AzPolicyExemption -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                    if ($resourceToDelete) {
                        $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete
                    }
                }
                'policySetDefinitions' {
                    $resourceToDelete = Get-AzPolicySetDefinition -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                    if ($resourceToDelete) {
                        $dependency += Get-AzPolicyAssignmentDeletionDependency -resourceToDelete $resourceToDelete
                        $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete
                    }
                }
                'roleAssignments' {
                    $resourceToDelete = (Invoke-AzRestMethod -Path "$($scopeObject.Scope)?api-version=2022-04-01" | Where-Object { $_.StatusCode -eq 200 }).Content | ConvertFrom-Json -Depth 100
                    if ($resourceToDelete) {
                        $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete
                    }
                }
                'resourceGroups' {
                    $resourceToDelete = Get-AzResourceGroup -Id $scopeObject.Scope -ErrorAction SilentlyContinue
                    if ($resourceToDelete) {
                        $resourceToDelete | Add-Member -MemberType NoteProperty -Name "Type" -Value "$($scopeObject.Type)"
                        $resourceToDelete | Add-Member -MemberType NoteProperty -Name "SubscriptionId" -Value "$($scopeObject.Subscription)"
                        $resourceToDelete | Add-Member -MemberType NoteProperty -Name "Id" -Value "$($resourceToDelete.ResourceId)"
                        $dependency += Get-AzLocksDeletionDependency -resourceToDelete $resourceToDelete
                    }
                }
            }
            # If no resource to delete was found return
            if (-not $resourceToDelete) {
                Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.ResourceNotFound' -LogStringValues $scopeObject.Resource, $scopeObject.Scope
                $results = 'What if operation failed:{1}Deletion of target resource {0}.{1}Resource could not be found' -f $scopeObject.scope, [environment]::NewLine
                Set-AzOpsWhatIfOutput -FilePath $TemplateFilePath -Results $results -RemoveAzOpsFlag $true
                return
            }
            if ($dependency) {
                foreach ($resource in $dependency) {
                    if ($resource.Id -notin $deletionList.ScopeObject.Scope) {
                        Write-AzOpsMessage -LogLevel Critical -LogString 'Remove-AzOpsDeployment.ResourceDependencyNotFound' -LogStringValues $resource.Id, $scopeObject.Scope
                        $results = 'Missing resource dependency:{2}{0} for successful deletion of {1}.{2}{2}Please add dependent resource to pull request and retry.' -f $resource.Id, $scopeObject.scope, [environment]::NewLine
                        Set-AzOpsWhatIfOutput -FilePath $TemplateFilePath -Results $results -RemoveAzOpsFlag $true
                        $dependencyMissing = [PSCustomObject]@{
                            dependencyMissing = $true
                        }
                    }
                }
            }
            else {
                $results = 'What if successful:{1}Performing the operation:{1}Deletion of target resource {0}.' -f $scopeObject.scope, [environment]::NewLine
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Set-AzOpsWhatIfOutput.WhatIfResults' -LogStringValues $results
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFile'
                Set-AzOpsWhatIfOutput -FilePath $TemplateFilePath -Results $results -RemoveAzOpsFlag $true
            }
            if ($dependencyMissing) {
                return $dependencyMissing
            }
            elseif ($dependency) {
                $results = 'What if successful:{1}Performing the operation:{1}Deletion of target resource {0}.' -f $scopeObject.scope, [environment]::NewLine
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Set-AzOpsWhatIfOutput.WhatIfResults' -LogStringValues $results
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFile'
                Set-AzOpsWhatIfOutput -FilePath $TemplateFilePath -Results $results -RemoveAzOpsFlag $true
            }
            if ($PSCmdlet.ShouldProcess("Remove $($scopeObject.Scope)?")) {
                $null = Remove-AzResourceRaw -ScopeObject $scopeObject -TemplateFilePath $TemplateFilePath -TemplateParameterFilePath $TemplateParameterFilePath
            }
            else {
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzOpsDeployment.SkipDueToWhatIf'
            }
        }
        elseif ($customDeletion -eq $false -and $scopeObject.Resource -notin $DeletionSupportedResourceType) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.SkipUnsupportedResource' -LogStringValues $TemplateFilePath -Target $scopeObject
            return
        }
        elseif ($customDeletion -eq $true)  {
            # Perform a New-AzOpsDeployment using WhatIf with ResourceIdOnly to extrapolate resources inside template
            $removalJob = New-AzOpsDeployment -DeploymentName $DeploymentName -TemplateFilePath $TemplateFilePath -TemplateParameterFilePath $TemplateParameterFilePath -WhatIfResultFormat 'ResourceIdOnly' -WhatIf:$true
            if ($removalJob.results.Changes.Count -gt 0) {
                # Initialize array to store items that need retry
                $retry = @()
                $removalJobChanges = Set-AzOpsRemoveOrder -DeletionList $removalJob.results.Changes -Index { (New-AzOpsScope -Scope $_.FullyQualifiedResourceId -WhatIf:$false).Resource }
                $allResults = @()
                foreach ($change in $removalJobChanges) {
                    $resource = $null
                    $resourceScopeObject = $null
                    $removeAction = $null
                    # Check if the resource exists
                    $resourceScopeObject = New-AzOpsScope -Scope $change.FullyQualifiedResourceId -WhatIf:$false
                    $resource = Get-AzOpsResource -ScopeObject $resourceScopeObject -ErrorAction SilentlyContinue
                    if ($resource) {
                        $results = 'What if successful:{1}Performing the operation:{1}Deletion of target resource {0}.' -f $resourceScopeObject.Scope, [environment]::NewLine
                        $allResults += $results
                        Write-AzOpsMessage -LogLevel Verbose -LogString 'Set-AzOpsWhatIfOutput.WhatIfResults' -LogStringValues $results
                        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFile'
                        # Check if the removal should be performed
                        if ($PSCmdlet.ShouldProcess("Remove $($resourceScopeObject.Scope)?")) {
                            $removeAction = Remove-AzResourceRaw -ScopeObject $resourceScopeObject -TemplateFilePath $TemplateFilePath -TemplateParameterFilePath $TemplateParameterFilePath
                            # If removal failed, add to retry
                            if ($removeAction.Status -eq 'failed') {
                                $retry += $removeAction
                            }
                        }
                        else {
                            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzOpsDeployment.SkipDueToWhatIf'
                        }
                    }
                    else {
                        # Log warning if resource not found
                        Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.ResourceNotFound' -LogStringValues $ScopeObject.Resource, $change.FullyQualifiedResourceId
                        $results = 'What if operation failed:{1}Deletion of target resource {0}.{1}Resource could not be found' -f $change.FullyQualifiedResourceId, [environment]::NewLine
                        $allResults += $results
                    }

                }
                $baseTemplateCheck = $TemplateFilePath -replace '\.bicep$', '.json'
                if ($TemplateParameterFilePath) {
                    $baseParameterCheck = $TemplateParameterFilePath -replace '\.bicepparam$', 'parameters.json'
                }
                if ($DeleteSet) {
                    $deleteSetCheck = $DeleteSet  -replace '\.bicep$', '.json'
                    $deleteSetCheck = $deleteSetCheck  -replace '\.bicepparam$', '.parameters.json'
                    # Check if template and parameter file exist in $DeleteSet, example AzOps has been instructed to remove template.json but not the associated parameter.json
                    $resultsFileAssociation = switch ($null) {
                        { $baseTemplateCheck -notin $deleteSetCheck -and $baseParameterCheck -notin $deleteSetCheck } {
                            'Missing template and parameter file association:{2}{0} and {1} for deletion.{2}{2}Ensure that you have reviewed and confirmed the necessity of each deletion.{2}If you are deleting files with extension .bicep or .bicepparam, keep in mind that AzOps converts them to .json or .parameters.json for deletion processing and outputs the results from the converted files here.{2}' -f $TemplateFilePath, $TemplateParameterFilePath, [environment]::NewLine
                        }
                        { $baseTemplateCheck -notin $deleteSetCheck } {
                            'Missing template file association:{1}{0} for deletion.{1}{1}Ensure that you have reviewed and confirmed the necessity of each deletion.{1}If you are deleting files with extension .bicep or .bicepparam, keep in mind that AzOps converts them to .json or .parameters.json for deletion processing and outputs the results from the converted files here.{1}' -f $TemplateFilePath, [environment]::NewLine
                        }
                        { $baseParameterCheck -notin $deleteSetCheck } {
                            'Missing parameter file association:{1}{0} for deletion.{1}{1}Ensure that you have reviewed and confirmed the necessity of each deletion.{1}If you are deleting files with extension .bicep or .bicepparam, keep in mind that AzOps converts them to .json or .parameters.json for deletion processing and outputs the results from the converted files here.{1}' -f $TemplateParameterFilePath, [environment]::NewLine
                        }
                    }
                    # If there are $resultsFileAssociation, combine them with existing results and log a warning
                    if ($resultsFileAssociation) {
                        $finalResults = @()
                        $finalResults += $resultsFileAssociation
                        $finalResults += $allResults
                        $allResults = $finalResults
                        Write-AzOpsMessage -LogLevel Warning -LogString 'Set-AzOpsWhatIfOutput.WhatIfResults' -LogStringValues $allResults
                    }
                }
                Set-AzOpsWhatIfOutput -FilePath $TemplateFilePath -ParameterFilePath $TemplateParameterFilePath -Results $allResults -RemoveAzOpsFlag $true
                if ($retry.Count -gt 0) {
                    # Retry failed removals recursively
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzOpsDeployment.Resource.RetryCount' -LogStringValues $retry.Count
                    foreach ($try in $retry) { $try.Status = $null }
                    $removeActionRecursive = Remove-AzResourceRaw -InputObject $retry -Recursive
                    $removeActionRecursiveRemaining = $removeActionRecursive | Where-Object { $_.Status -eq 'failed' }
                    return $removeActionRecursiveRemaining
                }
            }
            else {
                # No resource to remove was found
                Write-AzOpsMessage -LogLevel Warning -LogString 'Remove-AzOpsDeployment.ResourceNotFound' -LogStringValues $scopeObject.Resource, $scopeObject.Scope
                $results = 'What if operation failed:{1}Deletion of target resource {0}.{1}Resource could not be found' -f $scopeObject.Scope, [environment]::NewLine
                Set-AzOpsWhatIfOutput -FilePath $TemplateFilePath -ParameterFilePath $TemplateParameterFilePath -Results $results -RemoveAzOpsFlag $true
                return
            }
        }
        #endregion remove resources
    }
}

function Remove-AzOpsInvalidCharacter {

    <#
        .SYNOPSIS
            Takes string input and removes invalid characters.
        .DESCRIPTION
            Takes string input and removes invalid characters.
        .PARAMETER String
            String to remove invalid characters from.
        .PARAMETER Override
            Accepts input to skip selected invalid characters.
        .EXAMPLE
            > Remove-AzOpsInvalidCharacter -String "microsoft.operationalinsights_workspaces_savedsearches-fgh341_logmanagement(fgh343)_logmanagement|countofiislogentriesbyhostrequestedbyclient.json"
            Function returns with the '|' invalid character removed:
            microsoft.operationalinsights_workspaces_savedsearches-fgh341_logmanagement(fgh343)_logmanagementcountofiislogentriesbyhostrequestedbyclient.json
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $String,
        [array]
        $InvalidChars = @('"','<','>','|','â–º','â—„','↕','‼','¶','§','â–¬','↨','↑','↓','→','∟','↔','*','?','\','/',':'),
        [array]
        $Override
    )

    process {
        # If Override has been provided remove them from InvalidChars
        if ($Override) {
            $InvalidChars = Compare-Object -ReferenceObject $InvalidChars -DifferenceObject $Override -PassThru
        }
        # Check if string contains invalid characters
        $pattern = $InvalidChars | Out-String -NoNewline
        if ($String -match "[$pattern]") {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzOpsInvalidCharacter.Invalid' -LogStringValues $String
            # Arrange string into character array
            $fileNameChar = $String.ToCharArray()
            # Iterate over each character in string
            foreach ($character in $fileNameChar) {
                # If character exists in invalid array then replace character
                if ($character -in $InvalidChars) {
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzOpsInvalidCharacter.Removal' -LogStringValues $character, $String
                    # Remove invalid character
                    $String = $String.Replace($character.ToString(),'')
                }
            }
        }
        # Always remove square brackets
        $String = $String -replace "(\[|\])",""
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzOpsInvalidCharacter.Completed' -LogStringValues $String
        # Return processed string
        return $String
    }
}

function Remove-AzResourceRaw {

    <#
        .SYNOPSIS
            Performs resource deletion in Azure at any scope.
        .DESCRIPTION
            Performs resource deletion in Azure with FullyQualifiedResourceId and ScopeObject.
        .PARAMETER TemplateFilePath
            Path where the ARM templates can be found.
        .PARAMETER TemplateParameterFilePath
            Path where the parameters of the ARM templates can be found.
        .PARAMETER ScopeObject
            Resource to delete.
        .PARAMETER InputObject
            Object containing items for processing, used in combination with parameter Recursive.
        .PARAMETER Recursive
            If specified, performs recursive resource deletion and requires use of parameter InputObject.
        .EXAMPLE
            > Remove-AzResourceRaw -ScopeObject $ScopeObject -TemplateFilePath $TemplateFilePath -TemplateParameterFilePath $TemplateParameterFilePath
            Name Value
            ---- -----
            TemplateFilePath /root/managementgroup/subscription/resourcegroup/template.json
            TemplateParameterFilePath /root/managementgroup/subscription/resourcegroup/template.parameters.json
            ScopeObject ScopeObject
            Status success

            > Remove-AzResourceRaw -InputObject $retry -Recursive
            Name Value
            ---- -----
            TemplateFilePath /root/managementgroup/subscription/resourcegroup/template.json
            TemplateParameterFilePath /root/managementgroup/subscription/resourcegroup/template.parameters.json
            ScopeObject ScopeObject
            Status success
    #>


    [CmdletBinding()]
    param (
        [string]
        $TemplateFilePath,
        [string]
        $TemplateParameterFilePath,
        [AzOpsScope]
        $ScopeObject,
        [array]
        $InputObject,
        [switch]
        $Recursive
    )

    process {
        function Remove-AzResourceRawRecursive {

            <#
                .SYNOPSIS
                    Performs recursive resource deletion in Azure at any scope.
                .DESCRIPTION
                    Takes $InputObject and performs recursive resource deletion in Azure and exhaust any permutation.
                .PARAMETER InputObject
                    Parameter containing items for processing.
                .PARAMETER CurrentOrder
                    Internal parameter to track recursive progress.
                .PARAMETER OutputObject
                    Track item processing and return result.
                .EXAMPLE
                    > $successFullItems, $failedItems = Remove-AzResourceRawRecursive -InputObject $retry
                    Example of a $retry array with 6 items, the number of permutations will be 6×5×4×3×2×1=720
            #>


            [CmdletBinding()]
            param (
                [array]
                $InputObject,
                [array]
                $CurrentOrder = @(),
                [array]
                $OutputObject = @()
            )

            process {
                if ($InputObject.Count -eq 0) {
                    # Base case: All items have been used, perform action on the current order
                    foreach ($item in $CurrentOrder) {
                        if ($item.Status -eq 'failed' -or $null -eq $item.Status) {
                            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzResourceRawRecursive.Processing' -LogStringValues $item.ScopeObject.Resource, $item.ScopeObject.Scope
                            # Attempt to remove the resource
                            $result = Remove-AzResourceRaw -ScopeObject $item.ScopeObject -TemplateFilePath $item.TemplateFilePath -TemplateParameterFilePath $item.TemplateParameterFilePath
                            if ($result.Status -eq 'failed' -and $result.ScopeObject.Scope -notin $OutputObject.ScopeObject.Scope){
                                # Add failed result to the output object
                                $OutputObject += $result
                            }
                        }
                    }
                    # Return the final result
                    return $OutputObject
                }
                else {
                    if ($InputObject -and $OutputObject) {
                        # Filter out items already processed successfully
                        $filteredOutputObject = @()
                        foreach ($item in $InputObject) {
                            if ($item.ScopeObject.Scope -in $OutputObject.ScopeObject.Scope) {
                                foreach ($output in $OutputObject) {
                                    if ($output.ScopeObject.Scope -eq $item.ScopeObject.Scope -and $output.Status -eq 'failed') {
                                        # Add previously failed item to the filtered output
                                        $filteredOutputObject += $output
                                        continue
                                    }
                                }
                            }
                        }
                        if ($filteredOutputObject) {
                            $InputObject = $filteredOutputObject
                        }
                    }
                    # Recursive case: Try each item in the current position and recurse with the remaining items
                    foreach ($item in $InputObject) {
                        $remainingItems = $InputObject -ne $item
                        $newOrder = $CurrentOrder + $item
                        # Recursively call Remove-AzResourceRawRecursive
                        $OutputObject = Remove-AzResourceRawRecursive -InputObject $remainingItems -CurrentOrder $newOrder -OutputObject $OutputObject
                    }
                    # Return the output after all permutations
                    return $OutputObject
                }
            }
        }
        if ($null -ne $InputObject -and $Recursive) {
            # Perform recursive resource deletion
            $result = Remove-AzResourceRawRecursive -InputObject $InputObject
            if ($result) {
                return $result
            }
            else {
                return
            }
        }
        elseif ($null -eq $InputObject -and $Recursive) {
            # Recursive resource deletion missing input
            Write-AzOpsMessage -LogLevel Error -LogString 'Remove-AzResourceRaw.Resource.Recursive.Missing'
            return
        }
        else {
            if (-not $ScopeObject) {
                # Resource deletion missing input
                Write-AzOpsMessage -LogLevel Error -LogString 'Remove-AzResourceRaw.Resource.Missing'
                return
            }
            # Construct result object
            $result = [PSCustomObject]@{
                TemplateFilePath = $TemplateFilePath
                TemplateParameterFilePath = $TemplateParameterFilePath
                ScopeObject = $ScopeObject
                Status = 'success'
            }
            # Check if the resource exists
            $resource = Get-AzOpsResource -ScopeObject $ScopeObject -ErrorAction SilentlyContinue
            # Remove the resource if it exists
            if ($resource) {
                try {
                    # Set Azure context for removal operation
                    Set-AzOpsContext -ScopeObject $ScopeObject
                    $null = Remove-AzResource -ResourceId $ScopeObject.Scope -Force -ErrorAction Stop
                    $maxAttempts = 4
                    $attempt = 1
                    $gone = $false
                    while ($gone -eq $false -and $attempt -le $maxAttempts) {
                        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzResourceRaw.Resource.CheckExistence' -LogStringValues $ScopeObject.Scope
                        Start-Sleep -Seconds 10
                        $tryResource = Get-AzOpsResource -ScopeObject $ScopeObject -ErrorAction SilentlyContinue
                        if (-not $tryResource) {
                            $gone = $true
                        }
                        $attempt++
                    }
                }
                catch {
                    # Log failure message
                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'Remove-AzResourceRaw.Resource.Failed' -LogStringValues $ScopeObject.Resource, $ScopeObject.Scope
                    $result.Status = 'failed'
                }
            }
            else {
                # Log not found message
                $result.Status = 'notfound'
            }
            # Return result object
            return $result
        }
    }
}

function Save-AzOpsManagementGroupChild {

    <#
        .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-AzOpsManagementGroupChild -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-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Starting'
        Invoke-PSFProtectedCommand -ActionString 'Save-AzOpsManagementGroupChild.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-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Processing' -LogStringValues $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-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Data.StatePath' -LogStringValues $scopeStatepath
        Write-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Data.FileName' -LogStringValues $statepathFileName
        Write-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Data.Directory' -LogStringValues $statepathDirectory
        Write-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Data.ScopeDirectory' -LogStringValues $statepathScopeDirectory
        Write-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Data.ScopeDirectoryParent' -LogStringValues $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-AzOpsMessage -LogLevel Important -LogString 'Save-AzOpsManagementGroupChild.Moving.Source' -LogStringValues $exisitingScopePath
                    Move-Item -Path $exisitingScopePath.Parent -Destination $statepathScopeDirectoryParent -Force
                    Write-AzOpsMessage -LogLevel Important -LogString 'Save-AzOpsManagementGroupChild.Moving.Destination' -LogStringValues $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

        }
        # Create empty object for any discovered subscriptions below
        $subscriptions = @()
        # Based on $scopeObject perform unique logic
        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) {
                            # Subscription discovered at ManagementGroup scope, collect subscription information based on $child object and store information in $subscriptions for later
                            $subscriptions += Save-AzOpsManagementGroupChild -Scope $child.Id -StatePath $StatePath
                        }
                        else {
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Save-AzOpsManagementGroupChild.Subscription.NotFound' -LogStringValues $child.Name
                        }
                    }
                    else {
                        Save-AzOpsManagementGroupChild -Scope $child.Id -StatePath $StatePath
                    }
                }
            }
            subscriptions {
                if (($script:AzOpsSubscriptions.Id -contains $scopeObject.Scope) -and ($script:AzOpsAzManagementGroup.Children | Where-Object Name -eq $scopeObject.Name)) {
                    # Subscription matching conditions found, construct subscription and object statepath into $return and output information to caller
                    $return = [PSCustomObject]@{
                        Subscription = ($script:AzOpsAzManagementGroup.Children | Where-Object Name -eq $scopeObject.Name)
                        Path = $scopeObject.StatePath
                    }
                    return $return
                }
            }
        }
        # If $subscriptions exists process all subscriptions with parallel to increase performance during folder/file creation
        if ($subscriptions) {
            # Prepare Input Data for parallel processing
            $runspaceData = @{
                AzOpsPath                       = "$($script:ModuleRoot)\AzOps.psd1"
                StatePath                       = $StatePath
                runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup
                runspace_AzOpsSubscriptions     = $script:AzOpsSubscriptions
                runspace_AzOpsPartialRoot       = $script:AzOpsPartialRoot
                runspace_AzOpsResourceProvider  = $script:AzOpsResourceProvider
            }
            $subscriptions | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                $subscription = $_
                $runspaceData = $using:runspaceData

                Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru

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

                & $azOps {
                    ConvertTo-AzOpsState -Resource $subscription.Subscription -ExportPath $subscription.Path -StatePath $runspaceData.StatePath
                }
            }
            Clear-PSFMessage
        }
    }
}

function Search-AzOpsAzGraph {

    <#
        .SYNOPSIS
            Search Graph based on input query combined with scope ManagementGroupName or Subscription Id.
            Manages paging of results, ensuring completeness of results.
        .PARAMETER UseTenantScope
            Use Tenant as Scope true or false
        .PARAMETER ManagementGroupName
            ManagementGroup Name
        .PARAMETER Subscription
            Subscription Id's
        .PARAMETER Query
            AzureResourceGraph-Query
        .EXAMPLE
            > Search-AzOpsAzGraph -ManagementGroupName "5663f39e-feb1-4303-a1f9-cf20b702de61" -Query "policyresources | where type == 'microsoft.authorization/policyassignments'"
            Discover all policy assignments deployed at Management Group scope and below
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [switch]
        $UseTenantScope,
        [Parameter(Mandatory = $false)]
        [string]
        $ManagementGroupName,
        [Parameter(Mandatory = $false)]
        [object]
        $Subscription,
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [object]
        $Query
    )

    process {
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Search-AzOpsAzGraph.Processing' -LogStringValues $Query
        $results = @()
        if ($UseTenantScope) {
            do {
                $processing = Search-AzGraph -UseTenantScope -Query $Query -AllowPartialScope -SkipToken $processing.SkipToken -ErrorAction Stop
                $results += $processing
            }
            while ($processing.SkipToken)
        }
        if ($ManagementGroupName) {
            do {
                $processing = Search-AzGraph -ManagementGroup $ManagementGroupName -Query $Query -AllowPartialScope -SkipToken $processing.SkipToken -ErrorAction Stop
                $results += $processing
            }
            while ($processing.SkipToken)
        }
        if ($Subscription) {
            # Create a counter, set the batch size, and prepare a variable for the results
            $counter = [PSCustomObject] @{ Value = 0 }
            $batchSize = 1000
            # Group subscriptions into batches to conform with graph limits
            $subscriptionBatch = $Subscription | Group-Object -Property { [math]::Floor($counter.Value++ / $batchSize) }
            foreach ($group in $subscriptionBatch) {
                do {
                    $processing = Search-AzGraph -Subscription ($group.Group).Id -Query $Query -SkipToken $processing.SkipToken -ErrorAction Stop
                    $results += $processing
                }
                while ($processing.SkipToken)
            }
        }
        if ($results) {
            $resultsType = @()
            foreach ($result in $results) {
                # Process each graph result and normalize ProviderNamespace casing
                foreach ($ResourceProvider in $script:AzOpsResourceProvider) {
                    if ($ResourceProvider.ProviderNamespace -eq $result.type.Split('/')[0]) {
                        foreach ($ResourceTypeName in $ResourceProvider.ResourceTypes.ResourceTypeName) {
                            if ($ResourceTypeName -eq $result.type.Split('/')[1]) {
                                $result.type = ($result.type).replace($result.type.Split('/')[0],$ResourceProvider.ProviderNamespace)
                                $result.type = ($result.type).replace($result.type.Split('/')[1],$ResourceTypeName)
                                $resultsType += $result
                                break
                            }
                        }
                        break
                    }
                }
            }
            Write-AzOpsMessage -LogLevel Debug -LogString 'Search-AzOpsAzGraph.Processing.Done' -LogStringValues $Query
            return $resultsType
        }
        else {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Search-AzOpsAzGraph.Processing.NoResult' -LogStringValues $Query
        }
    }

}

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 [AzOpsScope] into which context to change.
        .EXAMPLE
            > Set-AzOpsContext -ScopeObject $scopeObject
            Changes the current context to the subscription of $scopeObject.
    #>


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

    begin {
        $context = Get-AzContext
    }

    process {
        if (-not $ScopeObject.Subscription) { return }
        if ($context.Subscription.Id -ne $ScopeObject.Subscription) {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsContext.Change' -LogStringValues $context.Subscription.Name, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription
            $null = Set-AzContext -SubscriptionId $scopeObject.Subscription -WhatIf:$false
        }
    }
}

function Set-AzOpsRemoveOrder {

    <#
        .SYNOPSIS
            Sorts a custom object list based on a specified priority order using a user-defined index.
        .DESCRIPTION
            Used to sort deletion priority, aka locks are removed prior to resource deletion attempts.
        .PARAMETER DeletionList
            Custom object list to be sorted based on the defined priority.
        .PARAMETER Index
            Script block that determines the index used for sorting the deletion list.
        .PARAMETER Priority
            Optional array of strings representing the priority order. Defaults to a predefined order if not provided.
        .EXAMPLE
            > $sortedList = Set-AzOpsRemoveOrder -DeletionList $myCustomObjectList -Index { $_.SomeProperty }
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $DeletionList,
        [Parameter(Mandatory = $true)]
        [scriptblock]
        $Index,
        [string[]]
        $Priority = @(
            "locks",
            "policyExemptions",
            "policyAssignments",
            "policySetDefinitions",
            "policyDefinitions",
            "resourceGroups",
            "managementGroups"
        )
    )

    process {
        #Sort 'DeletionList' based on 'Priority'
        $deletionListSorted = $DeletionList | Sort-Object -Property {
            $resolvedIndex = & $Index
            $priorityIndex = $Priority.IndexOf($resolvedIndex)
            if ($priorityIndex -eq -1) {
                # Set a default priority for items not found in Priority
                return [int]::MaxValue
            }
            else {
                return $priorityIndex
            }
        }
        # Return processed list
        return $deletionListSorted
    }
}

function Set-AzOpsStringLength {

    <#
        .SYNOPSIS
            Takes string input and shortens it according to maximum length.
        .DESCRIPTION
            Takes string input and shortens it according to maximum length.
        .PARAMETER String
            String to shorten.
        .PARAMETER MaxStringLength
            Set the maximum length for returned string, default is 255 characters.
            Maximum filename length is based on underlying execution environments maximum allowed filename character limit of 255 and the additional characters added by AzOps for files measured by buffer <.parameters> <.json> or <.bicep>.
        .EXAMPLE
            > Set-AzOpsStringLength -String "microsoft.recoveryservices_vaults_replicationfabrics_replicationprotectioncontainers_replicationprotectioncontainermappings-test1-migratevault-1470815024_1541289ea1c5c535f89c0788063b3f5af00e91a2c63438851d90ef7143747149_cloud_308af796-701f-4d5d-ba68-a2434abb3c84_defaultrecplicationvm-containermapping"
            Setting the string length for the above example with default character limit returns the following:
            microsoft.recoveryservices_vaults_replicationfabrics_replicationprotectioncontainers_replicationprotectioncontainermappings-test1-migratevault-1470815024_1541289ea1c5c535f89c-BD89FDDC1A27FAD7C0E62CE2AA0A4513193D4F13907CDCF08E540BB5EFBBA9BA
    #>


    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]
        $String,
        [Parameter(Mandatory = $false)]
        [ValidateRange(155,255)]
        [int]
        $MaxStringLength = "255"
    )

    process {
        # Determine required buffer for ending, set buffer according to space required
        $buffer = ".parameters".Length + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix').Length
        # Check if string exceed maximum length
        if (($String.Length + $buffer) -gt $MaxStringLength){
            $overSize = $String.Length + $buffer - $MaxStringLength
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsStringLength.ToLong' -LogStringValues $String,$MaxStringLength,$overSize
            # Generate 64-character hash based on input string
            $stringStream = [IO.MemoryStream]::new([byte[]][char[]]$String)
            $stringHash = Get-FileHash -InputStream $stringStream -Algorithm SHA256
            $startOfNameLength = $MaxStringLength - $stringHash.Hash.Length - $buffer - 1
            # Process new name
            if ($startOfNameLength -gt 1) {
                $startOfName = $String.Substring(0,($startOfNameLength))
                $newName = $startofName + '-' + $stringHash.Hash
            }
            else {
                $newName = $stringHash.Hash + $endofName
            }
            # Construct new string with modified name
            $String = $String.Replace($String,$newName)
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsStringLength.Shortened' -LogStringValues $String,$MaxStringLength
            return $String
        }
        else {
            # Return original string, it is within limit
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsStringLength.WithInLimit' -LogStringValues $String,$MaxStringLength
            return $String
        }
    }
}

function Set-AzOpsWhatIfOutput {

    <#
        .SYNOPSIS
            Logs the output from a What-If deployment
        .DESCRIPTION
            Logs the output from a What-If deployment
        .PARAMETER Results
            The WhatIf result from a deployment
        .PARAMETER RemoveAzOpsFlag
            RemoveAzOpsFlag is set to true when a need to push content about deletion is required
        .PARAMETER ResultSizeLimit
            The character limit allowed for comments 64,000
        .PARAMETER ResultSizeMaxLimit
            The maximum upper character limit allowed for comments 64,600
        .PARAMETER FilePath
            Template File in scope of WhatIf
        .PARAMETER ParameterFilePath
            Parameter File in scope of WhatIf
        .EXAMPLE
            > Set-AzOpsWhatIfOutput -Results $results
            > Set-AzOpsWhatIfOutput -Results $results -RemoveAzOpsFlag $true
            > Set-AzOpsWhatIfOutput -FilePath '/templates/root/myresource.bicep' -Results $results -RemoveAzOpsFlag $true
    #>


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

        [Parameter(Mandatory = $false)]
        $RemoveAzOpsFlag = $false,

        [Parameter(Mandatory = $false)]
        $ResultSizeLimit = "64000",

        [Parameter(Mandatory = $false)]
        $ResultSizeMaxLimit = "64600",

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

        [Parameter(Mandatory = $false)]
        $ParameterFilePath
    )

    process {
        $tempPath = [System.IO.Path]::GetTempPath()
        if ((-not (Test-Path -Path ($tempPath + 'OUTPUT.md'))) -or (-not (Test-Path -Path ($tempPath + 'OUTPUT.json')))) {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFile'
            New-Item -Path ($tempPath + 'OUTPUT.md') -WhatIf:$false | Out-Null
            New-Item -Path ($tempPath + 'OUTPUT.json') -WhatIf:$false | Out-Null
        }

        if ($ParameterFilePath) {
            $resultHeadline = "$($FilePath.split([System.IO.Path]::DirectorySeparatorChar)[-1]) with $($ParameterFilePath.split([System.IO.Path]::DirectorySeparatorChar)[-1])"
        }
        else {
            $resultHeadline = $FilePath.split([System.IO.Path]::DirectorySeparatorChar)[-1]
        }

        # Measure input $Results.Changes content
        $resultString = $Results | Out-String
        $resultStringMeasure = $resultString | Measure-Object -Line -Character -Word
        # Measure current OUTPUT.md content
        $existingContentMd = Get-Content -Path ($tempPath + 'OUTPUT.md') -Raw
        $existingContentStringMd = $existingContentMd | Out-String
        $existingContentStringMeasureMd = $existingContentStringMd | Measure-Object -Line -Character -Word
        # Gather current OUTPUT.json content
        $existingContent = @(Get-Content -Path ($tempPath + 'OUTPUT.json') -Raw | ConvertFrom-Json -Depth 100)
        # Export results to json file
        Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFileAdding' -LogStringValues 'json', $FilePath, $ParameterFilePath
        if ($RemoveAzOpsFlag) {
            $resultJson = [PSCustomObject]@{
                WhatIfResult = $Results
                TemplateFile = $resultHeadline
            }
        }
        else {
            $resultJson = $results.Changes
            $resultJson | Add-Member -Name "TemplateFile" -Value $resultHeadline -MemberType NoteProperty -Force
        }
        $existingContent += $resultJson
        $existingContent = $existingContent | ConvertTo-Json -Depth 100
        Set-Content -Path ($tempPath + 'OUTPUT.json') -Value $existingContent -WhatIf:$false
        # Check if $existingContentStringMeasureMd and $resultStringMeasure exceed allowed size in $ResultSizeLimit
        if (($existingContentStringMeasureMd.Characters + $resultStringMeasure.Characters) -gt $ResultSizeLimit) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Set-AzOpsWhatIfOutput.WhatIfFileMax' -LogStringValues $ResultSizeLimit
            $mdOutput = 'WhatIf Results for {1}:{0} WhatIf is too large for comment field, for more details look at PR files to determine changes.' -f [environment]::NewLine, $resultHeadline
        }
        else {
            if ($RemoveAzOpsFlag) {
                if ($Results -match 'Missing resource dependency' ) {
                    $mdOutput = ':x: **Action Required**{0}WhatIf Results for Resource Deletion of {2}:{0}```{0}{1}{0}```' -f [environment]::NewLine, $resultString, $resultHeadline
                }
                elseif ($Results -match 'What if operation failed' -or $Results -match 'Missing template and parameter file association' -or $Results -match 'Missing template file association' -or $Results -match 'Missing parameter file association') {
                    $mdOutput = ':warning: WhatIf Results for Resource Deletion of {2}:{0}```{0}{1}{0}```' -f [environment]::NewLine, $resultString, $resultHeadline
                }
                else {
                    $mdOutput = ':white_check_mark: WhatIf Results for Resource Deletion of {2}:{0}```{0}{1}{0}```' -f [environment]::NewLine, $resultString, $resultHeadline
                }
            }
            else {
                $mdOutput = 'WhatIf Results for {2}:{0}```{0}{1}{0}```{0}' -f [environment]::NewLine, $resultString, $resultHeadline
            }
        }
        if ((($mdOutput | Measure-Object -Line -Character -Word).Characters + $existingContentStringMeasureMd.Characters) -le $ResultSizeMaxLimit) {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFileAdding' -LogStringValues 'markdown', $FilePath, $ParameterFilePath
            Add-Content -Path ($tempPath + 'OUTPUT.md') -Value $mdOutput -WhatIf:$false
        }
        else {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Set-AzOpsWhatIfOutput.WhatIfMessageMax' -LogStringValues $ResultSizeMaxLimit
        }
    }
}

function Write-AzOpsMessage {
    <#
        .SYNOPSIS
            Wrapper function to emit logs with Write-PSFMessage and ApplicationInsights.
        .PARAMETER ApplicationInsights
            Boolean to indicate if function should emit logs to ApplicationInsights.
        .PARAMETER Data
            Additional data points.
        .PARAMETER ErrorRecord
            Additional exception information from catch.
        .PARAMETER LogLevel
            Set level of message severity.
        .PARAMETER LogString
            String used to construct message.
        .PARAMETER LogStringValues
            String array used to enrich to message.
        .PARAMETER Metric
            Used to output metric.
        .PARAMETER MetricName
            Override FunctionName as MetricName.
        .PARAMETER FunctionName
            Static set FunctionName in log, otherwise Write-AzOpsMessage collects this from callStack.
        .PARAMETER ModuleName
            Static set ModuleName in log, otherwise Write-AzOpsMessage collects this from callStack.
        .PARAMETER Target
            The object that was processed when invoked.
        .EXAMPLE
            Write-AzOpsMessage -LogLevel Verbose
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [bool]
        $ApplicationInsights = (Get-PSFConfigValue -FullName 'AzOps.Core.ApplicationInsights'),
        [Parameter(Mandatory = $false)]
        [hashtable]
        $Data,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.ErrorRecord]
        $ErrorRecord,
        [Parameter(Mandatory = $true)]
        [ValidateSet("Critical", "Debug", "Error", "Host", "Important", "InternalComment", "Output", "Significant", "SomewhatVerbose", "System", "Verbose", "VeryVerbose", "Warning")]
        [string]
        $LogLevel,
        [Parameter(Mandatory = $true)]
        [string]
        $LogString,
        [Parameter(Mandatory = $false)]
        [string[]]
        $LogStringValues,
        [Parameter(Mandatory = $false)]
        [int]
        $Metric,
        [Parameter(Mandatory = $false)]
        [string]
        $MetricName,
        [Parameter(Mandatory = $false)]
        [string]
        $FunctionName,
        [Parameter(Mandatory = $false)]
        [string]
        $ModuleName,
        [Parameter(Mandatory = $false)]
        [string]
        $Target
    )
    begin {
        # Collect callStack information to enrich logs with accurate caller information
        $callStack = (Get-PSCallStack)[1]
        if (-not $FunctionName) { $FunctionName = $callStack.Command }
        if (-not $ModuleName) { $ModuleName = $callstack.InvocationInfo.MyCommand.ModuleName }
        if (-not $ModuleName) { $ModuleName = "<Unknown>" }
        $File = $callStack.Position.File
        $Line = $callStack.Position.StartLineNumber
    }
    process {
        # Evaluate message verbosity
        $logLevels = @{
            "Critical" = 1
            "Debug" = 8
            "Error" = 667
            "Host" = 2
            "Important" = 2
            "InternalComment" = 9
            "Output" = 2
            "Significant" = 3
            "SomewhatVerbose" = 6
            "System" = 7
            "Verbose" = 5
            "VeryVerbose" = 4
            "Warning" = 666
        }
        $intLevel = $logLevels[$LogLevel]
        [int]$messageInfo = Get-PSFConfigValue -FullName 'PSFramework.Message.Info.Maximum'
        if (($messageInfo -lt $intLevel) -or ([PSFramework.Message.MessageHost]::MinimumInformation -gt $intLevel) -and ($intLevel -notin 666..667)) {
            # Message is below desired log verbosity, skip
            return
        }
        # Generate unique logTag information, used to identify each log entry
        $logTag = (New-Guid).Guid + '-' + (Get-Date).TimeOfDay.TotalMilliseconds
        # Pass information to Write-PSFMessage, to emit local log
        $params = @{
            Level = $LogLevel
            String = $LogString
            StringValues = $LogStringValues
            Tag = $logTag
            ModuleName = $ModuleName
            FunctionName = $FunctionName
            File = $File
            Line = $Line
            Target = $Target
            ErrorRecord = $ErrorRecord
            Data = $Data
        }
        Write-PSFMessage @params
        if ($env:APPLICATIONINSIGHTS_CONNECTION_STRING -ne '' -and $ApplicationInsights -eq $true) {
            # Initiate export of log to ApplicationInsights
            try {
                # Gather log generated by Write-PSFMessage with retry/backoff logic
                $logMessage = Invoke-AzOpsScriptBlock -ArgumentList $FunctionName, $LogTag -ScriptBlock {
                    Get-PSFMessage -FunctionName $FunctionName | Where-Object { $_.Tags -eq $LogTag } -ErrorAction Stop
                } -RetryCount 5 -RetryWait 1 -RetryType Exponential -ErrorAction Stop
            }
            catch {
                Write-PSFMessage -Level Warning -Message 'Get-PSFMessage failing: {0}' -StringValues $_
            }
            if (-not $logMessage) {
                # No log message, return
                return
            }
            if ($logMessage.Count -gt 1) {
                # Log message has duplicate, return
                Write-PSFMessage -Level Warning -Message 'Get-PSFMessage has duplicate entires for {0} with tag {1}' -StringValues $LogString, $logTag
                return
            }
            # Construct ApplicationInsights object
            $azOpsMessage = [Microsoft.ApplicationInsights.TelemetryClient]::new()
            # Set ApplicationInsights connectionstring
            $azOpsMessage.TelemetryConfiguration.ConnectionString = $env:APPLICATIONINSIGHTS_CONNECTIONSTRING
            $azOpsMessage.Context.Session.Id = $PID
            # Adjust logMessage.Level to align with ApplicationInsights
            switch ($logMessage.Level) {
                { ($_ -eq "SomewhatVerbose") -or ($_ -eq "System") -or ($_ -eq "Debug") -or ($_ -eq "InternalComment") } {
                    $level = "Verbose"
                    break
                }
                { ($_ -eq "Important") -or ($_ -eq "Output") -or ($_ -eq "Host") -or ($_ -eq "Significant") -or ($_ -eq "VeryVerbose") } {
                    $level = "Information"
                    break
                }
                default {
                    $level = ($logMessage.Level).ToString()
                    break
                }
            }
            # Create ApplicationInsights customDimensions
            $logProperties = [System.Collections.Generic.Dictionary[string, string]]::new()
            $logProperties.Add("Timestamp", $logMessage.Timestamp)
            $logProperties.Add("ModuleName", $logMessage.ModuleName)
            $logProperties.Add("FunctionName", $logMessage.FunctionName)
            $logProperties.Add("TargetObject", $logMessage.TargetObject)
            $logProperties.Add("Data", $logmessage.Data)
            $logProperties.Add("Runspace", $logMessage.Runspace)
            $logProperties.Add("ComputerName", $logMessage.ComputerName)
            $logProperties.Add("File", $logMessage.File)
            $logProperties.Add("Line", $logMessage.Line)
            $logProperties.Add("CallStack", $logMessage.CallStack)
            $logProperties.Add("ErrorRecord", $logMessage.ErrorRecord)
            $logProperties.Add("String", $logMessage.String)
            # Create TrackTrace
            $azOpsMessage.TrackTrace($logMessage.Message, $level, $logProperties)
            # Create TrackEvent
            $azOpsMessage.TrackEvent($logMessage.FunctionName)
            if ($Metric) {
                # Create TrackMetric
                if (-not $MetricName) {
                    $azOpsMessage.TrackMetric($logMessage.FunctionName, $Metric)
                }
                else {
                    $azOpsMessage.TrackMetric($MetricName, $Metric)
                }
            }
            if ($level -eq "Critical" -or $level -eq "Error" -or $level -eq "Warning") {
                # Create TrackException
                $azOpsMessage.TrackException($logMessage.Message, $logProperties)
            }
            # Immediately emit log to ApplicationInsights
            $azOpsMessage.Flush()
        }
    }
}

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 {
        # Change PSSStyle output rendering to Host to remove escape sequences are removed in redirected or piped output.
        $PSStyle.OutputRendering = [System.Management.Automation.OutputRendering]::Host
        # Assert dependencies
        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
        }

        # Adjust MultipleTemplateParameterFileSuffix if incorrect MultipleTemplateParameterFileSuffix is set and log warning
        if (-not $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').StartsWith('.')) {
            $updateMultipleTemplateParameterFileSuffix = ".$(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix')"
            Write-AzOpsMessage -LogLevel Warning -LogString 'Initialize-AzOpsEnvironment.MultipleTemplateParameterFileSuffix.Adjustment' -LogStringValues (Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix'), $updateMultipleTemplateParameterFileSuffix
            Set-PSFConfig -Module AzOps -Name Core.MultipleTemplateParameterFileSuffix -Value $updateMultipleTemplateParameterFileSuffix
        }

        # Adjust ThrottleLimit from previously default 10 to 5 if system has less than 2 cores
        [int]$cpuCores = if ($IsWindows) { $env:NUMBER_OF_PROCESSORS } else { Invoke-AzOpsNativeCommand -ScriptBlock { nproc --all } -IgnoreExitcode }
        $throttleLimit = (Get-PSFConfig -Module AzOps -Name Core.ThrottleLimit).Value
        if (-not[string]::IsNullOrEmpty($cpuCores) -and $cpuCores -le 2 -and $throttleLimit -gt 5) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Initialize-AzOpsEnvironment.ThrottleLimit.Adjustment' -LogStringValues $throttleLimit, $cpuCores
            Set-PSFConfig -Module AzOps -Name Core.ThrottleLimit -Value 5
        }

        # Validate optional custom path for custom jq template
        if ((Get-PSFConfig -Module AzOps -Name Core.SkipCustomJqTemplate).Value) {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Initialize-AzOpsEnvironment.SkipCustomJqTemplate.True'
        }
        else {
            $customJqTemplatePath = (Get-PSFConfig -Module AzOps -Name Core.CustomJqTemplatePath).Value
            if (Test-Path -Path $customJqTemplatePath) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Initialize-AzOpsEnvironment.CustomJqTemplatePath' -LogStringValues $customJqTemplatePath
            }
            else {
                Write-AzOpsMessage -LogLevel Warning -LogString 'Initialize-AzOpsEnvironment.CustomJqTemplatePath.PathNotFound' -LogStringValues $customJqTemplatePath
                Set-PSFConfig -Module AzOps -Name Core.SkipCustomJqTemplate -Value $true
            }
        }
    }

    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-AzOpsMessage -LogLevel Important -LogString 'Initialize-AzOpsEnvironment.UsingCache'
            return
        }

        #region Initialize & Prepare
        Write-AzOpsMessage -LogLevel Important -LogString 'Initialize-AzOpsEnvironment.Processing'
        $currentAzContext = Get-AzContext
        $tenantId = $currentAzContext.Tenant.Id
        Write-AzOpsMessage -LogLevel Important -LogString '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 -ListAvailable
        $script:AzOpsAzManagementGroup = @()
        $script:AzOpsPartialRoot = @()
        #endregion Initialize & Prepare

        #region Management Group Processing
        try {
            $managementGroups = Get-AzManagementGroup -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Initialize-AzOpsEnvironment.ManagementGroup.NoManagementGroupAccess' -LogStringValues $_
            return
        }

        #region Validate root '/' permissions - different methods of getting current context depending on principalType
        try {
            $currentPrincipal = Get-AzOpsCurrentPrincipal -AzContext $currentAzContext -ErrorAction Stop
        }
        catch {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Initialize-AzOpsEnvironment.CurrentPrincipal.Fail' -LogStringValues $_
        }
        if ($currentPrincipal.id) {
            try {
                $rootPermissions = Get-AzRoleAssignment -ObjectId $currentPrincipal.id -Scope "/" -ErrorAction Stop
            }
            catch {
                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Initialize-AzOpsEnvironment.CurrentPrincipal.RoleAssignmentFail' -LogStringValues $_
            }
        }

        if (-not $rootPermissions) {
            Write-AzOpsMessage -LogLevel Important -LogString 'Initialize-AzOpsEnvironment.ManagementGroup.NoRootPermissions' -LogStringValues $currentAzContext.Account.Id
            $PartialMgDiscovery = $true
        }
        else {
            $PartialMgDiscovery = $false
        }
        #endregion Validate root '/' permissions

        #region Partial Discovery
        if ($PartialMgDiscoveryRoot) {
            Write-AzOpsMessage -LogLevel Important -LogString '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-AzOpsMessage -LogLevel Important -LogString 'Initialize-AzOpsEnvironment.ManagementGroup.Resolution' -LogStringValues $managementGroups.Count -Metric $managementGroups.Count -MetricName 'ManagementGroup Count'
        $tempResolved = foreach ($mgmtGroup in $managementGroups) {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Initialize-AzOpsEnvironment.ManagementGroup.Expanding' -LogStringValues $mgmtGroup.Name
            Get-AzOpsManagementGroup -ManagementGroup $mgmtGroup.Name -PartialDiscovery:$PartialMgDiscovery
        }
        $script:AzOpsAzManagementGroup = $tempResolved | Sort-Object -Property Id -Unique
        #endregion Management Group Resolution
        #endregion Management Group Processing
        Write-AzOpsMessage -LogLevel Important -LogString 'Initialize-AzOpsEnvironment.Processing.Completed'
        Clear-PSFMessage
    }

}

function Invoke-AzOpsPull {

    <#
        .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 IncludeResourcesInResourceGroup
            Discover only resources in these resource groups.
        .PARAMETER IncludeResourceType
            Discover only specific resource types.
        .PARAMETER SkipChildResource
            Skip childResource discovery.
        .PARAMETER SkipPim
            Skip discovery of Privileged Identity Management resources.
        .PARAMETER SkipLock
            Skip discovery of resource lock resources.
        .PARAMETER SkipPolicy
            Skip discovery of policies.
        .PARAMETER SkipRole
            Skip discovery of role.
        .PARAMETER SkipResourceGroup
            Skip discovery of resource groups
        .PARAMETER SkipResource
            Skip discovery of resources inside resource groups.
        .PARAMETER Rebuild
            Delete all AutoGeneratedTemplateFolderPath folders inside AzOpsState directory.
        .PARAMETER Force
            Delete $script:AzOpsState directory.
        .PARAMETER StatePath
            The root folder under which to write the resource json.
        .EXAMPLE
            > Invoke-AzOpsPull
            Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment.
    #>


    [CmdletBinding()]
    [Alias("Initialize-AzOpsRepository")]
    param (
        [string[]]
        $IncludeResourcesInResourceGroup = (Get-PSFConfigValue -FullName 'AzOps.Core.IncludeResourcesInResourceGroup'),

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

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

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

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

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

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

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

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

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

        [switch]
        $Rebuild,

        [switch]
        $Force,

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

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

    begin {
        #region Prepare
        if (-not $SkipPim) {
            try {
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Invoke-AzOpsPull.Validating.UserRole'
                $null = Get-AzADUser -First 1 -ErrorAction Stop
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Invoke-AzOpsPull.Validating.UserRole.Success'
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Invoke-AzOpsPull.Validating.AADP2'
                $servicePlanName = "AAD_PREMIUM_P2"
                $subscribedSkus = Invoke-AzRestMethod -Uri https://graph.microsoft.com/v1.0/subscribedSkus -ErrorAction Stop
                $subscribedSkusValue = $subscribedSkus.Content | ConvertFrom-Json -Depth 100 | Select-Object value
                if ($servicePlanName -in $subscribedSkusValue.value.servicePlans.servicePlanName) {
                    Write-AzOpsMessage -LogLevel Verbose -LogString 'Invoke-AzOpsPull.Validating.AADP2.Success'
                }
                else {
                    Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPull.Validating.AADP2.Failed'
                    $SkipPim = $true
                }
            }
            catch {
                Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPull.Validating.UserRole.Failed'
                $SkipPim = $true
            }
        }

        if ($false -eq $SkipChildResource -or $false -eq $SkipResource -and $true -eq $SkipResourceGroup) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPull.Validating.ResourceGroupDiscovery.Failed' -LogStringValues "`n"
        }

        $resourceTypeDiff = Compare-Object -ReferenceObject $SkipResourceType -DifferenceObject $IncludeResourceType -ExcludeDifferent
        if ($resourceTypeDiff) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPull.SkipResourceType.Failed' -LogStringValues $($resourceTypeDiff.InputObject)
            $IncludeResourceType = $IncludeResourceType | Where-Object { $_ -notin $resourceTypeDiff.InputObject }
        }

        Assert-AzOpsInitialization -Cmdlet $PSCmdlet -StatePath $StatePath

        $tenantId = (Get-AzContext).Tenant.Id
        Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPull.Tenant' -LogStringValues $tenantId
        Write-AzOpsMessage -LogLevel Verbose -LogString 'Invoke-AzOpsPull.TemplateParameterFileSuffix' -LogStringValues (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')

        Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPull.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-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPull.Migration.Required'
            }

            if ($Force -or $migrationRequired) {
                Invoke-PSFProtectedCommand -ActionString 'Invoke-AzOpsPull.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 'Invoke-AzOpsPull.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
        }

        # Parameters
        $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include IncludeResourcesInResourceGroup, IncludeResourceType, SkipPim, SkipLock, SkipPolicy, SkipRole, SkipResourceGroup, SkipChildResource, SkipResource, SkipResourceType, StatePath

        Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPull.Building.State' -LogStringValues $StatePath
        if ($rootScope -and $script:AzOpsAzManagementGroup) {
            foreach ($root in $rootScope) {
                # Create AzOpsState structure recursively
                Save-AzOpsManagementGroupChild -Scope $root -StatePath $StatePath
                Get-AzOpsResourceDefinition -Scope $root @parameters
            }
        }
        else {
            # If no management groups are found, iterate through each subscription
            foreach ($subscription in $script:AzOpsSubscriptions) {
                ConvertTo-AzOpsState -Resource (Get-AzSubscription -SubscriptionId $subscription.subscriptionId) -StatePath $StatePath
                Get-AzOpsResourceDefinition -Scope $subscription.id @parameters
            }
        }
        #endregion Root Scopes
    }

    end {
        $stopWatch.Stop()
        Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPull.Duration' -LogStringValues $stopWatch.Elapsed -Metric $stopWatch.Elapsed.TotalSeconds -MetricName 'AzOpsPull Time'
        Clear-PSFMessage
    }

}

function Invoke-AzOpsPush {

    <#
        .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 DeleteSetContents
            Set of content from the deleted files in ChangeSet.
        .PARAMETER StatePath
            The root path to where the entire state is being built in.
        .PARAMETER AzOpsMainTemplate
            Path to the main template used by AzOps
        .PARAMETER CustomSortOrder
            Switch to honor the input ordering for ChangeSet. If not used, ChangeSet will be sorted in ascending order.
        .EXAMPLE
            > Invoke-AzOpsPush -ChangeSet changeSet -StatePath $StatePath -AzOpsMainTemplate $templatePath
            Applies a change to Azure from the AzOps configuration.
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    [Alias("Invoke-AzOpsChange")]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string[]]
        $ChangeSet,

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

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

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

        [switch]
        $CustomSortOrder
    )

    begin {
        #region Utility Functions
        function New-AzOpsList {
            [CmdletBinding()]
            param (
                [string[]]
                $FileSet,
                [string]
                $FilePath,
                [string]
                $AzOpsMainTemplate,
                [string[]]
                $ConvertedTemplate,
                [string[]]
                $ConvertedParameter,
                [switch]
                $CompareDeploymentToDeletion
            )

            # Avoid adding files destined for deletion to a deployment list
            if ($CompareDeploymentToDeletion) {
                if ($FilePath -in $deleteSet -or $FilePath -in ($deleteSet | Resolve-Path).Path) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.DeployDeletionOverlap' -LogStringValues $FilePath
                    continue
                }
            }

            # Avoid duplicate entries in the deployment list
            if ($FilePath.EndsWith(".parameters.json")) {
                if ($FileSet -contains $FilePath.Replace(".parameters.json", ".json") -or $FileSet -contains $FilePath.Replace(".parameters.json", ".bicep")) {
                    continue
                }
            }
            if ($FilePath.EndsWith(".bicepparam")) {
                if ($FileSet -contains $FilePath.Replace(".bicepparam", ".bicep")) {
                    continue
                }
            }

            # Handle Bicep templates
            if ($FilePath.EndsWith(".bicep")) {
                $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $FilePath -ConvertedTemplate $ConvertedTemplate -ConvertedParameter $ConvertedParameter -CompareDeploymentToDeletion:$CompareDeploymentToDeletion
                if ($true -eq $transpiledTemplatePaths.transpiledTemplateNew) {
                    $ConvertedTemplate += $transpiledTemplatePaths.transpiledTemplatePath
                }
                if ($true -eq $transpiledTemplatePaths.transpiledParametersNew) {
                    $ConvertedParameter += $transpiledTemplatePaths.transpiledParametersPath
                }
                $FilePath = $transpiledTemplatePaths.transpiledTemplatePath
            }

            try {
                # Create scope object from the given file path
                $scopeObject = New-AzOpsScope -Path $FilePath -StatePath $StatePath -ErrorAction Stop
            }
            catch {
                # Log a warning message if creating the scope object fails
                Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPush.Scope.Failed' -LogStringValues $FilePath -Target $FilePath -ErrorRecord $_
                continue
            }

            # Resolve ARM file association
            $resolvedArmFileAssociation = Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $FilePath -AzOpsMainTemplate $AzOpsMainTemplate -ConvertedTemplate $ConvertedTemplate -ConvertedParameter $ConvertedParameter -CompareDeploymentToDeletion:$CompareDeploymentToDeletion
            if ($resolvedArmFileAssociation) {
                foreach ($fileAssociation in $resolvedArmFileAssociation) {
                    if ($true -eq $transpiledTemplatePaths.transpiledTemplateNew -and $fileAssociation.TemplateFilePath -eq $transpiledTemplatePaths.transpiledTemplatePath) {
                        $fileAssociation.TranspiledTemplateNew = $true
                    }
                    if ($true -eq $transpiledTemplatePaths.TranspiledParametersNew -and $fileAssociation.TemplateParameterFilePath -eq $transpiledTemplatePaths.transpiledParametersPath) {
                        $fileAssociation.TranspiledParametersNew = $true
                    }
                }
                return $resolvedArmFileAssociation
            }
        }
        function Resolve-ArmFileAssociation {
            [CmdletBinding()]
            param (
                [AzOpsScope]
                $ScopeObject,
                [string]
                $FilePath,
                [string]
                $AzOpsMainTemplate,
                [string[]]
                $ConvertedTemplate,
                [string[]]
                $ConvertedParameter,
                [switch]
                $CompareDeploymentToDeletion
            )

            #region Initialization Prep

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

            $fileItem = Get-Item -Path $FilePath
            if ($fileItem.Extension -notin '.json' , '.bicep', '.bicepparam') {
                Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPush.Resolve.NoJson' -LogStringValues $fileItem.FullName -Target $ScopeObject
                return
            }

            # Generate deterministic id for DefaultDeploymentRegion to overcome deployment issues when changing DefaultDeploymentRegion
            $deploymentRegionId = (Get-FileHash -Algorithm SHA256 -InputStream ([IO.MemoryStream]::new([byte[]][char[]](Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion')))).Hash.Substring(0, 4)
            #endregion Initialization Prep

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

                #region Directly Associated Template file exists
                switch ($fileItem.Name) {
                    { $_.EndsWith('.parameters.json') } {
                        if ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and $fileItem.FullName.Split('.')[-3] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','')) {
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.MultipleTemplateParameterFile' -LogStringValues $FilePath
                            $templatePath = $fileItem.FullName -replace "\.$($fileItem.FullName.Split('.')[-3])\.parameters\.json$", '.json'
                            $bicepTemplatePath = $fileItem.FullName -replace "\.$($fileItem.FullName.Split('.')[-3])\.parameters\.json$", '.bicep'
                        }
                        else {
                            $templatePath = $fileItem.FullName -replace '\.parameters\.json$', (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')
                            $bicepTemplatePath = $fileItem.FullName -replace '\.parameters\.json$', '.bicep'
                        }
                        if (Test-Path $templatePath) {
                            if ($CompareDeploymentToDeletion) {
                                # Avoid adding files destined for deletion to a deployment list
                                if ($templatePath -in $deleteSet -or $templatePath -in ($deleteSet | Resolve-Path).Path) {
                                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.DeployDeletionOverlap' -LogStringValues $templatePath
                                    return
                                }
                            }
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.FoundTemplate' -LogStringValues $FilePath, $templatePath
                            $result.TemplateFilePath = $templatePath
                            $newScopeObject = New-AzOpsScope -Path $result.TemplateFilePath -StatePath $StatePath -ErrorAction Stop
                            $result.ScopeObject = $newScopeObject
                            $result.Scope = $newScopeObject.Scope
                            return $result
                        }
                        elseif (Test-Path $bicepTemplatePath) {
                            if ($CompareDeploymentToDeletion) {
                                # Avoid adding files destined for deletion to a deployment list
                                if ($bicepTemplatePath -in $deleteSet -or $bicepTemplatePath -in ($deleteSet | Resolve-Path).Path) {
                                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.DeployDeletionOverlap' -LogStringValues $bicepTemplatePath
                                    return
                                }
                            }
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.FoundBicepTemplate' -LogStringValues $FilePath, $bicepTemplatePath
                            $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $bicepTemplatePath -SkipParam -ConvertedTemplate $ConvertedTemplate -CompareDeploymentToDeletion:$CompareDeploymentToDeletion
                            $result.TranspiledTemplateNew = $transpiledTemplatePaths.transpiledTemplateNew
                            $result.TemplateFilePath = $transpiledTemplatePaths.transpiledTemplatePath
                            $newScopeObject = New-AzOpsScope -Path $result.TemplateFilePath -StatePath $StatePath -ErrorAction Stop
                            $result.ScopeObject = $newScopeObject
                            $result.Scope = $newScopeObject.Scope
                            return $result
                        }
                    }
                    { $_.EndsWith('.bicepparam') } {
                        if ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and $fileItem.FullName.Split('.')[-2] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','')) {
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.MultipleTemplateParameterFile' -LogStringValues $FilePath
                            $bicepTemplatePath = $fileItem.FullName -replace "\.$($fileItem.FullName.Split('.')[-2])\.bicepparam$", '.bicep'
                        }
                        else {
                            $bicepTemplatePath = $fileItem.FullName -replace '\.bicepparam$', '.bicep'
                        }
                        if (Test-Path $bicepTemplatePath) {
                            if ($CompareDeploymentToDeletion) {
                                # Avoid adding files destined for deletion to a deployment list
                                if ($bicepTemplatePath -in $deleteSet -or $bicepTemplatePath -in ($deleteSet | Resolve-Path).Path) {
                                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.DeployDeletionOverlap' -LogStringValues $bicepTemplatePath
                                    return
                                }
                            }
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.FoundBicepTemplate' -LogStringValues $FilePath, $bicepTemplatePath
                            $transpiledTemplatePaths = ConvertFrom-AzOpsBicepTemplate -BicepTemplatePath $bicepTemplatePath -BicepParamTemplatePath $fileItem.FullName -ConvertedTemplate $ConvertedTemplate -ConvertedParameter $ConvertedParameter -CompareDeploymentToDeletion:$CompareDeploymentToDeletion
                            $result.TranspiledTemplateNew = $transpiledTemplatePaths.transpiledTemplateNew
                            $result.TranspiledParametersNew = $transpiledTemplatePaths.transpiledParametersNew
                            $result.TemplateFilePath = $transpiledTemplatePaths.transpiledTemplatePath
                            $result.TemplateParameterFilePath = $transpiledTemplatePaths.transpiledParametersPath
                            $newScopeObject = New-AzOpsScope -Path $result.TemplateFilePath -StatePath $StatePath -ErrorAction Stop
                            $result.ScopeObject = $newScopeObject
                            $result.Scope = $newScopeObject.Scope
                            return $result
                        }
                    }
                }
                #endregion Directly Associated Template file exists

                #region Check in the main template file for a match
                Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Resolve.NotFoundTemplate' -LogStringValues $FilePath, $templatePath
                $mainTemplateItem = Get-Item $AzOpsMainTemplate
                Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.FromMainTemplate' -LogStringValues $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 -ccontains "Type") {
                        # ManagementGroup and Subscription
                        $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.Type
                    }
                    elseif ($templateParameterFileHashtable.parameters.input.value.Keys -ccontains "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-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.MainTemplate.Supported' -LogStringValues $effectiveResourceType, $mainTemplateItem.FullName
                    $result.TemplateFilePath = $mainTemplateItem.FullName
                    return $result
                }
                Write-AzOpsMessage -LogLevel Warning -LogString 'Invoke-AzOpsPush.Resolve.MainTemplate.NotSupported' -LogStringValues $effectiveResourceType, $mainTemplateItem.FullName -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) {
                if ($CompareDeploymentToDeletion) {
                    # Avoid adding files destined for deletion to a deployment list
                    if ($parameterPath -in $deleteSet -or $parameterPath -in ($deleteSet | Resolve-Path).Path) {
                        Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.DeployDeletionOverlap' -LogStringValues $parameterPath
                        $skipParameters = $true
                    }
                }
                else {
                    $skipParameters = $false
                }
                if (-not $skipParameters) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.ParameterFound' -LogStringValues $FilePath, $parameterPath
                    $result.TemplateParameterFilePath = $parameterPath
                }
            }
            elseif ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and (Get-PSFConfigValue -FullName 'AzOps.Core.DeployAllMultipleTemplateParameterFiles') -eq $true) {
                # Check for multiple associated template parameter files
                $paramFileList = Get-ChildItem -Path $fileItem.Directory | Where-Object { ($_.Name.Split('.')[-3] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','')) -or ($_.Name.Split('.')[-2] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','')) }
                if ($paramFileList) {
                    $multiResult = @()
                    foreach ($paramFile in $paramFileList) {
                        # Check if the parameter file's name matches the expected pattern
                        $escapedBaseName = $fileItem.BaseName -replace '\.', '\.'
                        if ($paramFile.BaseName -match "^$escapedBaseName(\$(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix'))") {
                            if ($CompareDeploymentToDeletion) {
                                # Avoid adding files destined for deletion to a deployment list
                                if ($paramFile.VersionInfo.FileName -in $deleteSet -or $paramFile.VersionInfo.FileName -in ($deleteSet | Resolve-Path).Path) {
                                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.DeployDeletionOverlap' -LogStringValues $paramFile.VersionInfo.FileName
                                    continue
                                }
                            }
                            # Process parameter files for template equivalent
                            if (($fileItem.FullName.Split('.')[-2] -eq $paramFile.FullName.Split('.')[-3]) -or ($fileItem.FullName.Split('.')[-2] -eq $paramFile.FullName.Split('.')[-4])) {
                                Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.MultipleTemplateParameterFile' -LogStringValues $paramFile.FullName
                                $multiResult += Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $paramFile -AzOpsMainTemplate $AzOpsMainTemplate -ConvertedTemplate $ConvertedTemplate -ConvertedParameter $ConvertedParameter -CompareDeploymentToDeletion:$CompareDeploymentToDeletion
                            }
                        }
                    }
                    if ($multiResult) {
                        # Return completed object
                        return $multiResult
                    }
                    else {
                        Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.ParameterNotFound' -LogStringValues $FilePath, $parameterPath
                        if (-not (Test-TemplateDefaultParameter -FilePath $FilePath)) {
                            continue
                        }
                    }
                }
            }
            else {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.ParameterNotFound' -LogStringValues $FilePath, $parameterPath
                if ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true) {
                    if (-not (Test-TemplateDefaultParameter -FilePath $FilePath)) {
                        continue
                    }
                }
            }

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

            $result
            #endregion Case: Template File
        }
        function Test-TemplateDefaultParameter {
            param(
                [string]$FilePath
            )

            # Read the template file
            $defaultValueContent = Get-Content $FilePath
            # Check for parameters without a default value using jq
            $missingDefaultParam = $defaultValueContent | jq '.parameters | with_entries(select(.value.defaultValue == null))' | ConvertFrom-Json -AsHashtable
            if ($missingDefaultParam.Count -ge 1) {
                # Skip template deployment when template parameters without defaultValue are found and no parameter file identified
                $missingString = foreach ($item in $missingDefaultParam.Keys.GetEnumerator()) {"$item,"}
                # Log a debug message with the missing parameters
                Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Resolve.NotFoundParamFileDefaultValue' -LogStringValues $FilePath, ($missingString | Out-String -NoNewline)
                # Missing default value were found
                return $false
            } else {
                # Default values found
                return $true
            }
        }
        #endregion Utility Functions

        $WhatIfPreferenceState = $WhatIfPreference
        $WhatIfPreference = $false

        # Create array of strings to track bicep file conversion
        [string[]] $AzOpsTranspiledTemplate = @()
        [string[]] $AzOpsTranspiledParameter = @()

        # Remove lingering files from previous run
        $tempPath = [System.IO.Path]::GetTempPath()
        if ((Test-Path -Path ($tempPath + 'OUTPUT.md')) -or (Test-Path -Path ($tempPath + 'OUTPUT.json'))) {
            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Set-AzOpsWhatIfOutput.WhatIfFile.Remove'
            Remove-Item -Path ($tempPath + 'OUTPUT.md') -Force -ErrorAction SilentlyContinue
            Remove-Item -Path ($tempPath + 'OUTPUT.json') -Force -ErrorAction SilentlyContinue
        }
        $stopWatch = [System.Diagnostics.Stopwatch]::StartNew()
    }

    process {
        if (-not $ChangeSet) { return }
        Assert-AzOpsInitialization -Cmdlet $PSCmdlet -StatePath $StatePath
        #region Categorize Input
        Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.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') { $filename }
            elseif ($operation -match '^R[0-9][0-9][0-9]$') {
                $operation, $oldFileLocation, $newFileLocation = ($change -split "`t")[0, 1, 2]
                if (-not ((Split-Path -Path $oldFileLocation) -eq (Split-Path -Path $newFileLocation))) {
                    $deleteSet += $oldFileLocation
                }
                $newFileLocation
            }
        }
        if ($deleteSet -and -not $CustomSortOrder) { $deleteSet = $deleteSet | Sort-Object }
        if ($addModifySet -and -not $CustomSortOrder) { $addModifySet = $addModifySet | Sort-Object }

        if ($addModifySet) {
            Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Change.AddModify'
            foreach ($item in $addModifySet) {
                Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Change.AddModify.File'-LogStringValues $item
            }
        }
        if ($DeleteSetContents -and $deleteSet) {
            Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Change.Delete'
            # Count if $DeleteSetContents contains 1 or less
            if ($DeleteSetContents.Count -le 1) {
                # DeleteSetContents has no file content or is malformed
                Write-AzOpsMessage -LogLevel Error -LogString 'Invoke-AzOpsPush.Change.Delete.DeleteSetContents' -LogStringValues $DeleteSetContents
            }
            else {
                # Iterate through each line in $DeleteSetContents
                for ($i = 0; $i -lt $DeleteSetContents.Count; $i++) {
                    $line = $DeleteSetContents[$i].Trim()
                    # Check if the line starts with '-- ' and matches any filename in $deleteSet
                    if ($line -match '^-- (.+)$') {
                        $fileName = $matches[1]
                        if ($deleteSet -contains $fileName) {
                            Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Change.Delete.File' -LogStringValues $fileName
                            # Collect lines until the next line starting with '--'
                            $objectLines = @($line)
                            $i++
                            while ($i -lt $DeleteSetContents.Count) {
                                $currentLine = $DeleteSetContents[$i].Trim()
                                # Check if the line starts with '-- ' followed by any filename in $deleteSet
                                if ($currentLine -match '^-- (.+)$' -and $deleteSet -contains $matches[1]) {
                                    $i--
                                    Write-AzOpsMessage -LogLevel InternalComment -LogString 'Invoke-AzOpsPush.Change.Delete.NextTempFile' -LogStringValues $currentLine
                                    break  # Exit the loop if the line starts with '-- ' and matches a filename in $deleteSet
                                }
                                $objectLines += $currentLine
                                $i++
                            }
                            # When processed as designed there is no file present in the running branch.
                            # To run a removal AzOps re-creates the file and content based on $DeleteSetContents momentarily for processing, it is disregarded afterwards.
                            if (-not(Test-Path -Path (Split-Path -Path $fileName))) {
                                Write-AzOpsMessage -LogLevel InternalComment -LogString 'Invoke-AzOpsPush.Change.Delete.TempFile' -LogStringValues $fileName
                                New-Item -Path (Split-Path -Path $fileName) -ItemType Directory | Out-Null
                            }
                            # Create $fileName and set $content
                            $objectLines = $objectLines[1..$objectLines.Count]
                            $content = $objectLines.replace("-- $fileName", "") -join "`r`n"
                            Write-AzOpsMessage -LogLevel InternalComment -LogString 'Invoke-AzOpsPush.Change.Delete.SetTempFileContent' -LogStringValues $fileName, $content
                            Set-Content -Path $fileName -Value $content
                            $i--  # Move back one step to process the next line properly
                        }
                    }
                }
            }
        }
        #endregion Categorize Input

        #region Deploy State

        # 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-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Deploy.Subscription' -LogStringValues $addition -Target $addition
            $newStateDeploymentCmd.Process($addition)
        }
        foreach ($addition in $addModifySet) {
            if ($addition -notmatch '/*.providerfeatures.json$') { continue }
            Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Deploy.ProviderFeature' -LogStringValues $addition -Target $addition
            $newStateDeploymentCmd.Process($addition)
        }
        foreach ($addition in $addModifySet) {
            if ($addition -notmatch '/*.resourceproviders.json$') { continue }
            Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Deploy.ResourceProvider' -LogStringValues $addition -Target $addition
            $newStateDeploymentCmd.Process($addition)
        }
        $newStateDeploymentCmd.End()
        #endregion Deploy State

        #region Create DeploymentList
        $deploymentList = foreach ($addition in $addModifySet | Where-Object { $_ -match ((Get-Item $StatePath).Name) }) {
            # Create a list of deployment file associations using the New-AzOpsList function
            $deployFileAssociationList = New-AzOpsList -FilePath $addition -FileSet $addModifySet -AzOpsMainTemplate $AzOpsMainTemplate -ConvertedTemplate $AzOpsTranspiledTemplate -ConvertedParameter $AzOpsTranspiledParameter -CompareDeploymentToDeletion
            # Iterate through each file association in the list
            foreach ($fileAssociation in $deployFileAssociationList) {
                # Check if the transpiled template is new and add it to the collection if true
                if ($true -eq $fileAssociation.transpiledTemplateNew) {
                    $AzOpsTranspiledTemplate += $fileAssociation.TemplateFilePath
                }
                # Check if the transpiled parameters are new and add them to the collection if true
                if ($true -eq $fileAssociation.transpiledParametersNew) {
                    $AzOpsTranspiledParameter += $fileAssociation.TemplateParameterFilePath
                }
            }
            # Output the list of file associations for the current addition
            $deployFileAssociationList
        }
        #endregion Create DeploymentList

        #region Create DeletionList
        $deletionList = foreach ($deletion in $deleteSet | Where-Object { $_ -match ((Get-Item $StatePath).Name) }) {
            # Create a list of deletion file associations using the New-AzOpsList function
            $deletionFileAssociationList = New-AzOpsList -FilePath $deletion -FileSet $deleteSet -AzOpsMainTemplate $AzOpsMainTemplate -ConvertedTemplate $AzOpsTranspiledTemplate -ConvertedParameter $AzOpsTranspiledParameter
            # Iterate through each file association in the list
            foreach ($fileAssociation in $deletionFileAssociationList) {
                # Check if the transpiled template is new and add it to the collection if true
                if ($true -eq $fileAssociation.transpiledTemplateNew) {
                    $AzOpsTranspiledTemplate += $fileAssociation.TemplateFilePath
                }
                # Check if the transpiled parameters are new and add them to the collection if true
                if ($true -eq $fileAssociation.transpiledParametersNew) {
                    $AzOpsTranspiledParameter += $fileAssociation.TemplateParameterFilePath
                }
            }
            # Output the list of file associations for the current deletion
            $deletionFileAssociationList
        }
        #endregion Create DeletionList

        #If addModifySet exists and no deploymentList has been generated at the same time as the StatePath root has additional directories and AllowMultipleTemplateParameterFiles is default false, exit with terminating error
        if (($addModifySet -and -not $deploymentList) -and (Get-ChildItem -Path $StatePath -Directory) -and ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $false)) {
            Write-AzOpsMessage -LogLevel Critical -LogString 'Invoke-AzOpsPush.DeploymentList.NotFound'
            throw
        }

        #Starting deployment
        $WhatIfPreference = $WhatIfPreferenceState
        $uniqueProperties = 'Scope', 'DeploymentName', 'TemplateFilePath', 'TemplateParameterFilePath'
        $uniqueDeployment = $deploymentList | Select-Object $uniqueProperties -Unique | ForEach-Object {
            $TemplateFileContent = [System.IO.File]::ReadAllText($_.TemplateFilePath)
            $TemplateObject = ConvertFrom-Json $TemplateFileContent -AsHashtable
            $_ | Add-Member -MemberType NoteProperty -Name 'TemplateObject' -Value $TemplateObject -PassThru
        }
        $deploymentResult = @()

        if ($uniqueDeployment) {
            #Determine what deployment pattern to adopt serial or parallel
            if ((Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and (Get-PSFConfigValue -FullName 'AzOps.Core.ParallelDeployMultipleTemplateParameterFiles') -eq $true) {
                Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.ParallelCondition'
                # Group deployments based on TemplateFilePath
                $groups = $uniqueDeployment | Group-Object -Property TemplateFilePath | Where-Object { $_.Count -ge '2' -and $_.Name -ne $(Get-Item $AzOpsMainTemplate).FullName }
                if ($groups) {
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.ParallelGroup'
                    $processedTargets = @()
                    # Process each deployment and evaluate serial or parallel deployment pattern
                    foreach ($deployment in $uniqueDeployment) {
                        if ($deployment.TemplateFilePath -in $groups.Name -and $deployment -notin $processedTargets) {
                            # Deployment part of group association for parallel processing, process entire group as parallel deployment
                            $targets = $($groups | Where-Object { $_.Name -eq $deployment.TemplateFilePath }).Group
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.Parallel' -LogStringValues $deployment.TemplateFilePath, $targets.Count
                            # Prepare Input Data for parallel processing
                            $runspaceData = @{
                                AzOpsPath                       = "$($script:ModuleRoot)\AzOps.psd1"
                                StatePath                       = $StatePath
                                WhatIfPreference                = $WhatIfPreference
                                runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup
                                runspace_AzOpsSubscriptions     = $script:AzOpsSubscriptions
                                runspace_AzOpsPartialRoot       = $script:AzOpsPartialRoot
                                runspace_AzOpsResourceProvider  = $script:AzOpsResourceProvider
                            }
                            # Pass deployment targets for parallel processing and output deployment result for later
                            $deploymentResult += $targets | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel {
                                $deployment = $_
                                $runspaceData = $using:runspaceData

                                Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1"
                                $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru

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

                                & $azOps {
                                    $deployment | New-AzOpsDeployment -WhatIf:$runspaceData.WhatIfPreference
                                }
                            } -UseNewRunspace
                            Clear-PSFMessage
                            # Add targets to processed list to avoid duplicate deployment
                            $processedTargets += $targets
                        }
                        elseif ($deployment -notin $processedTargets) {
                            # Deployment not part of group association for parallel processing, process this as serial deployment
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.Serial' -LogStringValues $deployment.Count
                            $deploymentResult += $deployment | New-AzOpsDeployment -WhatIf:$WhatIfPreference
                        }
                        else {
                            # Deployment already processed by group association from parallel processing, skip this duplicate deployment
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.Skip' -LogStringValues $deployment.TemplateFilePath, $deployment.TemplateParameterFilePath
                        }
                    }
                }
                else {
                    # No deployments with matching TemplateFilePath identified
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.Serial' -LogStringValues $deployment.Count
                    $deploymentResult += $uniqueDeployment | New-AzOpsDeployment -WhatIf:$WhatIfPreference
                }
            } else {
                # Perform serial deployment only
                Write-AzOpsMessage -LogLevel Debug -LogString 'Invoke-AzOpsPush.Deployment.Serial' -LogStringValues $uniqueDeployment.Count
                $deploymentResult += $uniqueDeployment | New-AzOpsDeployment -WhatIf:$WhatIfPreference
            }

            if ($deploymentResult) {
                # Output deploymentResult outside module
                $deploymentResult
                #Process deploymentResult and output result
                foreach ($result in $deploymentResult) {
                    Set-AzOpsWhatIfOutput -FilePath $result.filePath -ParameterFilePath $result.parameterFilePath -Results $result.results
                }
            }
        }

        if ($deletionList) {
            #Removal of Supported resourceTypes and Custom Templates
            $deletionList = Set-AzOpsRemoveOrder -DeletionList $deletionList -Index { $_.ScopeObject.Resource }
            $removalJob = $deletionList | Select-Object $uniqueProperties -Unique | Remove-AzOpsDeployment -WhatIf:$WhatIfPreference -DeleteSet (Resolve-Path -Path $deleteSet).Path
            if ($removalJob.ScopeObject.Scope.Count -gt 0) {
                Clear-PSFMessage
                # Identify failed removal attempts for potential retries
                $retry = $removalJob | Where-Object { $_.Status -eq 'failed' }
                # If there are retries, log and attempt them again
                if ($retry) {
                    Write-AzOpsMessage -LogLevel Verbose -LogString 'Invoke-AzOpsPush.Deletion.Retry' -LogStringValues $retry.Count
                    Start-Sleep -Seconds 30
                    # Reset the status of failed attempts and perform recursive removal
                    foreach ($try in $retry) { $try.Status = $null }
                    $removeActionRecursive = Remove-AzResourceRaw -InputObject $retry -Recursive
                    $removeActionFail = $removeActionRecursive | Where-Object { $_.Status -eq 'failed' }
                    # If removal fails, log and attempt to fetch the resource causing the failure
                    if ($removeActionFail) {
                        Start-Sleep -Seconds 90
                        $throwFail = $false
                        # Check each failed removal and attempt to get the associated resource
                        foreach ($fail in $removeActionFail) {
                            $resource = $null
                            $resource = Get-AzOpsResource -ScopeObject $fail.ScopeObject -ErrorAction SilentlyContinue
                            # If the resource is found, log the failure
                            if ($resource) {
                                $throwFail = $true
                                Write-AzOpsMessage -LogLevel Critical -LogString 'Invoke-AzOpsPush.Deletion.Failed' -LogStringValues $fail.ScopeObject.Scope, $fail.TemplateFilePath, $fail.TemplateParameterFilePath
                            }
                        }
                        # If any failures occurred, throw an exception
                        if ($throwFail) {
                            throw
                        }
                    }
                }
            }
            # If there are missing dependencies, log the error and throw an exception
            if ($removalJob.dependencyMissing -eq $true) {
                Write-AzOpsMessage -LogLevel Critical -LogString 'Invoke-AzOpsPush.Dependency.Missing'
                throw
            }
        }
        $stopWatch.Stop()
        Write-AzOpsMessage -LogLevel Important -LogString 'Invoke-AzOpsPush.Duration' -LogStringValues $stopWatch.Elapsed -Metric $stopWatch.Elapsed.TotalSeconds -MetricName 'AzOpsPush Time'
        Clear-PSFMessage
    }
}

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.ApplicationInsights -Value $false -Initialize -Validation bool -Description 'Global flag to turn on or off logging to Application Insight'
Set-PSFConfig -Module AzOps -Name Core.AutoGeneratedTemplateFolderPath -Value "." -Initialize -Validation string -Description 'Auto-Generated 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.CustomTemplateResourceDeletion -Value $false -Initialize -Validation bool -Description 'Global flag declaring on or off deletion of resources in custom template.'
Set-PSFConfig -Module AzOps -Name Core.DeletionSupportedResourceType -Value @('Microsoft.Authorization/locks', 'locks', 'Microsoft.Authorization/policyAssignments', 'policyAssignments', 'Microsoft.Authorization/policyDefinitions', 'policyDefinitions', 'Microsoft.Authorization/policyExemptions', 'policyExemptions', 'Microsoft.Authorization/policySetDefinitions', 'policySetDefinitions', 'Microsoft.Authorization/roleAssignments', 'roleAssignments', 'Microsoft.Resources/resourceGroups', 'microsoft.resources/subscriptions/resourcegroups', 'resourceGroups') -Initialize -Validation stringarray -Description 'Global flag declaring resource types supported for deletion by AzOps.'
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.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.CustomJqTemplatePath -Value (Join-Path $pwd -ChildPath ".customtemplates") -Initialize -Validation string -Description 'Optional custom path to search for custom jq template'
Set-PSFConfig -Module AzOps -Name Core.SkipCustomJqTemplate -Value $true -Initialize -Validation bool -Description 'Controls usage of CustomJqTemplatePath to search for custom 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 'Generate folder hierachy for specific Management Groups IDs'
Set-PSFConfig -Module AzOps -Name Core.IncludeResourcesInResourceGroup -Value @('*') -Initialize -Validation stringarray -Description 'Global flag to discover only resources in these resource groups.'
Set-PSFConfig -Module AzOps -Name Core.IncludeResourceType -Value @('*') -Initialize -Validation stringarray -Description 'Global flag to discover only specific resource types.'
Set-PSFConfig -Module AzOps -Name Core.SkipChildResource -Value $true -Initialize -Validation bool -Description 'Global flag to indicate whether child resources should be discovered or not. Requires SkipResourceGroup and SkipResource to be false.'
Set-PSFConfig -Module AzOps -Name Core.SkipPim -Value $true -Initialize -Validation bool -Description 'Global flag to control discovery of Privileged Identity Management resources.'
Set-PSFConfig -Module AzOps -Name Core.SkipLock -Value $true -Initialize -Validation bool -Description 'Global flag to control discovery of resource lock resources.'
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.SkipResourceType -Value @('Microsoft.VSOnline/plans', 'Microsoft.PowerPlatform/accounts', 'Microsoft.PowerPlatform/enterprisePolicies') -Initialize -Validation stringarray -Description 'Global flag to skip discovery of specific Resource types.'
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.SubscriptionsToIncludeChildResource -Value @('*') -Initialize -Validation stringarray -Description 'Requires SkipResourceGroup, SkipResource and SkipChildResource to be false. Subscription ID that matches the filter.'
Set-PSFConfig -Module AzOps -Name Core.SubscriptionsToIncludeResourceGroups -Value @('*') -Initialize -Validation stringarray -Description 'Requires SkipResourceGroup to be false. Subscription ID that matches the filter.'
Set-PSFConfig -Module AzOps -Name Core.TemplateParameterFileSuffix -Value '.json' -Initialize -Validation string -Description 'Parameter file suffix identifier'
Set-PSFConfig -Module AzOps -Name Core.AllowMultipleTemplateParameterFiles -Value $false -Initialize -Validation string -Description 'Global flag to control multiple parameter file behaviour'
Set-PSFConfig -Module AzOps -Name Core.DeployAllMultipleTemplateParameterFiles -Value $false -Initialize -Validation string -Description 'Global flag to control base template deployment behaviour with changes and un-changed multiple corresponding parameter files'
Set-PSFConfig -Module AzOps -Name Core.MultipleTemplateParameterFileSuffix -Value '.x' -Initialize -Validation string -Description 'Multiple parameter file suffix identifier'
Set-PSFConfig -Module AzOps -Name Core.ParallelDeployMultipleTemplateParameterFiles -Value $false -Initialize -Validation string -Description 'Global flag to control parallel deployment of MultipleTemplateParameterFiles behaviour'
Set-PSFConfig -Module AzOps -Name Core.ThrottleLimit -Value 5 -Initialize -Validation integer -Description 'Throttle limit used in Foreach-Object -Parallel for resource/subscription discovery'
Set-PSFConfig -Module AzOps -Name Core.WhatifExcludedChangeTypes -Value @('NoChange', 'Ignore') -Initialize -Validation stringarray -Description 'Exclude specific change types from WhatIf operations.'

<#
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 ([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