Blog Post

Azure PaaS Blog
35 MIN READ

Azure Managed Redis Deployment Automation

LuisFilipe's avatar
LuisFilipe
Icon for Microsoft rankMicrosoft
Aug 21, 2026

Create your Azure Managed Redis (AMR) caches recurrently. (v2.1 update)

Sometimes we may see some temporary capacity restrictions on some Azure Regions, while deploying Azure Managed Redis (AMR), and when that happen, the recommendation is usually retrying using a different Azure Region, a different AMR SKU, or retrying during off-peak hours or weekends.

Retrying the cache deployment on a different region without capacity restrictions allow us to create the cache immediately.
But our business needs, requirements or company policies may not allow us to use a different region, and in that case we may need to keep retrying the AMR cache creation at different times of the day or during next days, or even trying different AMR SKUs.
This PowerShell script was build to help on those scenarios.

If this script was useful for you, please share your comments at the end of this page.
Thank you.

 

Overview

This script will try to create one Azure Managed Redis recurrently at different times of the day, based on just one SKU or on the list of SKUs provided.
It will use different and random time intervals between each attempt, based on parameters provided. 
The script will remain in loop, trying to create an AMR cache with the details provided and from the SKU list, waiting a random time between each attempt. 
Deleting the failed resource is required before any other attempt to create another cache with same name. For that reason, if the deployment fails due to capacity restrictions, the script will delete the failed deployment before try to redeploy the same cache name again.  
The script will stop only after successful cache creation or due to any other exception not capacity related.

It supports configuration for:

  • Create caches with no Geo-Replication, new or existent Geo-Replication group
  • With or without High Availability (HA) 
  • Eviction Policy definition
  • Any supported clustering Policy
  • Max retries and and minimum and maximum time between retries (script will use a random time between both)

Requirements

This script was written with the minimum requirements, using only PowerShell v7.6.5 or above and ARM template.
It requires Az.Accounts and Az.Resources PowerShell modules that are usually already installed on most common PowerShell environments.

Updates

v2.1 Azure changed the way to fail in case of capacity restrictions and introduced a fast-fail mechanism to fail the deployment even before starting. This avoid wasting time waiting for the deployment to fail, and validate if the deployment is possible at the beginning. This script version 2.1 supports the new fast-fail mechanism.

How to run

This PowerShell script can run on any environment that supports PowerShell, and includes three different modes to run:

  • Using by default values: on this case the default values can be defined inside of script code. On this case just call script name:
 ./AMR_AutomatedCreation.ps1
  • Defining values on command line: on this case some (or all) script parameters can be provided on the command line, that will override the default ones on script code: 
./AMR_AutomatedCreation.ps1 -CacheName "MyAMRCache" -HighAvailability "Enabled"
  • Providing values interactively: if script parameters are not provided by default (inside script code) or on command line, it will be asked to the user interactively, initially after starting the script.

The priority to pass parameters to the script is:  Default -> Command Line -> Interactive

Script parameters

As described above, each script parameter can be defined by default on script code, on command line, or requested interactively.
The script will validate all values provided
and may request additional details interactively, if some parameter are missing or incorrect, before applying to ARM deployment.
Some rules to be aware:

  • any space on any parameter value will be removed
  • ResourceGroup need to be created previously. This is by design to avoid mistakes on creating expensive caches on random or unknown Resource Groups, by mistake
  • Location will be converted automatically to lowercase
  • Skus, HighAvailability, ClusteringPolicy, EvictionPolicy will be converted automatically to the correct casing
  • CacheName, ResouceGroup and GeoReplicationGroupNickname maintain the provided casing, but validate the provided values based on accepted conventions

Script parameters are:

  • CacheName: Cache Name to be created
  • Location: Desired location for the new cache
  • ResourceGroup: Resource group to deploy the new cache
  • Skus: Any valid AMR SKUs can be provided, in a string comma separated list - the script will do creation attempts based on the SKU list on same order, and will start again from beginning of the list after trying all SKUs provided. Only one SKU is also supported as list of SKUs.
  • HighAvailability: Possible values are: "Enabled" or "Disabled"
  • ClusteringPolicy: Possible values are: "OSSCluster", "EnterpriseCluster", "NoCluster"
  • EvictionPolicy: Possible values are: "NoEviction", "AllKeysLRU", "AllKeysLFU", "AllKeysRandom", "VolatileLRU", "VolatileLFU", "VolatileRandom", "VolatileTTL"
  • GeoReplicationEnabled: Possible value are $true, $false
  • GeoReplicationGroupNickname: If GeoReplicationEnabled is $true, GeoReplicationGroupNickname is required. It could be a new Geo-replication group, or an existing geo-replication group.
    If GeoReplicationGroupNickname is an existing geo-replication group, the two next parameters are mandatory. If two next parameters not provided, the script will assume GeoReplicationGroupNickname is a new geo-replication group.
    Next two parameters and needed to internally create the existent linked cache IDs to add the new cache to the existent Geo-Replication group.
  • ExistingLinkedCacheNames: this is a string comma separated list, containing the current cache names in the existent geo-replication group. Only cache names without region.
  • ExistingLinkedResourceGroupNames: The same but for Resource Groups. This is also a string comma separated list, containing the resource group names from each one of the current caches in the existent geo-replication group. 
    Note: Both lists above are required only if GeoReplicationEnabled is $true and you want to add the cache to an existing Geo-replication group. Leave both lists empty if you want to create a new Geo-Replication group. If provided, both lists needs to have the same number of elements (cache name and correspondent Resource group). If not in same number, both lists will be requested again interactively.
  • MaxRetries: maximum number of retries to try to create the cache. Any integer value higher that 0 is a valid value.
  • MinDelayMinutes: minimum delay time, in minutes, to wait between each cache creation attempt, between 15 to 1440 (24 hours)
  • MaxDelayMinutes: maximum delay time, in minutes, to wait between each cache creation attempt, equal or higher than MinDelayMinutes 
    The script will use a random time between MinDelayMinutes and MaxDelayMinutes between each attempt creation.

Samples

The below commands will try to create: 

  • AMR cache using default values defined on script code parameters:
./AMR_AutomatedCreation.ps1

 

  • AMR cache name MyAMRCache on Central US location, using all other default values defined on script code parameters:
./AMR_AutomatedCreation.ps1 -CacheName "MyAMRCache" -Location "centralus"

 

  • AMR cache name MyAMRCache on Central US location, under MyResourceGroup with High Availability (HA) disabled, OSSCluster clustering policy, AllKeysLRU eviction policy, and no Geo-Replication. It will loop on 15 attempts to create an Balanced_B0 cache on first attempt and an MemoryOptimized_M10 on second attempt. All next 13 attempts will follow the same order on Skus list. It will use a random time between the default MinDelayMinutes and MaxDelayMinutes times.
./AMR_AutomatedCreation.ps1 -CacheName "MyAMRCache" -Location "centralus" -ResourceGroup "MyResourceGroup" -Skus "Balanced_B0,MemoryOptimized_M10" -HighAvailability "Disabled" -ClusteringPolicy "OSSCluster" -EvictionPolicy "AllKeysLRU" -GeoReplicationEnabled $false -MaxRetries 15

 

  • AMR cache name MyAMRCache on Central US location, under MyResourceGroup with High Availability (HA) enabled, OSSCluster clustering policy, AllKeysLRU eviction policy, and in a new Geo-Replication group with alias "MyGeoGroup". As previous example, this will try to create an Balanced_B0 cache or an MemoryOptimized_M10 cache alternately on each attempt. It will used the default MinDelayMinutes and MaxDelayMinutes values to calculate the random times between attempts. Because Balanced_B0 does not support Geo-replication, if succeed on Balanced_B0 cache creation, it will be created without Geo-Replication and a waring will be shown initially. Instead, if succeed on MemoryOptimized_M10 cache creation, this one will be created on a new geo-replicated group.
./AMR_AutomatedCreation.ps1 -CacheName "MyAMRCache" -Location "centralus" -ResourceGroup "MyResourceGroup" -Skus "Balanced_B0,MemoryOptimized_M10" -HighAvailability "Enabled" -ClusteringPolicy "OSSCluster" -EvictionPolicy "AllKeysLRU" -GeoReplicationEnabled $true -MaxRetries 15 -GeoReplicationGroupNickname "MyGeoGroup"

 

  • This is a similar example, but on this case we want to force to create a Balanced_B5 only (Skus list only contain one SKU). On this example we assume we have already one existing Geo-Replication group "MyGeoGroup" with two already linked caches : MyCache1 on ResourcegroupCache1 and MyCache2 on ResourcegroupCache2. To add the new cache to that geo-replication group we need to provide the existent cache names and resource groups in the two lists (ExistentLinkedCacheNames and ExistentLinkedResourceGroupNames, with same number of elements on each one).
./AMR_AutomatedCreation.ps1 -CacheName "MyAMRCache" -Location "centralus" -ResourceGroup "MyResourceGroup" -Skus "Balanced_B0,MemoryOptimized_M10" -HighAvailability "Enabled" -ClusteringPolicy "OSSCluster" -EvictionPolicy "AllKeysLRU" -GeoReplicationEnabled $true -MaxRetries 15 -GeoReplicationGroupNickname "MyGeoGroup" -ExistentLinkedCacheNames "MyCache1,MyCache2" -ExistentLinkedResourceGroupNames "ResourcegroupCache1,ResourcegroupCache2"

Authentication

Authentication on Azure is needed to deploy the cache, and the user needs to have Owner, Contributor or Azure Managed Redis Contributor RBAC roles assigned at Subscription or Resource Group level to be able to create new AMR caches.
If the user who is running the script is not authenticated, the authentication will be required interactively. To avoid interactive authentication, the user can authenticate previously on a PowerShell console and the script will use that authentication context:

Calling the script without any previous authentication, the script will request user credentials and subscription interactively:

./AMR_AutomatedCreation.ps1

After initial authentication, the script will use Az Context and not need interactive authentication:

