Private/Azure/Get-ValidAzureRegions.ps1

function Get-ValidAzureRegions {
    <#
    .SYNOPSIS
        Returns list of valid Azure region names that support Compute, with caching.
    .DESCRIPTION
        Uses REST API for speed (2-3x faster than Get-AzLocation).
        Falls back to Get-AzLocation if REST API fails.
        Caches result in the passed-in -Caches dictionary to avoid repeated calls.
    #>

    [OutputType([string[]])]
    param(
        [int]$MaxRetries = 3,
        [hashtable]$AzureEndpoints,
        [System.Collections.IDictionary]$Caches = @{}
    )

    # Return cached result if available
    $cachedRegions = $Caches.ValidRegions
    if ($cachedRegions) {
        Write-Verbose "Using cached region list ($($cachedRegions.Count) regions)"
        return $cachedRegions
    }

    Write-Verbose "Fetching valid Azure regions..."

    try {
        # Get current subscription context
        $ctx = Get-AzContext -ErrorAction Stop
        if (-not $ctx) {
            throw "No Azure context available"
        }

        $subId = $ctx.Subscription.Id

        # Use environment-aware ARM URL (supports sovereign clouds)
        $armUrl = if ($AzureEndpoints) { $AzureEndpoints.ResourceManagerUrl } else { 'https://management.azure.com' }
        $armUrl = $armUrl.TrimEnd('/')

        $token = (Get-AzAccessToken -ResourceUrl $armUrl -ErrorAction Stop).Token

        # REST API call (faster than Get-AzLocation)
        $uri = "$armUrl/subscriptions/$subId/locations?api-version=2022-12-01"
        $headers = @{
            'Authorization' = "Bearer $token"
            'Content-Type'  = 'application/json'
        }

        try {
            $response = Invoke-WithRetry -MaxRetries $MaxRetries -OperationName 'Region list API' -ScriptBlock {
                Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop
            }
        }
        finally {
            $headers['Authorization'] = $null
            $token = $null
        }

        # Filter to regions with valid names (exclude logical/paired regions)
        $validRegions = $response.value | Where-Object {
            $_.metadata.regionCategory -ne 'Other' -and
            $_.name -match '^[a-z0-9]+$'
        } | Select-Object -ExpandProperty name | ForEach-Object { $_.ToLower() }

        if ($validRegions.Count -eq 0) {
            throw "REST API returned no valid regions"
        }

        Write-Verbose "Fetched $($validRegions.Count) regions via REST API"
        $Caches.ValidRegions = @($validRegions)
        return @($validRegions)
    }
    catch {
        Write-Verbose "REST API failed: $($_.Exception.Message). Falling back to Get-AzLocation..."

        try {
            # Fallback to Get-AzLocation (slower but more reliable)
            $validRegions = Get-AzLocation -ErrorAction Stop |
            Where-Object { $_.Providers -contains 'Microsoft.Compute' } |
            Select-Object -ExpandProperty Location |
            ForEach-Object { $_.ToLower() }

            if ($validRegions.Count -eq 0) {
                throw "Get-AzLocation returned no valid regions"
            }

            Write-Verbose "Fetched $($validRegions.Count) regions via Get-AzLocation"
            $Caches.ValidRegions = @($validRegions)
            return @($validRegions)
        }
        catch {
            Write-Warning "Failed to retrieve valid Azure regions: $($_.Exception.Message)"
            Write-Warning "Region validation metadata is unavailable."
            return $null
        }
    }
}