internal/functions/Get-AzOpsDeploymentStackSetting.ps1

function Get-AzOpsDeploymentStackSetting {

    <#
        .SYNOPSIS
            Identifies and resolves the deployment stack configuration for a given template file, ensuring proper handling of excluded files and stack settings.
        .DESCRIPTION
            Processes a specified template file path to determine its associated deployment stack configuration.
            It checks for metadata, file variants, and exclusion patterns to identify whether the file is part of a deployment stack.
            If a deployment stack is found, it retrieves and returns the stack's settings and template file path.
            The function also handles exclusions defined in the stack configuration and logs relevant messages for debugging and tracing purposes.
        .PARAMETER TemplateFilePath
            The file path of the template to be processed. This should point to a JSON or Bicep file that may be part of a deployment stack.
        .PARAMETER ParameterTemplateFilePath
            the file path of an optional parameter template file associated with the TemplateFilePath.
        .PARAMETER ScopeObject
            An optional object that specifies the deployment scope, such as ResourceGroup, Subscription, or ManagementGroup.
        .PARAMETER ReverseLookup
            Indicates whether the function should perform a reverse lookup to identify the associated template file(s) for a given deployment stack file.
            When specified, the function attempts to resolve and return the template file paths that are part of the deployment stack configuration.
        .EXAMPLE
            > $result = Get-AzOpsDeploymentStackSetting -TemplateFilePath "C:\Templates\example.bicep" -ScopeObject (New-AzOpsScope -Path C:\Templates\example.bicep)
            > $result

            DeploymentStackTemplateFilePath : C:\Templates\example.deploymentStacks.json
            DeploymentStackSettings : @{property1=value1; property2=value2}
            ReverseLookupTemplateFilePath :

        .EXAMPLE
            > $result = Get-AzOpsDeploymentStackSetting -TemplateFilePath "C:\Templates\.deploymentStacks.json" -ReverseLookup
            > $result

            DeploymentStackTemplateFilePath :
            DeploymentStackSettings :
            ReverseLookupTemplateFilePath : {C:\Templates\example1.bicep, C:\Templates\example2.json}
    #>


    #region Parameters
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline = $true)]
        [string]
        $TemplateFilePath,
        [Parameter(ValueFromPipeline = $true)]
        [string]
        $ParameterTemplateFilePath,
        [Parameter(ValueFromPipeline = $true)]
        [object]
        $ScopeObject,
        [Parameter(ValueFromPipeline = $true)]
        [switch]
        $ReverseLookup
    )
    #endregion

    begin {
        #region Helper Functions
        function Get-AzOpsDeploymentStackSettingReverseLookup {

            <#
                .SYNOPSIS
                    Performs a reverse lookup to identify associated template files for a deployment stack.
                .DESCRIPTION
                    This function checks if the provided template file is a root stack file and retrieves all associated
                    Bicep and JSON files in the same folder, excluding `.deploymentStacks.json` files.
                .PARAMETER TemplateFilePath
                    The path to the template file being processed.
                .PARAMETER result
                    A PSCustomObject to store the resolved file paths.
                .OUTPUTS
                    Updates the result object with the resolved file paths.
            #>


            param (
                [string]
                $TemplateFilePath,
                [PSCustomObject]
                $result
            )

            # Check if the file is a root stack file by matching its name
            if ((Split-Path -Path $TemplateFilePath -Leaf) -eq '.deploymentStacks.json') {
                # This is a root stack file
                $folderPath = Split-Path -Path $TemplateFilePath -Parent
                $folderPathLookup = Join-Path -Path $folderPath -ChildPath '*'

                # Retrieve all Bicep and JSON files in the folder
                $allTemplateFiles = Get-ChildItem -Path $folderPathLookup -File -Include *.bicep, *.json -Exclude *.deploymentStacks.json | Select-Object -ExpandProperty FullName
                $nonAzOpsFiles = @()

                foreach ($file in $allTemplateFiles) {
                    if ($file.EndsWith('.json')) {
                        # Check if the JSON file has AzOps metadata
                        $fileContent = Get-Content -Path $file | ConvertFrom-Json -AsHashtable
                        if ($fileContent.metadata._generator.name -eq "AzOps" -or ($fileContent.Keys -contains "`$schema" -and $fileContent.parameters.input.value)) {
                            Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.DeploymentStack.Metadata.AzOps' -LogStringValues $file
                        }
                        else {
                            $nonAzOpsFiles += $file
                        }
                    }
                    else {
                        $nonAzOpsFiles += $file
                    }
                }

                # Update the result object with the resolved file paths
                $result.ReverseLookupTemplateFilePath = $nonAzOpsFiles
                Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.ReverseLookup.TemplateFilePath' -LogStringValues $TemplateFilePath, $result.ReverseLookupTemplateFilePath
                return $result
            }
            elseif ($TemplateFilePath.EndsWith('.deploymentStacks.json') -and (Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and $TemplateFilePath.Split('.')[-3] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','')) {
                # Handle parameter template stack files if AllowMultipleTemplateParameterFiles is true
                if (Test-Path -Path ($TemplateFilePath -replace '\.deploymentStacks.json$', '.bicepparam')) {
                    $result.ReverseLookupTemplateFilePath = $TemplateFilePath -replace '\.deploymentStacks.json$', '.bicepparam'
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.ReverseLookup.TemplateFilePath' -LogStringValues $TemplateFilePath, $result.ReverseLookupTemplateFilePath
                    return $result
                }
                elseif (Test-Path -Path ($TemplateFilePath -replace '\.deploymentStacks.json$', '.parameters.json')) {
                    $result.ReverseLookupTemplateFilePath = $TemplateFilePath -replace '\.deploymentStacks.json$', '.parameters.json'
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.ReverseLookup.TemplateFilePath' -LogStringValues $TemplateFilePath, $result.ReverseLookupTemplateFilePath
                    return $result
                }
            }
            else {
                # Handle dedicated template stack files
                if (Test-Path ($TemplateFilePath -replace '\.deploymentStacks.json$', '.bicep')) {
                    $result.ReverseLookupTemplateFilePath = $TemplateFilePath -replace '\.deploymentStacks.json$', '.bicep'
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.ReverseLookup.TemplateFilePath' -LogStringValues $TemplateFilePath, $result.ReverseLookupTemplateFilePath
                    return $result
                }
                if (Test-Path ($TemplateFilePath -replace '\.json$', '.json')) {
                    $result.ReverseLookupTemplateFilePath = $TemplateFilePath -replace '\.json$', '.json'
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.ReverseLookup.TemplateFilePath' -LogStringValues $TemplateFilePath, $result.ReverseLookupTemplateFilePath
                    return $result
                }
            }
        }
        function Get-AzOpsDeploymentStackFile {

            <#
                .SYNOPSIS
                    Resolves the deployment stack configuration for a given template file by identifying and processing the associated stack file.
                .DESCRIPTION
                    The `Get-AzOpsDeploymentStackFile` function evaluates a specified template file and its associated stack file to determine the deployment stack configuration.
                    It checks for the existence of the stack file, parses its content, and filters valid parameters based on the deployment scope.
                    The function also handles exclusions defined in the stack file, ensuring that excluded files are not processed as part of the deployment stack.
                    If the stack file is found and valid, the function returns the stack's settings and template file path.
                .PARAMETER StackPath
                    The file path of the deployment stack file to be evaluated. This file typically contains configuration settings for the deployment stack.
                .PARAMETER TemplateFilePath
                    The file path of the template file being processed. This should point to a JSON or Bicep file that may be part of a deployment stack.
                .PARAMETER ParameterTemplateFilePath
                    The file path of an optional parameter template file associated with the TemplateFilePath. This is used when multiple template parameter files are allowed.
                .PARAMETER FileVariants
                    A switch parameter indicating whether to check for file variants (e.g., `.bicep` and `.json` versions of the template file) when evaluating exclusions.
                .PARAMETER result
                    A PSCustomObject used to store the resolved deployment stack settings and template file path. This object is updated and returned by the function.
                .PARAMETER ScopeObject
                    An object specifying the deployment scope, such as ResourceGroup, Subscription, or ManagementGroup. This determines the type of deployment stack command to use.
                .OUTPUTS
                    PSCustomObject
                        Returns a custom object containing the following properties:
                        - DeploymentStackTemplateFilePath: The file path of the resolved deployment stack file.
                        - DeploymentStackSettings: A hashtable of filtered parameters from the stack file.
                        - ReverseLookupTemplateFilePath: Null (not used in this function).
            #>


            param (
                [string]
                $StackPath,
                [Parameter(Mandatory=$true, ValueFromPipeline = $true)]
                [string]
                $TemplateFilePath,
                [string]
                $ParameterTemplateFilePath,
                [switch]
                $FileVariants,
                [PSCustomObject]
                $result,
                [object]
                $ScopeObject
            )
            if ($StackPath) {
                # Check if the stack file exists
                if (Test-Path $StackPath) {
                    try {
                        # Read and parse the JSON content from the stack file
                        $stackContent = Get-Content -Path $StackPath -Raw | ConvertFrom-Json -AsHashtable
                    }
                    catch {
                        # Handle errors during JSON conversion or other operations
                        Write-AzOpsMessage -LogLevel Error -LogString 'Get-AzOpsDeploymentStackSetting.Setting.Error' -LogStringValues $StackPath, $TemplateFilePath
                        $result.DeploymentStackTemplateFilePath = $StackPath
                        $result.DeploymentStackSettings = $null
                        return $result
                    }
                    if ($ScopeObject.ResourceGroup -and $ScopeObject.ResourceGroup -ne "") {
                        $command = "New-AzResourceGroupDeploymentStack"
                    }
                    elseif ($ScopeObject.Subscription -and $ScopeObject.Subscription -ne "") {
                        $command = "New-AzSubscriptionDeploymentStack"
                    }
                    elseif ($ScopeObject.ManagementGroup -and $ScopeObject.ManagementGroup -ne "") {
                        $command = "New-AzManagementGroupDeploymentStack"
                    }
                    else {
                        Write-AzOpsMessage -LogLevel Error -LogString 'Get-AzOpsDeploymentStackSetting.Scope.Error' -LogStringValues $StackPath, $TemplateFilePath
                        return $result
                    }
                    $allowedSettings = @(
                        "ActionOnUnmanage",
                        "DenySettingsMode",
                        "DenySettingsExcludedPrincipal",
                        "DenySettingsExcludedAction",
                        "DenySettingsApplyToChildScopes",
                        "BypassStackOutOfSyncError",
                        "Location"
                    )
                    # Get the valid parameters for the command
                    $validParameters = (Get-Command $command).Parameters.Keys | Where-Object { $_ -in $allowedSettings }

                    # Initialize an empty hashtable to store the filtered parameters
                    $finalParameters = @{}

                    # Iterate over the keys in the stack content
                    foreach ($key in $stackContent.Keys) {
                        # Check if the key is a valid parameter
                        if ($validParameters -contains $key) {
                            # Retrieve the parameter metadata
                            $parameterMetadata = (Get-Command $command).Parameters[$key]
                            # Check if the parameter type is an enum
                            $allowedValues = @()
                            if ($parameterMetadata.ParameterType.IsEnum) {
                                $allowedValues = $parameterMetadata.ParameterType.GetEnumNames()
                            }
                            # Validate the value from $stackContent
                            if ($allowedValues.Count -gt 0 -and -not ($allowedValues -contains $stackContent[$key])) {
                                Write-AzOpsMessage -LogLevel Error -LogString 'Get-AzOpsDeploymentStackSetting.Parameter.Error' -LogStringValues $stackContent[$key], $key, $StackPath, $command
                                throw
                            }
                            # Add the key-value pair to the prepared parameters
                            $finalParameters[$key] = $stackContent[$key]
                        }
                    }
                    # Handle excluded files
                    if ($stackContent.excludedAzOpsFiles -and ($stackContent.excludedAzOpsFiles).Count -gt 0 -and $FileVariants) {
                        # Generate a list of potential file names to check
                        $fileName = Split-Path -Path $TemplateFilePath -Leaf
                        $checkFileVariants = @($fileName)
                        if ($fileName -like '*.json') {
                            $checkFileVariants += $fileName -replace '\.json$', '.bicep'
                        }
                        elseif ($fileName -like '*.bicep') {
                            $checkFileVariants += $fileName -replace '\.bicep$', '.json'
                        }
                        # Check if the parameter template file ends with 'parameters.json', if multiple template parameter files are allowed, and the file name matches the configured suffix.
                        if ($ParameterTemplateFilePath.EndsWith('parameters.json') -and (Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and $ParameterTemplateFilePath.Split('.')[-3] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','') ) {
                            # Extract the parameter file name and add it to the list of file variants
                            $parameterFileName = Split-Path -Path $ParameterTemplateFilePath -Leaf
                            $checkFileVariants += $parameterFileName
                            $checkFileVariants += $parameterFileName -replace '\.parameters.json$', '.bicepparam'
                        }
                        $matchedFile = $checkFileVariants | Where-Object { $stackContent.excludedAzOpsFiles -eq $_ }
                        if ($matchedFile) {
                            # Log the exclusion
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.ExcludedFromDeploymentStack' -LogStringValues $TemplateFilePath, $StackPath, $matchedFile
                        }
                        else {
                            # Update the result object if the file is not excluded
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.DeploymentStackTemplateFilePath' -LogStringValues $StackPath, $TemplateFilePath
                            $result.DeploymentStackTemplateFilePath = $StackPath
                            $result.DeploymentStackSettings = $finalParameters
                        }
                    }
                    else {
                        # Update the result object if there are no excluded files
                        Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.DeploymentStackTemplateFilePath' -LogStringValues $StackPath, $TemplateFilePath
                        $result.DeploymentStackTemplateFilePath = $StackPath
                        $result.DeploymentStackSettings = $finalParameters
                    }
                    return $result
                }
                else {
                    # Log if the stack file does not exist
                    Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.NoDeploymentStackFound' -LogStringValues $StackPath
                    return $result
                }
            }
            else {
                if ($ParameterTemplateFilePath.EndsWith('parameters.json') -and (Get-PSFConfigValue -FullName 'AzOps.Core.AllowMultipleTemplateParameterFiles') -eq $true -and $ParameterTemplateFilePath.Split('.')[-3] -match $(Get-PSFConfigValue -FullName 'AzOps.Core.MultipleTemplateParameterFileSuffix').Replace('.','') -and (Test-Path -Path ($ParameterTemplateFilePath -replace '\.parameters.json$', '.deploymentStacks.json'))) {
                    $stackParameterTemplatePath = $ParameterTemplateFilePath -replace '\.parameters.json$', '.deploymentStacks.json'
                    $evaluateStackTemplatePath = Get-AzOpsDeploymentStackFile -StackPath $stackParameterTemplatePath -TemplateFilePath $TemplateFilePath -ParameterTemplateFilePath $ParameterTemplateFilePath -result $result -ScopeObject $ScopeObject
                    if ($evaluateStackTemplatePath.DeploymentStackTemplateFilePath) {
                        $result = $evaluateStackTemplatePath
                        return $result
                    }
                }
                else {
                    $stackTemplatePath = ($TemplateFilePath -replace '\.json$', '.deploymentStacks.json')
                    $evaluateStackTemplatePath = Get-AzOpsDeploymentStackFile -StackPath $stackTemplatePath -TemplateFilePath $TemplateFilePath -ParameterTemplateFilePath $ParameterTemplateFilePath -FileVariants -result $result -ScopeObject $ScopeObject
                    if ($evaluateStackTemplatePath.DeploymentStackTemplateFilePath) {
                        $result = $evaluateStackTemplatePath
                        return $result
                    }
                    else {
                        $parentStackPath = Join-Path -Path (Split-Path -Path $TemplateFilePath) -ChildPath ".deploymentStacks.json"
                        $evaluateParentStackPath = Get-AzOpsDeploymentStackFile -StackPath $parentStackPath -TemplateFilePath $TemplateFilePath -ParameterTemplateFilePath $ParameterTemplateFilePath -FileVariants -result $result -ScopeObject $ScopeObject
                        if ($evaluateParentStackPath.DeploymentStackTemplateFilePath) {
                            $result = $evaluateParentStackPath
                            return $result
                        }
                        else {
                            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.NoDeploymentStackFound' -LogStringValues $TemplateFilePath
                            return $result
                        }
                    }
                }
            }
        }
        #endregion
        # Initialize the result object with default null values
        $result = [PSCustomObject] @{
            DeploymentStackTemplateFilePath = $null
            DeploymentStackSettings         = $null
            ReverseLookupTemplateFilePath   = $null
        }
    }

    process {
        # Handle ReverseLookup Mode
        if ($ReverseLookup) {
            $validatedResult = Get-AzOpsDeploymentStackSettingReverseLookup -TemplateFilePath $TemplateFilePath -result $result
            if ($validatedResult) {
                $result = $validatedResult
                return $result
            }
        }
        if (-not $TemplateFilePath.EndsWith('.json') -or ($TemplateFilePath.EndsWith('parameters.json')) -or $TemplateFilePath.EndsWith('.deploymentStacks.json')) {
            Write-AzOpsMessage -LogLevel Warning -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.NoJson' -LogStringValues $TemplateFilePath
            return
        }
        try {
            # Process the template file to determine its deployment stack configuration
            $templateContent = Get-Content -Path $TemplateFilePath -Raw | ConvertFrom-Json -AsHashtable
            if ($templateContent.metadata._generator.name -eq "AzOps") {
                Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.DeploymentStack.Metadata.AzOps' -LogStringValues $TemplateFilePath
                return
            }
        }
        catch {
            Write-AzOpsMessage -LogLevel Verbose -LogString 'Get-AzOpsDeploymentStackSetting.Template.Error' -LogStringValues $TemplateFilePath
            return
        }
        # Process the call
        $evaluateStackTemplatePath = Get-AzOpsDeploymentStackFile -TemplateFilePath $TemplateFilePath -ParameterTemplateFilePath $ParameterTemplateFilePath -result $result -ScopeObject $ScopeObject
        if ($evaluateStackTemplatePath.DeploymentStackTemplateFilePath) {
            $result = $evaluateStackTemplatePath
            return $result
        }
        else {
            Write-AzOpsMessage -LogLevel Debug -LogString 'Get-AzOpsDeploymentStackSetting.Resolve.NoDeploymentStackFound' -LogStringValues $TemplateFilePath
            return $result
        }
    }
}