Connect-AzAccount
./AMR_AutomatedCreation.ps1

 

If we plan to have this script running during some hours or days, the recommendation is to use some VM and run the script in a PowerShell console. Authentication tokens are usually valid for around 1 hour, but Azure PowerShell uses the Microsoft Authentication Library (MSAL), which also stores a refresh token and automatically acquires new access tokens when needed. 
So, after the first authentication, there are no need for any other authentication on each run attempt, independently of how long the script will be active.
Just make sure the PowerShell session remain active.

The script can also be used on some other Azure automation and using Managed Identity. That is not implemented on this script but can be easily changed.

Logging

Additionally, to output messages on screen, a log file is generated by the script with same outputs, for future reference.
Output log file format is AMR-yyyyMMdd-HHmmss.log, located on same script location.

Sample logging:

**********************
PowerShell transcript start
Start time: 20260818182212
Username: MYCOMPANY\myusername
RunAs User: MYCOMPANY\myusername
Configuration Name: 
Machine: MY-VM-MACHINE (Microsoft Windows NT 10.0.26200.0)
Host Application: C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.5.0_x64__8wekyb3d8bbwe\pwsh.dll
Process ID: 30632
PSVersion: 7.6.5
PSEdition: Core
GitCommitId: 7.6.5
OS: Microsoft Windows 10.0.26200
Platform: Win32NT
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1, 6.0, 7.0
PSRemotingProtocolVersion: 2.4
SerializationVersion: 1.1.0.1
WSManStackVersion: 3.0
**********************
Transcript started, output file is .\AMR-20260818-182212.log
2026-08-18 18:22:12 - Connecting to Azure.
2026-08-18 18:22:12 - AzContext found - No need to authenticate interactively.
2026-08-18 18:22:17 - Using subscription 16xxxxbc-xxxx-xxxx-xxxx-5675xxxxceda.
2026-08-18 18:22:29 - Using 15 attempts to try to create cache
2026-08-18 18:22:29 - With random delay between 120 minutes to 300 minutes on each attempt
2026-08-18 18:22:31 - Geo-Replication Selected:
2026-08-18 18:22:31 -   Enabled: False

2026-08-18 18:22:31 - ==== Attempt 1 of 15 ============================
2026-08-18 18:22:31 - Cache: AMRCache1-B0
2026-08-18 18:22:31 - Resource Group: resource-group1 (existent)
2026-08-18 18:22:31 - Region: frencecentral
2026-08-18 18:22:31 - SKU: Balanced_B0
2026-08-18 18:22:31 - High Availability: Enabled
2026-08-18 18:22:31 - Clustering Policy: OSSCluster
2026-08-18 18:22:31 - Eviction Policy: NoEviction
2026-08-18 18:22:31 - Geo-Replication enabled: False
2026-08-18 18:22:31 - Geo-Replication Group Name:
2026-08-18 18:22:31 - Deployment name: amr-20260818182231-01-7400f588
2026-08-18 18:22:31 - Starting ARM deployment...
2026-08-18 18:22:32 - Deployment succeeded using SKU Balanced_B0.
**********************
PowerShell transcript end
End time: 20260818182232
**********************

Support

This script is out of Microsoft support scope. Microsoft or the Author don't offer any support for this script and any errors or unexpected results need to be investigated and debugged by the user. 

THIS POWERSHELL SCRIPT IS OUT OF MICROSOFT or AUTHOR SUPPORT.

Disclaimer

THIS POWERSHELL SCRIPT IS PROVIDED AS IT IS, WITHOUT ANY GUARANTEES THIS WILL WORK AS EXPECTED ON YOUR ENVIRONMET.
USE THE BELOW POWERSHELL SCRIPT AT YOUR OWN RISK AND RESPONSIBILITY.
FEEL FREE TO CHANGE THE SCRIPT AND DO YOUR TESTS BEFORE USING IT IN PRODUCTION.
MICROSOFT OR THE AUTHOR DON'T ASSUME ANY RESPONSABILITY FOR UNEXPECTED RESULTS.

BE AWARE THAT AMR SKUS CONFIGURED CAN BE EXPENSIVE.
SEE AMR PRICING PAGE TO SELECT THE BEST SKU OPTIONS TO YOU:
https://azure.microsoft.com/en-us/pricing/details/managed-redis/?msockid=3c5252159b416703295045b19aff66ce

The script

The script is provided below. Create a new PowerShell script file (ex: AMR_AutomatedCreation.ps1) on your desired local folder, and copy/paste the below content.

# ============================================================================
# AMR_AutomatedCreation - v2.1
# ============================================================================
# Azure Managed Redis automated creation, to deal with capacity restrictions.
# Try to create a AMR cache and identify capacity restriction in the selected region.
# It keep retrying to create the cache on the desired region, at different random times of the day or during next days.
# The retry only happens if the cache creation fails due to capacity/quota restrictions.
# The script will stop for any other failure reason.
#
# This script will try to create one Azure Managed Redis with the cache name and region provided on parameters inside the script.
# It will try to create the cache using the list of SKUs provided on the provided order, 
# at different and random time intervals defined on $MaxRetries, $MinDelayMinutes, $MaxDelayMinutes
# The script validates the SKUs provided and will request additional details for the cache creation after authentication.
#
# It supports configuration for:
#   Geo-Replication to new or existent geo-groups
#   High Availability (HA)
#   Eviction Policy
#   Clustering Policy 
#   Cache Name, Region, Resource Group, Max retries and random intervals hard coded on top of script parameters.
#
# Note: During a successeful depolyment, Azure Portal may show database status is Failed state while Redis database is not completely created.
#       Wait for the script sucesseful or failed output, to confirm on Azure Portal. 
# ============================================================================
# Change log:
# v2.0 - support for preflight validation and fast-fail before starting the deployment 
# v2.1 - Moving to Rest API instead of -Debug to simplify preflight validation
# ============================================================================
# WARNING:
# USE THE BELOW POWERSHELL SCRIPT AS AT YOUR OWN RISK AND RESPONSIBILITY.
# FEEL FREE TO CHANGE THE SCRIPT AND DO YOUR TESTS, BEFORE USING IT IN PRODUCTION.
# WE DON'T ASSUME ANY RESPONSABILITY FOR UNEXPECTED RESULTS.
# WE DONT OFFER ANY SUPPORT THIS SCRIPT AND ANY ERRORS NEEDS TO BE INVESTIGATED AND RESOLVED BY YOU OUT OF MICROSOFT SUPPORT.
# BE AWARE THAT AMR SKUS CONFIGURED CAN BE EXPENSIVE.
# SEE AMR PRICING PAGE TO SELECT THE BEST SKU OPTIONS TO YOU: 
# https://azure.microsoft.com/en-us/pricing/details/managed-redis/?msockid=3c5252159b416703295045b19aff66ce 
# ============================================================================
# AUTHENTICATION:
# Call Connect-AzAccount before running this script
# If not called, authentication will be requested interactively
# ========================================================
# Parameters:
# Set the desired CacheName, Location and ResourceGroup to create your cache
# Set MaxRetries to try to create the cache (>1)
# MinDelayMinutes and MaxDelayMinutes to wait between each retry (MaxDelayMinutes should be higher MinDelayMinutes)
# Set the desired SKU options (minimum at least 1 SKU required)
# DeletionTimeoutMinutes and DeletionCheckIntervalSeconds may need to be adjusted if MinDelayMinutes is defined too short
# Script dont validate this parameters - use it within valid values
#
# After starting the script, will request to:
#  login with user credenthials
#  select the desired subscription
#  High Availability desired (enabled/disabled)
#  Eviction Policy policy desired (select from a list)
#  ClusteringPolicy desired (OSS, Enterprise or no clustered) (select from a list)
#  Geo-Replication details desired (with/without geo-replication, and geo replication group name and database ID's to link)
# ============================================================================
# Usage - command line:
# After setting the correct script parameters and save ps1 file, just call the script file name
# ============================================================================
# Logging:
# Additionally to output screen, a log file is generated by the script with same outputs, for future reference.
# Output log file format is AMR-yyyyMMdd-HHmmss.log, on same script location.
# ============================================================================
# Requires PowerShell Modules:
#   Az.Accounts
#   Az.Resources
# ============================================================================
# Created by Luis Filipe 
# ============================================================================



# ============================================================================
# ============================================================================
# Configuration
# Additional settings will be requested later after starting the script:
# HighAvailability, EvictionPolicy, ClusteringPolicy, Geo-Replication details
# ============================================================================
param(
    # --------------------------------------------------------------------------------------------------------
# Required parameters can be set here, as default, or overrided by script command line paramenters
[string]$CacheName = "CacheName", # Required
[string]$Location = "CacheRegion",            # Required, with no spaces
[string]$ResourceGroup = "ResourceGroup",     # Required, Resource Group
[string[]]$Skus = @(                          # Required, SKU options to deploy. SKU names will be validated by the script
    "Balanced_B0",
    "Balanced_B3",
    "Balanced_B5",
    "MemoryOptimized_M10",
    "ComputeOptimized_X3"
    ),
# --------------------------------------------------------------------------------------------------------
# Below parameters are also required. can be set here as default, or overrided by script command line parameters
# If empty as default and not provided on script command line parameters, it will be interactively requested
[string]$HighAvailability = "Enabled",       # Valid values: "Enabled", "Disabled"
[string]$ClusteringPolicy = "OSSCluster",    # Valid values: "OSSCluster","EnterpriseCluster","NoCluster"
[string]$EvictionPolicy = "NoEviction",      # Valid values: "NoEviction","AllKeysLRU","AllKeysLFU","AllKeysRandom","VolatileLRU","VolatileLFU","VolatileRandom","VolatileTTL"
# Geo-Replication details:
# $ExistentLinkedCacheNames and $ExistentLinkedResourceGroupNames will be used to build the Existing Linked Database Ids for the GeoReplication Group Name 
[bool]$GeoReplicationEnabled = $false,                    # $true/$false
[string]$GeoReplicationGroupNickname = "",                # Geo Replication Group Name to include the cache (only if $GeoReplicationEnabled = $true)
[string[]]$ExistentLinkedCacheNames = "",                 # Cache names from each one of the existent linked caches in the Geo Replication Group Name provided (comma separated, empty for new Geo-group) 
[string[]]$ExistentLinkedResourceGroupNames = ""          # Resource Group names from each one of the existent linked cache names, on the same order (comma separated, empty for new Geo-group) 
# --------------------------------------------------------------------------------------------------------
)


$MaxRetries = 15         # default 15
$MinDelayMinutes = 120   # default 120 minutes (2 hours)
$MaxDelayMinutes = 300   # default 300 minutes (5 hours)

# Maximum time to wait for a failed AMR resource to be deleted.
# Do not change these values
$DeletionTimeoutMinutes = 20
$DeletionCheckIntervalSeconds = 15

# ============================================================================
# Output preferences
# Suppress progress, warning, and information messages from Az PowerShell.
# The script's Write-Host messages are still displayed.
# ============================================================================
$ProgressPreference = "SilentlyContinue"
$WarningPreference = "SilentlyContinue"
$InformationPreference = "SilentlyContinue"

# Prevent confirmation prompts.
$ConfirmPreference = "None"
# ============================================================================
# ============================================================================



# ============================================================================
# Transcript output to log file
# ============================================================================
Write-Host ""

Start-Transcript `
    -Path ".\AMR-$((Get-Date).ToString('yyyyMMdd-HHmmss')).log"


# ============================================================================
# Capacity-related error patterns
# ============================================================================

$CapacityErrorPatterns = @(
    "AllocationFailed"
    "SkuNotAvailable"
    "InsufficientCapacity"
    "insufficient capacity"
    "not enough capacity"
    "out of capacity"
    "capacity constraints"
    "capacity constraint"
    "capacity restriction"
    "capacity is currently unavailable"
	"not supported for your subscription"
    "Try an alternate SKU or region"
    "is not currently available in"
)


# ============================================================================
# Helper function: Write timestamped messages
# ============================================================================
function Write-ScriptMessage {
    param(
        [Parameter(Mandatory)]
        [string]$Message,
        [AllowNull()]
        [AllowEmptyString()]
        [string]$ForegroundColor = $null
    )

    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    if ([string]::IsNullOrWhiteSpace($ForegroundColor))
    {
        Write-Host "$Timestamp - $Message"
    }
    else
    {
		Write-Host "$Timestamp - $Message" -ForegroundColor $ForegroundColor
	}
}

# ============================================================================
# Helper function: Convert any object to searchable text
# ============================================================================
function ConvertTo-SearchableText {
    param(
        [AllowNull()]
        [object]$InputObject
    )

    if ($null -eq $InputObject)
    {
        return ""
    }

    if ($InputObject -is [string])
    {
        return $InputObject
    }

    try
    {
        return $InputObject |
            ConvertTo-Json `
                -Depth 100 `
                -Compress `
                -ErrorAction Stop
    }
    catch
    {
        return $InputObject | Out-String
    }
}

# ============================================================================
# Helper function: Identify capacity-related errors
# Check both the original PowerShell exceptions and ARM operation details.
#-------------------------------------------------------------------------------------------
# $ErrorText - contains the normal output objects returned by the cmdlet - ARM deployment operation status/errors
# ============================================================================
function Test-CapacityError {
    param(
        [AllowNull()]
        [string]$ErrorText
    )

    if ([string]::IsNullOrWhiteSpace($ErrorText))
    {
        return $false
    }

    foreach ($Pattern in $CapacityErrorPatterns)
    {
		# Check original PowerShell exceptions
        if (
            $ErrorText.IndexOf(
                $Pattern,
                [System.StringComparison]::OrdinalIgnoreCase
            ) -ge 0
        )
        {
            return $true
        }
    }

    return $false
}

# ============================================================================
# Validate Integer Positive Values
# ============================================================================
function Validate-RetryValues {
    param(
        [string]$ParameterName,
        [string]$Value,
        [int]$MinValue = 0,
        [int]$MaxValue = -1
    )

    do {
        if ([string]::IsNullOrWhiteSpace($Value)) {
            $Value = Read-Host $ParameterName
        }

        $ParsedValue = 0

        $IsValid =
            [int]::TryParse($Value, [ref]$ParsedValue) -and
            $ParsedValue -ge $MinValue -and
            ($MaxValue -eq -1 -or $ParsedValue -le $MaxValue)

        if (-not $IsValid) {
            if ($MaxValue -eq -1) {
                Write-Host "$ParameterName must be an integer greater than or equal to $MinValue." 
            }
            else {
                Write-Host "$ParameterName must be an integer between $MinValue and $MaxValue." 
            }

            $Value = $null
        }

    } while (-not $IsValid)

    return $ParsedValue
}

# ============================================================================
# Validate Cache name convention (case senitive - same case provided will be used)
# Validate if cache name already exists in the selected subscription, in any region and any Resource group
# If Cache Name not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate value provided by any way and throw and exception if value provided is not valid.
# ------------------------------------------------
# Accept only letters, numbers and hyphens. 
# The first and last characters must each be a letter or a number. 
# Consecutive hyphens are not allowed.
# Any space will be removed from the provided name, and casing will be maintained
# ============================================================================
function Validate-CacheName {
    param(
        [string]$CacheName
    )

    # make sure Cache Name is not empty and request it interactively, removing any space
    while ([string]::IsNullOrWhiteSpace($CacheName)) {
        $CacheName = (Read-Host "Enter Cache name").Trim() -replace '\s', ''
    }

    # remove any spaces again in case provided on parameter, before validation
    $CacheName = $CacheName.Trim() -replace '\s', ''

    # Validate Cache naming convention
    while ($CacheName -notmatch '^(?!.*--)[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$') 
    {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Invalid Cache Name '$CacheName'. Only letters, numbers, and single hyphens are allowed. The first and last character must be a letter or number."
    }

    # Checking if cache already exists in the selected subscription, in any region and any Resource group
    $ExistingCache = Get-AzResource `
        -ResourceType "Microsoft.Cache/redisEnterprise" `
        -ResourceName $CacheName `
        -ErrorAction SilentlyContinue

    if ($null -ne $ExistingCache) {
        throw "Cache '$CacheName' already exists in resource group '$($ExistingCache.ResourceGroupName)' and '$($ExistingCache.Location)', under subscription '$(($ExistingCache.ResourceId -split '/')[2])'."
    }

    return $CacheName
}

# ============================================================================
# Validate Location (case insenitive)
# If Location not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate value provided by any way and throw and exception if value provided is not valid.
# ARM template used is case sensitive. This function ensure the correct casing.
# Location will be converted to lowercase and any space will be removed.
# ============================================================================
function Validate-Location {
    param(
        [string]$Location
    )

    # make sure location is not empty and request it interactively
    while ([string]::IsNullOrWhiteSpace($Location)) {
        $Location = (Read-Host "Enter Azure Location for the cache").Trim().ToLower() -replace '\s', ''
    }

    # removes any space and convert to lowercase again in case provided on parameter, before validation
    $Location = $Location.Trim().ToLower() -replace '\s', ''

    # Get available locations, for validation
    $Locations = (
        Invoke-AzRestMethod `
            -Path "/subscriptions/$((Get-AzContext).Subscription.Id)/providers/Microsoft.Cache/skus?api-version=2025-04-01" `
            -Method GET
    ).Content | ConvertFrom-Json

    $SupportedRegions = $Locations.value |
    Select-Object -ExpandProperty locations |
    Sort-Object -Unique

    # Validate Region provided
    while ($Location -notin $SupportedRegions)  {
               
        if ($Location -notin $SupportedRegions) {
            #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
            #Write-Host $_.Exception.Message -ForegroundColor Yellow
            throw "Invalid Location '$Location' - Supported Locations are: $($SupportedRegions -join ', ')"
        }
    } 

    return $Location
}

# ============================================================================
# Validate if Resource Group provided exists (case insenitive)
# If Resource Group not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate value provided by any way and throw and exception if value provided is not valid.
# ------------------------------------------------
# Can only include alphanumeric, underscore, parentheses, hyphen, period (except at end), 
# and Unicode characters that match the allowed characters
# ARM template used is case sensitive. This function ensure the correct casing.
# Resource Group will be converted to lowercase and any space will be removed.
# ============================================================================
function Validate-ResourceGroup {
    param(
        [string]$ResourceGroup
    )

    # make sure location is not empty and request it interactively
    while ([string]::IsNullOrWhiteSpace($ResourceGroup)) {
        $ResourceGroup = (Read-Host "Enter Resource Group for the cache").Trim() -replace '\s', ''
    }

    # removes any space again in case provided on parameter, before validation
    $ResourceGroup = $ResourceGroup.Trim() -replace '\s', ''

    # Validate RG naming convention 
    while ($ResourceGroup -notmatch '^[\p{L}\p{N}_()\-\.]*[\p{L}\p{N}_()\-]$') 
    {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Invalid Resource Group Name '$ResourceGroup'. Only accept alphanumeric, underscore, parentheses, hyphen, period (except at end), and Unicode characters that match the allowed characters"
    }

    # Verify if resource group exists. By design, don't create if not exists, but stop the script with error
    try
    {
        $ResourceGroupObject = Get-AzResourceGroup `
            -Name $ResourceGroup `
            -ErrorAction Stop `
            -WarningAction SilentlyContinue `
            -InformationAction SilentlyContinue
    }
    catch
    {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Resource group '$ResourceGroup' could not be found or accessed."
    }

    # Because Get-AzResourceGroup is case insentitive, returns the existing RG in the existing casing, instead of the provided casing
    return $ResourceGroupObject.ResourceGroupName
}

# ============================================================================
# Validate AMR SKUs for the Cache
# At least on SKU is required, and same SKU can be provided more than one time.
# SKUs should be provided on script default parameter, or on script command line parameter comma separated format: 
#   Both "Balanced_B0,Balanced_B5" or "Balanced_B0","Balanced_B5" formats are supported
# If not provided script default value, or on script command line parameter, will be requested interactively
# ARM template used is case sensitive. This function ensure the correct casing.
# SKU will be converted to lowercase and any space will be removed.
# ============================================================================
function Validate-Skus {
    param(
        [string[]]$Skus
    )

    # removes any space in case provided on parameter, before validation
    $Skus = $Skus.Trim() -replace '\s', ''

    # to support both comma-separated input and arrays: -Skus "aa,bb" and -Skus "aa", "bb"
    $Skus = @(
        $Skus |
            ForEach-Object { $_ -split ',' } |
            ForEach-Object { $_.Trim() } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    )

    # Get valid AMR SKUs
    $result = Invoke-AzRestMethod `
        -Path "/subscriptions/$((Get-AzContext).Subscription.Id)/providers/Microsoft.Cache/skus?api-version=2025-04-01" `
        -Method GET

    $ValidSkus = ($result.Content | ConvertFrom-Json).value | Select-Object -ExpandProperty name | Sort-Object -Unique


    # build intercative list options if $Skus became empty or null
    if ([string]::IsNullOrEmpty($Skus)){
        $Skus = Select-MultipleOptions `
        -Prompt "Select one or more SKUs" `
        -Options $ValidSkus
    }

    # Validate SKUs provided
    $InvalidSkus = $Skus | Where-Object { $_ -notin $ValidSkus }

    # Check if have at least one invalid SKU
    if ($InvalidSkus.Count -gt 0) {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Invalid SKU(s) provided: $($InvalidSkus -join ', ') `n Valid SKUs are: $($ValidSkus -join ', ')"
    }

    # Exact casing is required by template. Using SKU casing from RestAPI instead of SKU case provided by parameter
    $NormalizedSkus = foreach ($Sku in $Skus)
    {
        $Sku = $Sku.Trim()

        if ([string]::IsNullOrWhiteSpace($Sku))
        {
            throw "The SKU list contains a null, empty, or whitespace value."
        }

        # Ensure valid casing if provided wronly as default value, or through script command line parameter
        # -eq is case-insensitive by default.
        # The matched value comes from Azure and preserves its exact casing.
        $MatchedSku = $ValidSkus |
            Where-Object { $_ -eq $Sku } |
            Select-Object -First 1

        if ($null -eq $MatchedSku)
        {
            #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
            #Write-Host $_.Exception.Message -ForegroundColor Yellow
            throw "Invalid SKU(s) provided: '$Sku'. `n Valid SKUs are: $($ValidSkus -join ', ')"
        }

        $MatchedSku
    }

    return $NormalizedSkus
}

# ============================================================================
# Validate High Availability - accepted values: "Enabled" or "Disabled" (case insenitive)
# If Resource Group not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate value provided by any way and throw and exception if value provided is not valid.
# Uses default value (Enabled) if the interactive selection is invalid
# ARM template used is case sensitive. This function ensure the correct casing.
# ============================================================================
function Validate-HighAvailability {
    param(
        [string]$HighAvailability
    )

    # Define High Availability options
    $HighAvailabilityOptions = @(
            "Enabled",
            "Disabled"
        )

    # Ensure valid casing if provided wrongly as default value, or through script command line parameter
    $HighAvailability = $HighAvailabilityOptions |
    Where-Object { $_ -eq $HighAvailability } |
    Select-Object -First 1

    # build intercative list options if $HighAvailability became empty or null
    if ([string]::IsNullOrEmpty($HighAvailability)){
        $HighAvailability = Select-Option `
        -Prompt "Select High Availability (HA)" `
        -Options $HighAvailabilityOptions
    }

    if ($HighAvailability -notin $HighAvailabilityOptions)  # Value can be invalid if passed on command line
    {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Invalid High Availability value '$HighAvailability'."
    }

    return $HighAvailability
}

# ============================================================================
# Validate Clustering Policy (OSSCluster, EnterpriseCluster, NoCluster)
# If Clustering Policy not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate value provided by any way and throw and exception if value provided is not valid.
# Uses default value (OSSCluster) if the interactive selection is invalid
# ARM template used is case sensitive. This function ensure the correct casing.
# ============================================================================
function Validate-ClusteringPolicy {
    param(
        [string]$ClusteringPolicy
    )

    $ClusteringPolicyOptions = @(
        "OSSCluster",
        "EnterpriseCluster",
        "NoCluster"
    )

    # removes any space again in case provided on parameter, before validation
    $ClusteringPolicy = $ClusteringPolicy.Trim() -replace '\s', ''

    # Ensure valid casing if provided wrongly as default value, or through script command line parameter
    $ClusteringPolicy = $ClusteringPolicyOptions |
    Where-Object { $_ -eq $ClusteringPolicy } |
    Select-Object -First 1

    # build intercative list options if $ClusteringPolicy became empty or null
    if ([string]::IsNullOrEmpty($ClusteringPolicy)){
        $ClusteringPolicy = Select-Option `
        -Prompt "Select Clustering Policy" `
        -Options $ClusteringPolicyOptions
    }

    if ($ClusteringPolicy -notin $ClusteringPolicyOptions)  # Value can be invalid if passed on command line
    {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Invalid Clustering Policy value '$ClusteringPolicy'."
    }

    return $ClusteringPolicy
}

# ============================================================================
# Validate Eviction Policy (NoEviction,AllKeysLRU,AllKeysLFU,AllKeysRandom,VolatileLRU,VolatileLFU,VolatileRandom,VolatileTTL)
# If Eviction Policy not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate value provided by any way and throw and exception if value provided is not valid.
# Uses default value (NoEviction) if the interactive selection is invalid
# ARM template used is case sensitive. This function ensure the correct casing.
# ============================================================================
function Validate-EvictionPolicy {
    param(
        [string]$EvictionPolicy
    )

    $EvictionPolicyOptions = @(
        "NoEviction",
        "AllKeysLRU",
        "AllKeysLFU",
        "AllKeysRandom",
        "VolatileLRU",
        "VolatileLFU",
        "VolatileRandom",
        "VolatileTTL"
    )

    # removes any space again in case provided on parameter, before validation
    $EvictionPolicy = $EvictionPolicy.Trim() -replace '\s', ''

    # Ensure valid casing if provided wronlgy as default value, or through script command line parameter
    $EvictionPolicy = $EvictionPolicyOptions |
    Where-Object { $_ -eq $EvictionPolicy } |
    Select-Object -First 1

    # build intercative list options if $EvictionPolicy became empty or null
    if ([string]::IsNullOrEmpty($EvictionPolicy)){
        $EvictionPolicy = Select-Option `
        -Prompt "Select Eviction Policy" `
        -Options $EvictionPolicyOptions
    }

    if ($EvictionPolicy -notin $EvictionPolicyOptions)  # Value can be invalid if passed on command line
    {
        #Write-Host $_.Exception.GetType().FullName -ForegroundColor Red
        #Write-Host $_.Exception.Message -ForegroundColor Yellow
        throw "Invalid Eviction Policy value '$EvictionPolicy'."
    }

    return $EvictionPolicy
}

# ============================================================================
# Validate Geo-Replication Configuration details provided
# and build a list with the ExistingLinkedDatabaseIds to be used by geo-replication configuration on ARM template
# If any Geo-Replication Configuration details not provided script default value, or on script command line parameter, will be requested interactively
# Then, validate values provided by any way and throw and exception if any values provided are not valid.
# Uses No Geo-Replication as default value (NoEviction) if the interactive selection is invalid
# -------------------------------------------------
# User provides Geo-Replication group name and two lists, comma separated, to used to build the $ExistingLinkedDatabaseIds:
#   $ExistentLinkedCacheNames - containing existent cache names on that Geo-Replication group (empty, if is a new Geo-Group)
#   $ExistentLinkedResourceGroupNames - containing RG names from existent cache names on that Geo-Replication group (empty, if is a new Geo-Group)
# ============================================================================
function Validate-GeoReplication {
    param(
        [string]$HighAvailability,
        [bool]$GeoReplicationEnabled,           
        [string]$GeoReplicationGroupNickname,
        [string[]]$ExistentLinkedCacheNames,  
        [string[]]$ExistentLinkedResourceGroupNames
    )

    # Get Geo-Replication details and validate selection 
    if ($HighAvailability -eq "Disabled") {
        Write-ScriptMessage -Message "No Geo-Replication details needed because is not supported on caches with High Availability (HA) disabled"
        
        return @{
            GeoReplicationEnabled = $false
            GeoReplicationGroupNickname = ""
            ExistingLinkedDatabaseIds = @()
        }
    } else {
        # Get Geo-Replication detils and validate selection 
        # User provides Geo-Replication group name and two lists, comma separated, to used to build the $ExistingLinkedDatabaseIds:
        #   $ExistentLinkedCacheNames - containing existent cache names on that Geo-Replication group (empty, if is a new Geo-Group)
        #   $ExistentLinkedResourceGroupNames - containing RG names from existent cache names on that Geo-Replication group (empty, if is a new Geo-Group)
        # Get-GeoReplicationConfiguration collect geo details interactively, if not povided by default or on command line script
        # and build a list with the ExistingLinkedDatabaseIds to be used by geo-replication configuration on ARM template
        $GeoConfig = Get-GeoReplicationConfiguration `
                        -GeoReplicationEnabled $GeoReplicationEnabled `
                        -GeoReplicationGroupNickname $GeoReplicationGroupNickname `
                        -ExistentLinkedCacheNames @($ExistentLinkedCacheNames) `
                        -ExistentLinkedResourceGroupNames @($ExistentLinkedResourceGroupNames)

        $GeoReplicationEnabled = $GeoConfig.GeoReplicationEnabled
        $GeoReplicationGroupNickname = $GeoConfig.GeoReplicationGroupNickname
        $ExistingLinkedDatabaseIds = $GeoConfig.ExistingLinkedDatabaseIds

        $newGroupMsg = $null
        if([string]::IsNullOrWhiteSpace($ExistingLinkedDatabaseIds)){ $newGroupMsg = "(New Group)" } 

        Write-ScriptMessage -Message "Geo-Replication Selected:"
        Write-ScriptMessage -Message "  Enabled: $GeoReplicationEnabled"
        Write-ScriptMessage -Message "  Geo-Replication Group Name: $GeoReplicationGroupNickname $newGroupMsg"
        if(-not [string]::IsNullOrWhiteSpace($ExistingLinkedDatabaseIds)){
                Write-ScriptMessage -Message "  Existing Linked Database Ids:"
                $ExistingLinkedDatabaseIds | ForEach-Object {
                    Write-Host "    $_"
            }
        }

        # GeoReplication messaging
        if (
            $GeoReplicationEnabled -and
            ($Skus -contains "Balanced_B0") -or
            ($Skus -contains "Balanced_B1")
        ) {
            Write-Host "  WARNING: Balanced_B0 or Balanced_B1 don't support Geo-Replication and will be created without Geo-Replication."
        }

        if ($GeoReplicationEnabled -and $HighAvailability -eq "Disabled")
        {
            Write-Host "  WARNING: Geo-Replication is not supported on caches with High Availability (HA) disabled. Cache will be created without Geo-Replication."
        }

        return @{
            GeoReplicationEnabled = $GeoReplicationEnabled
            GeoReplicationGroupNickname = $GeoReplicationGroupNickname
            ExistingLinkedDatabaseIds = $ExistingLinkedDatabaseIds
        }
    }
}

# ============================================================================
# Build Geo-Replication Linked Database Ids to be used on ARM template
# ============================================================================
function Get-GeoReplicationConfiguration {
    param(
        [bool]$GeoReplicationEnabled,           
        [string]$GeoReplicationGroupNickname,
        [string[]]$ExistentLinkedCacheNames,  
        [string[]]$ExistentLinkedResourceGroupNames
    )

    # No Geo-Replication
    # ------------------------------------------------------
    if (-not $GeoReplicationEnabled) {
            return @{
            GeoReplicationEnabled = $false
            GeoReplicationGroupNickname = ""
            ExistingLinkedDatabaseIds = @()
        }
    }

    # If $GeoReplicationEnabled and no Linked cached names and no linked RGs, means this is a new Group
    if([string]::IsNullOrWhiteSpace($ExistentLinkedCacheNames) -and [string]::IsNullOrWhiteSpace($ExistentLinkedResourceGroupNames)) {
        $GeoNewGroup = $true
    }

    # Geo-Replication for a New Geo-Group
    # ------------------------------------------------------
    if(([string]::IsNullOrWhiteSpace($GeoReplicationGroupNickname))) {
        write-host "Geo-Replication Group Name cannot be empty." -ForegroundColor Red
        $GeoReplicationGroupNickname = Read-Host "Geo-Replication Group name"
        while (-not (Verify-GeoGroupName $GeoReplicationGroupNickname)) {
            Write-Host "Invalid Geo-Replication Group name. Try again." -ForegroundColor Red
            $GeoReplicationGroupNickname = Read-Host "Geo-Replication Group name"
        }
    } 
    
    if ($GeoReplicationEnabled -and $GeoNewGroup) {
            return @{
            GeoReplicationEnabled = $true
            GeoReplicationGroupNickname = $GeoReplicationGroupNickname
            ExistingLinkedDatabaseIds = @()
        }
    }

    # Geo-Replication for an existing Geo-Group
    # ------------------------------------------------------
    while([string]::IsNullOrWhiteSpace($ExistentLinkedCacheNames) -or 
        [string]::IsNullOrWhiteSpace($ExistentLinkedResourceGroupNames) -or 
        ((CountElementsFromList -List $ExistentLinkedCacheNames) -ne (CountElementsFromList -List $ExistentLinkedResourceGroupNames))) {
            
        if([string]::IsNullOrWhiteSpace($ExistentLinkedCacheNames)) {
            write-host "List of current linked Cache Names cannot be empty." -ForegroundColor Red
            $ExistentLinkedCacheNames = Request-ListOfNames -Prompt "current Cache Names"
        } 
        
        if([string]::IsNullOrWhiteSpace($ExistentLinkedResourceGroupNames)) {
            write-host "List of Resource Groups from the each one of the current linked Cache Names cannot be empty." -ForegroundColor Red
            $ExistentLinkedResourceGroupNames = Request-ListOfNames -Prompt "Resource Groups from the current linked Cache Names"
        } 

        if((CountElementsFromList -List $ExistentLinkedCacheNames) -ne (CountElementsFromList -List $ExistentLinkedResourceGroupNames)) {
            write-host "List of existent linked Cache Names and List of the correspondent Resource Groups requires same number of elements." -ForegroundColor Red
            $ExistentLinkedCacheNames = Request-ListOfNames -Prompt "Cache Names"
            $ExistentLinkedResourceGroupNames = Request-ListOfNames -Prompt "Resource Groups"
        } 
    }

    [string[]]$ExistingLinkedDatabaseIds = @()

    # to support both comma-separated input and arrays: -Skus "aa,bb" and -Skus "aa", "bb"
    [string[]]$GeoCacheNames = @(
        $ExistentLinkedCacheNames.Split(',') |
        ForEach-Object { $_.Trim() } |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )
    [string[]]$GeoResourceGroups = @(
        $ExistentLinkedResourceGroupNames.Split(',') |
        ForEach-Object { $_.Trim() } |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        )


    for ($i = 0; $i -lt $GeoCacheNames.Count; $i++) {
        $ExistingLinkedDatabaseIds += "/subscriptions/$((Get-AzContext).Subscription.Id)/resourceGroups/$($GeoResourceGroups[$i])/providers/Microsoft.Cache/redisEnterprise/$($GeoCacheNames[$i])/databases/default"
    }

    return @{
        GeoReplicationEnabled = $true
        GeoReplicationGroupNickname = $GeoReplicationGroupNickname
        ExistingLinkedDatabaseIds = $ExistingLinkedDatabaseIds
    }
}

# ============================================================================
# Count elements from a comma separated list
# ============================================================================
function CountElementsFromList {
    param(
        [string[]]$List
    )

    if ([string]::IsNullOrWhiteSpace($List)) {
        return 0
    }

    return @(
        $List.Split(',') |
        ForEach-Object { $_.Trim() } |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    ).Count
}

# ============================================================================
# Request a Comma separated list of strings from user
# Mandatory at least one element
# ============================================================================
function Request-ListOfNames {
    param(      
        [string]$Prompt
    )

    $List = Read-Host "List of $Prompt (comma separated)"
    while ([string]::IsNullOrWhiteSpace($List) ) {   # $CacheName -notmatch '^(?!.*--)[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$'
        Write-Host "Invalid or empty list of $Prompt. Try again." -ForegroundColor Red
        $List = Read-Host "List of $Prompt (comma separated)"
    }

    return $List
}

# ============================================================================
# Request a Comma separated list of Cache Names and RGs to build ID to be used on Geo-replication
# NOT USED
# ============================================================================
function Request-GeoLinkedCacheNamesAndRGs {
    param(      
        [string]$GeoReplicationGroupNickname
    )

    [int]$CacheCount = 0

    do {
        $CacheCountText = Read-Host "How many caches currently belong to this existing Geo-replication group"
    }
    while (
        -not [int]::TryParse(
            $CacheCountText,
            [ref]$CacheCount
        ) -or [int]$CacheCount -lt 1   
    )

    # collect Exixtent linked cache names and RGs for the existent Geo-Group
    [string[]]$ListExistingCacheName = @()
    [string[]]$ListExistingRG = @()

    for ($i = 1; $i -le $CacheCount; $i++) {

        Write-Host "  Existing cache $i of $CacheCount in this $GeoReplicationGroupNickname Geo-Replication group:"

        do {
            $ExistingCacheName = Read-Host "    #$i Cache name"
        }
        while ([string]::IsNullOrWhiteSpace($ExistingCacheName))
        $ListExistingCacheName += $ExistingCacheName

        do {
            $ExistingCacheRG = Read-Host "    #$i Resource Group name from Cache $ExistingCacheName"
        }
        while ([string]::IsNullOrWhiteSpace($ExistingCacheRG))
        $ListExistingRG += $ExistingCacheRG
    }
    write-host "ListExistingCacheName: $ListExistingCacheName"
    write-host "ListExistingRG: $ListExistingRG"

    return @{
        ExistentLinkedCacheNames = $ListExistingCacheName
        ExistentLinkedResourceGroupNames = $ListExistingRG
    }
}

# ============================================================================
# Verify Geo-Replication Group Name
# It can only contain letters, numbers and hyphens. 
# The first and last characters must each be a letter or a number. 
# Consecutive hyphens and empty names are not allowed
# ============================================================================
function Verify-GeoGroupName {
    param([string]$Name)

    if ([string]::IsNullOrWhiteSpace($Name)) {
        return $false
    }

    return $Name -match '^(?!.*--)[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$'
}

# ============================================================================
# Helper function: Retrieve detailed ARM deployment errors
#
# This function first confirms that the deployment record exists.
# It only calls Get-AzResourceGroupDeploymentOperation if the deployment
# record was found, preventing the secondary 404 error.
# ============================================================================
function Get-DeploymentOperationError {
    param(
        [Parameter(Mandatory)]
        [string]$DeploymentName,

        [Parameter(Mandatory)]
        [string]$ResourceGroupName
    )

    $Result = [ordered]@{
        DeploymentFound = $false
        OperationsFound = $false
        CapacityDetected = $false
        FailedOperations = @()
        ErrorText = ""
    }

    # ------------------------------------------------------------------------
    # First check whether ARM created the deployment record.
    # ------------------------------------------------------------------------

    try
    {
        #-------------------------------------------------------------------------------------------
        # $Deployment - contains the normal output objects returned by the cmdlet - ARM deployment operation status/errors
        #-------------------------------------------------------------------------------------------
        $Deployment = Get-AzResourceGroupDeployment `
            -ResourceGroupName $ResourceGroupName `
            -Name $DeploymentName `
            -ErrorAction SilentlyContinue `
            -WarningAction SilentlyContinue `
            -InformationAction SilentlyContinue 

        if ($null -eq $Deployment)
        {
            $Result.ErrorText = "The ARM deployment record was not found."

            return [PSCustomObject]$Result
        }

        $Result.DeploymentFound = $true

        $DeploymentText = ConvertTo-SearchableText -InputObject $Deployment

        if (Test-CapacityError -ErrorText $DeploymentText)
        {
            $Result.CapacityDetected = $true
        }
    }
    catch
    {
        $DeploymentLookupError = $_ | Out-String

        $Result.ErrorText = @(
            "The ARM deployment record could not be retrieved."
            $DeploymentLookupError
        ) -join [Environment]::NewLine

        return [PSCustomObject]$Result
    }

    # ------------------------------------------------------------------------
    # Retrieve deployment operations only if the deployment record exists.
    # $Operations - contains the normal output objects returned by the cmdlet - ARM deployment operation status/errors
    # ------------------------------------------------------------------------
    try
    {
        $Operations = @(
            Get-AzResourceGroupDeploymentOperation `
                -ResourceGroupName $ResourceGroupName `
                -DeploymentName $DeploymentName `
                -ErrorAction SilentlyContinue `
                -WarningAction SilentlyContinue `
                -InformationAction SilentlyContinue
        )

        if ($Operations.Count -eq 0)
        {
            $Result.ErrorText = "The deployment exists, but no deployment operations were returned."

            return [PSCustomObject]$Result
        }

        $Result.OperationsFound = $true

        foreach ($Operation in $Operations)
        {
            $OperationText = ConvertTo-SearchableText -InputObject $Operation

            if (Test-CapacityError -ErrorText $OperationText)
            {
                $Result.CapacityDetected = $true
            }

            $ProvisioningState = $Operation.ProvisioningState

            if (
                [string]::IsNullOrWhiteSpace($ProvisioningState) -and
                $null -ne $Operation.Properties
            )
            {
                $ProvisioningState = $Operation.Properties.ProvisioningState
            }

            if (
                $ProvisioningState -eq "Failed" -or
                $ProvisioningState -eq "Canceled" -or
                $ProvisioningState -eq "Cancelled"
            )
            {
                $Result.FailedOperations += $Operation
            }
        }

        if ($Result.FailedOperations.Count -gt 0)
        {
            $FailedOperationText = foreach (
                $FailedOperation in $Result.FailedOperations
            )
            {
                ConvertTo-SearchableText -InputObject $FailedOperation
            }

            $Result.ErrorText = $FailedOperationText -join `
                [Environment]::NewLine
        }
        else
        {
            $AllOperationText = foreach ($Operation in $Operations)
            {
                ConvertTo-SearchableText -InputObject $Operation
            }

            $Result.ErrorText = $AllOperationText -join `
                [Environment]::NewLine
        }
    }
    catch
    {
        $OperationLookupError = $_ | Out-String

        $Result.ErrorText = @(
            "The deployment exists, but its operations could not be retrieved."
            $OperationLookupError
        ) -join [Environment]::NewLine
    }

    return [PSCustomObject]$Result
}

# ============================================================================
# Helper function: Remove failed AMR resource
#
# The function waits until the resource is no longer returned by ARM.
# This prevents the next attempt from starting while deletion is in progress.
# ============================================================================
function Remove-FailedAmrResource {
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory)]
        [int]$TimeoutMinutes,

        [Parameter(Mandatory)]
        [int]$CheckIntervalSeconds
    )

    try
    {
        $Resource = Get-AzResource `
            -ResourceGroupName $ResourceGroupName `
            -ResourceType "Microsoft.Cache/redisEnterprise" `
            -ResourceName $Name `
            -ErrorAction SilentlyContinue

        if ($null -eq $Resource)
        {
            Write-ScriptMessage `
                -Message "No failed AMR resource was found to remove."

            return $true
        }

        Write-ScriptMessage `
            -Message "Removing failed AMR resource '$Name'..."

        try
        {
            Remove-AzResource `
                -ResourceGroupName $ResourceGroupName `
                -ResourceType "Microsoft.Cache/redisEnterprise" `
                -ResourceName $Name `
                -Force `
                -ErrorAction Stop | Out-Null
        }
        catch
        {
            $DeleteError = $_ | Out-String

            if ($DeleteError -match "ResourceNotFound")
            {
                Write-ScriptMessage `
                    -Message "Resource was already removed."

                return $true
            }

            throw
        }

        Write-ScriptMessage `
            -Message "Delete request accepted."

        $DeletionDeadline = (Get-Date).AddMinutes($TimeoutMinutes)

        while ((Get-Date) -lt $DeletionDeadline)
        {
            $Resource = Get-AzResource `
                -ResourceGroupName $ResourceGroupName `
                -ResourceType "Microsoft.Cache/redisEnterprise" `
                -ResourceName $Name `
                -ErrorAction SilentlyContinue

            if ($null -eq $Resource)
            {
                Write-ScriptMessage `
                    -Message "Failed AMR resource was completely removed."

                return $true
            }

            Write-ScriptMessage `
                -Message "Resource deletion is still in progress."

            Start-Sleep `
                -Seconds $CheckIntervalSeconds
        }

        Write-ScriptMessage `
            -Message "Timed out waiting for resource deletion."

        return $false
    }
    catch
    {
        $CleanupError = $_ | Out-String

        Write-ScriptMessage `
            -Message "Cleanup operation returned an exception."

        Write-Host $CleanupError

        $Resource = Get-AzResource `
            -ResourceGroupName $ResourceGroupName `
            -ResourceType "Microsoft.Cache/redisEnterprise" `
            -ResourceName $Name `
            -ErrorAction SilentlyContinue

        if ($null -eq $Resource)
        {
            Write-ScriptMessage `
                -Message "Resource is no longer present. Continuing."

            return $true
        }

        return $false
    }
}

# ============================================================================
# UI to select one option from a list
# ============================================================================
function Select-Option {
    param(
        [string]$Prompt,
        [string[]]$Options,
        [int]$DefaultOption = 1
    )

    Write-Host ""
    Write-Host $Prompt

    for ($i = 0; $i -lt $Options.Count; $i++) {
    $DefaultText = if (($i + 1) -eq $DefaultOption) { " (default)" } else { "" }
    Write-Host "$($i + 1). $($Options[$i])$DefaultText"
    }

    # try again until a valid option is provided.
    # If just hit enter, the default option will be used
    do {
        $Selection = Read-Host "Select an option ($($Options[$DefaultOption-1]))"

        # used default option if just hit enter
        if ([string]::IsNullOrWhiteSpace($Selection)) {    
            $Selection = $DefaultOption    
        }

        try {
                $Selection = [int]$Selection
                if ($Selection -lt 1 -or $Selection -gt $Options.Count) {
                   throw "Invalid selection." }
            

            return $Options[[int]$Selection - 1]
        }
        catch {
            Write-Host "Invalid selection. Try again." -ForegroundColor Red
        }
    } while ($true)

}

# ============================================================================
# UI to select multiple options from a list, comma separated
# ============================================================================
function Select-MultipleOptions {
    param(
        [string]$Prompt,
        [string[]]$Options,
        [int]$DefaultOption = 1
    )

    Write-Host ""
    Write-Host $Prompt 

    for ($i = 0; $i -lt $Options.Count; $i++) {
        $DefaultText = if (($i + 1) -eq $DefaultOption) { " (default)" } else { "" }
        Write-Host "$($i + 1). $($Options[$i])$DefaultText"
    }

    do {
        $Selection = Read-Host "Enter option numbers separated by commas (e.g. 1,3,5)"

        # used default option if just hit enter
        if ([string]::IsNullOrWhiteSpace($Selection)) {    
            $Selection = $DefaultOption    
        }

        try {
                $Indexes = $Selection -split ',' | ForEach-Object { [int]($_.Trim()) - 1 }           
                $Invalid = $Indexes | Where-Object { $_ -lt 0 -or $_ -ge $Options.Count }
                if ($Invalid) {
                   throw "Invalid selection." }
            

            return $Indexes | ForEach-Object { $Options[$_] }
        }
        catch {
            Write-Host "Invalid selection. Try again." -ForegroundColor Red
        }
    } while ($true)
}
# ============================================================================


# ============================================================================
# Validate ARM deployment to check if any type of error can occur, including inner errors
# Return main error and inner errors, or empty string if no error ocurr
# ============================================================================
function ValidateDeployment {

    $ValidatePath = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.Resources/deployments/$DeploymentName/validate?api-version=2021-04-01"

    $ValidateBody = @{
        properties = @{
            template = $ArmTemplateObject

            parameters = @{
                redisName = @{
                    value = $CacheName
                }
                location = @{
                    value = $Location
                }
                skuName = @{
                    value = $Sku
                }
                highAvailability = @{
                    value = $HighAvailability
                }
                clusteringPolicy = @{
                    value = $ClusteringPolicy
                }
                evictionPolicy = @{
                    value = $EvictionPolicy
                }
                geoReplicationEnabled = @{
                    value = $loc_GeoReplicationEnabled
                }
                geoReplicationGroupNickname = @{
                    value = $loc_GeoReplicationGroupNickname
                }
                existingLinkedDatabaseIds = @{
                    value = $loc_ExistingLinkedDatabaseIds
                }
            }
            mode = "Incremental"
        }
    }

    $ValidateJson = $ValidateBody | ConvertTo-Json -Depth 100

    try
    {
        $ValidationResponse = Invoke-AzRestMethod `
            -Path $ValidatePath `
            -Method POST `
            -Payload $ValidateJson

        $StatusCode = $($ValidationResponse.StatusCode)

        if ($ValidationResponse.Content)
        {
            $ValidationResult = $ValidationResponse.Content | ConvertFrom-Json

            if ($ValidationResult.error.details)
            {
                $InnerMessages = @(
                    $ValidationResult.error.details | ForEach-Object {
                        $_.message
                    }
                )

                $InnerMessagesText = ""
                $InnerMessagesText = $InnerMessages -join "`n"
                $ErrorMessage = $ValidationResult.error.message
                $InnerMessagesText = "StatusCode: $StatusCode" + "`n" + $ErrorMessage + "`n" + $InnerMessagesText

                if ($StatusCode -lt 300)
                {
                    return ""
                } else {
                    return $InnerMessagesText
                }
            }
        }
        return ""
    }
    catch
    {
        return "CATCH: " + $_
    }
}
# ============================================================================


# ============================================================================
# ARM template
# ============================================================================

$ArmTemplate = @'
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
    "redisName": {
    "type": "string"
    },
    "location": {
    "type": "string"
    },
    "highAvailability": {
    "type": "string"
    },
    "clusteringPolicy": {
    "type": "string"
    },
    "evictionPolicy": {
    "type": "string"
    },
    "skuName": {
    "type": "string"
    },
    "geoReplicationEnabled": {
    "type": "bool"
    },
    "geoReplicationGroupNickname": {
    "type": "string",
    "defaultValue": ""
    },
    "existingLinkedDatabaseIds": {
    "type": "array",
    "defaultValue": []
    }
},

"variables": {
    "currentDatabaseId": "[resourceId('Microsoft.Cache/redisEnterprise/databases', parameters('redisName'), 'default')]",
    "allLinkedDatabaseIds": "[union(parameters('existingLinkedDatabaseIds'), createArray(variables('currentDatabaseId')))]",
    "linkedDatabases": "[map(variables('allLinkedDatabaseIds'), lambda('databaseId', createObject('id', lambdaVariables('databaseId'))))]"
},

"resources": [
    {
        "type": "Microsoft.Cache/redisEnterprise",
        "apiVersion": "2025-04-01",
        "name": "[parameters('redisName')]",
        "location": "[parameters('location')]",
        "sku": {
            "name": "[parameters('skuName')]"
        },
        "properties": {
            "highAvailability": "[parameters('highAvailability')]",
            "minimumTlsVersion": "1.2",
            "clusteringPolicy": "[parameters('clusteringPolicy')]"
        }
    },

    {
        "condition": "[or(not(parameters('geoReplicationEnabled')), equals(parameters('skuName'), 'Balanced_B0'), equals(parameters('skuName'), 'Balanced_B1'))]",
        "type": "Microsoft.Cache/redisEnterprise/databases",
        "apiVersion": "2025-04-01",
        "name": "[concat(parameters('redisName'), '/default')]",
        "dependsOn": [
            "[resourceId('Microsoft.Cache/redisEnterprise', parameters('redisName'))]"
        ],
        "properties": {
            "clientProtocol": "Encrypted",
            "clusteringPolicy": "[parameters('clusteringPolicy')]",
            "evictionPolicy": "[parameters('evictionPolicy')]",
            "port": 10000
        }
    },

    {
        "condition": "[and(parameters('geoReplicationEnabled'), not(or(equals(parameters('skuName'), 'Balanced_B0'), equals(parameters('skuName'), 'Balanced_B1'))))]",
        "type": "Microsoft.Cache/redisEnterprise/databases",
        "apiVersion": "2025-04-01",
        "name": "[concat(parameters('redisName'), '/default')]",
        "dependsOn": [
            "[resourceId('Microsoft.Cache/redisEnterprise', parameters('redisName'))]"
        ],
        "properties": {
            "clientProtocol": "Encrypted",
            "clusteringPolicy": "[parameters('clusteringPolicy')]",
            "evictionPolicy": "[parameters('evictionPolicy')]",
            "port": 10000,
            "geoReplication": {
                "groupNickname": "[parameters('geoReplicationGroupNickname')]",
                "linkedDatabases": "[variables('linkedDatabases')]"
            }
        }
    }
]
}
'@

# ============================================================================================================== 
# Start deployment
# ==============================================================================================================
try
{
    # ============================================================================
    # Validate ARM template JSON locally
    # ============================================================================

    try
    {
        $ArmTemplateObject = $ArmTemplate |
            ConvertFrom-Json `
                -AsHashtable `
                -ErrorAction Stop
    }
    catch
    {
        Write-ScriptMessage `
            -Message "The ARM template is not valid JSON."

        Write-Host ($_ | Out-String)

        exit 1
    }


    # ============================================================================
    # Connect to Azure
    # ============================================================================

    try
    {
        Write-ScriptMessage `
            -Message "Connecting to Azure."

        $Context = Get-AzContext

        if($Context) # Requires Connect-AzAccount before running the script
        {
            Write-ScriptMessage `
                -Message "AzContext found - No need to authenticate interactively."
        } 
        else  # Do not Requires Connect-AzAccount before running the script - requested interactively
        {
            Connect-AzAccount `
                -ErrorAction Stop `
                -WarningAction SilentlyContinue `
                -InformationAction SilentlyContinue |
                    Out-Null

    #Connect-AzAccount -Identity

            $Context = Get-AzContext `
                -ErrorAction Stop `
                -WarningAction SilentlyContinue `
                -InformationAction SilentlyContinue
        } 


        if ($null -eq $Context)
        {
            throw "No active Azure context was returned."
        }

        if ($null -eq $Context.Subscription)
        {
            throw "The active Azure context does not contain a subscription."
        }

        $SubscriptionId = $Context.Subscription.Id

        if ([string]::IsNullOrWhiteSpace($SubscriptionId))
        {
            throw "The active Azure subscription ID is empty."
        }

        Set-AzContext `
            -SubscriptionId $SubscriptionId `
            -ErrorAction Stop `
            -WarningAction SilentlyContinue `
            -InformationAction SilentlyContinue |
                Out-Null

        Write-ScriptMessage `
            -Message "Using subscription $SubscriptionId."

    }
    catch
    {
        Write-ScriptMessage `
            -Message "Azure authentication or context selection failed."

        Write-Host ($_ | Out-String)

        exit 1
    }


    # ============================================================================
    # Get and Validate cache configurations
    # ============================================================================
    try
    {
        # Validate Retry Values
        $MaxRetries = Validate-RetryValues -ParameterName "MaxRetries", -Value $MaxRetries -MinValue 1   # -MaxValue 30                                           # Between 1 to 30 retries     - default 15
        $MinDelayMinutes = Validate-RetryValues -ParameterName "Minimum Delay in Minutes", -Value $MinDelayMinutes -MinValue 15 -MaxValue 1440                # Between 15 minutes to 1 day - default 120 minutes (2 hours)
        $MaxDelayMinutes = Validate-RetryValues -ParameterName "Maximum Delay in Minutes", -Value $MaxDelayMinutes -MinValue $MinDelayMinutes # -MaxValue 2880  # Between MinDelay to 2 days  - default 300 minutes (5 hours)

        Write-ScriptMessage -Message "Using $MaxRetries attempts to try to create cache"
        Write-ScriptMessage -Message "With random delay from $MinDelayMinutes minutes to $MaxDelayMinutes minutes between each attempt"

        # Validate cache name sintax 
        $CacheName = Validate-CacheName -CacheName $CacheName

        # Validate location sintax - to lowercase, no spaces 
        $Location = Validate-Location -Location $Location -SubscriptionId $SubscriptionId

        # Validate existence of ResourceGroup and sintax name - no spaces  
        $ResourceGroup = Validate-ResourceGroup -ResourceGroup $ResourceGroup

        # Validate AMR SKUs provided 
        $Skus = Validate-Skus -Skus $Skus

        # Validate High Availability selection (Enabled / Disabled))
        $HighAvailability = Validate-HighAvailability -HighAvailability $HighAvailability

        # Validate Clustering Policy selection 
        $ClusteringPolicy = Validate-ClusteringPolicy -ClusteringPolicy $ClusteringPolicy

        # Validate EvictionPolicy policy 
        $EvictionPolicy = Validate-EvictionPolicy -EvictionPolicy $EvictionPolicy 
        
        # Validate Geo-Replication details
        $GeoConfig = Validate-GeoReplication `
                -HighAvailability $HighAvailability `
                -GeoReplicationEnabled $GeoReplicationEnabled `
                -GeoReplicationGroupNickname $GeoReplicationGroupNickname `
                -ExistentLinkedCacheNames $ExistentLinkedCacheNames `
                -ExistentLinkedResourceGroupNames $ExistentLinkedResourceGroupNames

        $GeoReplicationEnabled = $GeoConfig.GeoReplicationEnabled
        $GeoReplicationGroupNickname = $GeoConfig.GeoReplicationGroupNickname
        $ExistingLinkedDatabaseIds = $GeoConfig.ExistingLinkedDatabaseIds
    }
    catch
    {
        Write-ScriptMessage `
            -Message "Failed on getting cache configurations"

        #Write-Host ($_ | Out-String)
        Write-Error $_.Exception.Message

        exit 1
    }


    # ============================================================================
    # Deployment retry loop
    # ============================================================================

    for ($Attempt = 1; $Attempt -le $MaxRetries; $Attempt++)
    {
        # Start with array index zero on attempt 1.
        $SkuIndex = ($Attempt - 1) % $Skus.Count
        $Sku = $Skus[$SkuIndex]

        # Generate a unique ARM deployment name for each attempt.
        $DeploymentName = "amr-{0}-{1:D2}-{2}" -f `
            (Get-Date -Format "yyyyMMddHHmmss"),
            $Attempt,
            ([guid]::NewGuid().ToString("N").Substring(0,8))

        Write-Host ""

        Write-ScriptMessage `
            -Message "==== Attempt $Attempt of $MaxRetries ============================"

        Write-ScriptMessage -Message "Cache: $CacheName"
        Write-ScriptMessage -Message "Resource Group: $ResourceGroup (existent)"
        Write-ScriptMessage -Message "Region: $Location"
        Write-ScriptMessage -Message "SKU: $Sku"
        Write-ScriptMessage -Message "High Availability: $HighAvailability"
        Write-ScriptMessage -Message "Clustering Policy: $ClusteringPolicy"
        Write-ScriptMessage -Message "Eviction Policy: $EvictionPolicy"


        # Geo-Replication details from each deployment ============================
        # Geo-Replication not supported with HA disabled
        # we need to check the sku used on each deployment iteraction to enable / disable Geo-Replication
        if (($HighAvailability -eq "Disabled") -or ($Sku -in @("Balanced_B0", "Balanced_B1"))) {     
            $loc_GeoReplicationEnabled = $false
            $loc_GeoReplicationGroupNickname = ""
            $loc_ExistingLinkedDatabaseIds = @()
        } 
        else # Geo-Replication supported
        {            
            $loc_GeoReplicationEnabled = $GeoReplicationEnabled
            $loc_GeoReplicationGroupNickname = $GeoReplicationGroupNickname
            $loc_ExistingLinkedDatabaseIds = $ExistingLinkedDatabaseIds
        }

        Write-ScriptMessage -Message "Geo-Replication enabled: $loc_GeoReplicationEnabled"

        $localGeoGroupMsg = ""
        if(-not [string]::IsNullOrWhiteSpace($loc_ExistingLinkedDatabaseIds)){
             $localGeoGroupMsg = "(existent Geo-Group)"
        }

        if(-not [string]::IsNullOrWhiteSpace($loc_GeoReplicationEnabled)) {
            Write-ScriptMessage -Message "Geo-Replication Group Name: $loc_GeoReplicationGroupNickname $localGeoGroupMsg"
            # No need to share the Linked Database IDs again on each deployment attempt iteration
            # Write-ScriptMessage -Message "Geo-Replication Linked Database IDs: $loc_ExistingLinkedDatabaseIds"
        }
        # ========== End of parameter validation ==========================


        # =========== Starting the deployment ==============================
        Write-ScriptMessage -Message "Deployment name: $DeploymentName"
        try
        {
            Write-ScriptMessage `
            -Message "Starting ARM deployment..."

            $DeploymentValidationError = ValidateDeployment
            if(-not [String]::IsNullOrEmpty($DeploymentValidationError))
            {
                throw
            }

            #-------------------------------------------------------------------------------------------
            # $Deployment - contains the normal output objects returned by the cmdlet - ARM deployment operation status/errors
            #-------------------------------------------------------------------------------------------
            $Deployment = New-AzResourceGroupDeployment `
                -Name $DeploymentName `
                -ResourceGroupName $ResourceGroup `
                -TemplateObject $ArmTemplateObject `
                -redisName $CacheName `
                -location $Location `
                -skuName $Sku `
                -highAvailability $HighAvailability `
                -clusteringPolicy $ClusteringPolicy `
                -evictionPolicy $EvictionPolicy `
                -geoReplicationEnabled $loc_GeoReplicationEnabled `
                -geoReplicationGroupNickname $loc_GeoReplicationGroupNickname `
                -existingLinkedDatabaseIds $loc_ExistingLinkedDatabaseIds `
                -Mode Incremental `
                -DeploymentDebugLogLevel All `
                -ErrorAction SilentlyContinue `
                -WarningAction SilentlyContinue `
                -InformationAction SilentlyContinue `
                -Confirm:$false 

            # Retrieve the deployment again to confirm its final ARM state.
            $DeploymentStatus = Get-AzResourceGroupDeployment `
                -ResourceGroupName $ResourceGroup `
                -Name $DeploymentName `
                -ErrorAction SilentlyContinue `
                -WarningAction SilentlyContinue `
                -InformationAction SilentlyContinue 

            if ($DeploymentStatus.ProvisioningState -eq "Succeeded")
            {
                Write-ScriptMessage `
                    -Message "Deployment succeeded using SKU $Sku."

                exit 0
            }

            # $DeploymentStatus - contains the normal output objects returned by the cmdlet - ARM deployment operation status/errors
            $UnexpectedProvisioningState = $DeploymentStatus.ProvisioningState
            throw "Deployment '$DeploymentName' completed with provisioning state '$UnexpectedProvisioningState'."
        }
        catch
        {
            if(-not [String]::IsNullOrEmpty($DeploymentValidationError))
            {
                # $DeploymentValidationError contain main error + inner errors in case of validation fail, or empty string
                $ExceptionText += $DeploymentValidationError
            } else {
                # Save the original deployment error before running diagnostic commands.
                $OriginalErrorRecord = $_

                # Add inner errors to main error as a string to seach for Capacity patterns
                $ExceptionText = $OriginalErrorRecord.Exception.Message
            }

            Write-ScriptMessage `
                -Message "Deployment attempt failed."

            Write-ScriptMessage `
                                -Message $ExceptionText -ForegroundColor Yellow

            Write-ScriptMessage `
                -Message "Checking whether an ARM deployment record was created."

            $OperationResult = Get-DeploymentOperationError `
                -DeploymentName $DeploymentName `
                -ResourceGroupName $ResourceGroup

            if ($OperationResult.DeploymentFound)
            {
                Write-ScriptMessage `
                    -Message "The ARM deployment record was found."
            }
            else
            {
                Write-ScriptMessage `
                    -Message "The ARM deployment record was not found."

                Write-ScriptMessage `
                    -Message "The deployment failed before ARM persisted the deployment record."
            }

            if ($OperationResult.OperationsFound)
            {
                Write-ScriptMessage `
                    -Message "Deployment operation details were retrieved."

                if ($OperationResult.FailedOperations.Count -eq 0)
                {
                    Write-ScriptMessage `
                        -Message "No operation with a failed or canceled state was returned."
                }

                foreach (
                    $FailedOperation in $OperationResult.FailedOperations
                )
                {
                    $ProvisioningState = `
                        $FailedOperation.ProvisioningState

                    if (
                        [string]::IsNullOrWhiteSpace($ProvisioningState) -and
                        $null -ne $FailedOperation.Properties
                    )
                    {
                        $ProvisioningState = `
                            $FailedOperation.Properties.ProvisioningState
                    }

                    $OperationType = $FailedOperation.OperationId
                    $TargetType = ""
                    $TargetName = ""

                    if ($null -ne $FailedOperation.TargetResource)
                    {
                        $TargetType = `
                            $FailedOperation.TargetResource.ResourceType

                        $TargetName = `
                            $FailedOperation.TargetResource.ResourceName
                    }

                    if (
                        $null -ne $FailedOperation.Properties -and
                        $null -ne $FailedOperation.Properties.TargetResource
                    )
                    {
                        $TargetType = `
                            $FailedOperation.Properties.TargetResource.ResourceType

                        $TargetName = `
                            $FailedOperation.Properties.TargetResource.ResourceName
                    }
    
                    $FailedOperationMessage = `
                    "Failed operation: State='{0}', Operation='{1}', ResourceType='{2}', ResourceName='{3}'." -f `
                    $ProvisioningState,
                    $OperationType,
                    $TargetType,
                    $TargetName

                    Write-ScriptMessage `
                        -Message $FailedOperationMessage
                }
            }
            else
            {
                Write-ScriptMessage `
                    -Message "Detailed deployment operations were not available."
            }

            # Check both the original PowerShell exception and ARM operation details.
            $ExceptionIndicatesCapacity = Test-CapacityError -ErrorText $ExceptionText 

            $CapacityIssueDetected = (
                $ExceptionIndicatesCapacity -or
                $OperationResult.CapacityDetected
            )

            if ($CapacityIssueDetected)
            {
                Write-ScriptMessage `
                    -Message "Capacity-related error detected." -ForegroundColor Red

                $CleanupSucceeded = Remove-FailedAmrResource `
                    -Name $CacheName `
                    -ResourceGroupName $ResourceGroup `
                    -TimeoutMinutes $DeletionTimeoutMinutes `
                    -CheckIntervalSeconds $DeletionCheckIntervalSeconds

                if (-not $CleanupSucceeded)
                {
                    Write-ScriptMessage `
                        -Message "The failed resource could not be confirmed as deleted."

                    Write-ScriptMessage `
                        -Message "The script will not start another deployment over the existing resource."

                    exit 1
                }

                if ($Attempt -ge $MaxRetries)
                {
                    Write-Host ""

                    Write-ScriptMessage `
                        -Message "Maximum retry count of $MaxRetries reached."

                    if (
                        -not [string]::IsNullOrWhiteSpace(
                            $OperationResult.ErrorText
                        )
                    )
                    {
                        Write-ScriptMessage `
                            -Message "Last ARM error details:"

                        Write-Host $OperationResult.ErrorText
                    }

                    exit 1
                }

                # Get-Random excludes the maximum value.
                # Add 1 to make MaxDelayMinutes inclusive.
                $DelayMinutes = Get-Random `
                    -Minimum $MinDelayMinutes `
                    -Maximum ($MaxDelayMinutes + 1)

                Write-ScriptMessage `
                    -Message "Retrying in $DelayMinutes minutes."

                Start-Sleep `
                    -Seconds ($DelayMinutes * 60)

                continue
            }

            # Non-capacity errors are not retried automatically.
            Write-ScriptMessage `
                -Message "The failure was not identified as a capacity error."

            if (
                -not [string]::IsNullOrWhiteSpace(
                    $OperationResult.ErrorText
                )
            )
            {
                Write-ScriptMessage `
                    -Message "Detailed ARM error:"

                Write-Host $OperationResult.ErrorText
            }
            else
            {
                Write-ScriptMessage `
                    -Message "Original PowerShell error:"

                Write-Host $ExceptionText
            }

            exit 1
        }
    }


    # ============================================================================
    # The loop should normally exit from either the success or failure path.
    # ============================================================================

    Write-Host ""

    Write-ScriptMessage `
        -Message "Maximum retry count of $MaxRetries reached without a successful deployment."

    exit 1
}
    finally
{
    Stop-Transcript
}

# ========================================================
#========================================================



I hope this can be useful.
If this was useful for you, please share your comments below.
Thank you.

Updated Oct 08, 2026
Version 2.0