content/src/scripts/Resolve-ResourceName.ps1

# ---------------------------------------------------------------------------
# Resolve-ResourceName.ps1
# Axeon Naming Convention Engine
#
# A token-based, pattern-driven naming resolver that produces Azure resource
# names from a declarative configuration in platform-spec.json.
#
# TOKEN SYSTEM
# ────────────
# Tokens are placeholders in patterns, enclosed in braces: {org}, {env}, etc.
# Each token has three length variants:
# s = short (e.g. "ax", "p", "vnet")
# m = medium (e.g. "axeon", "prod", "vnetwork")
# l = long (e.g. "axeon-global", "production", "virtual-network")
#
# IN-PATTERN LENGTH OVERRIDE
# ──────────────────────────
# A pattern can force a specific length for any token:
# {org} → uses the default length (naming.defaultLength)
# {org:s} → always uses short
# {org:m} → always uses medium
# {org:l} → always uses long
#
# RESOLUTION ORDER
# ────────────────
# 1. Look for a resource-specific override in naming.overrides.{resourceType}
# 2. Look for a resource-specific pattern in naming.patterns.{resourceType}
# 3. Fall back to naming.patterns.default
# 4. Resolve each {token[:length]} using naming.tokens + naming.resources
# 5. Join segments with the configured separator
# 6. Apply casing, charset filtering, and maxLength constraints
#
# CHARSET CONSTRAINTS
# ───────────────────
# alphanumeric → [a-z0-9] only (e.g. storage accounts)
# alphanumericHyphen → [a-z0-9-] (e.g. key vaults)
# any → no filtering (default)
# ---------------------------------------------------------------------------

<#
.SYNOPSIS
    Resolves an Azure resource name using the Axeon naming convention engine.
.DESCRIPTION
    Takes the naming configuration from platform-spec.json and a resource type
    key, resolves all tokens, applies constraints, and returns the final name.
.PARAMETER NamingConfig
    The naming hashtable from platform-spec (i.e. $spec.naming).
.PARAMETER ResourceType
    The logical resource type key (e.g. "resourceGroup", "storageAccount").
.PARAMETER TokenOverrides
    Optional hashtable of token values to override for this specific call.
    For example: @{ idx = @{ s='1'; m='02'; l='002' } }
    Or shorthand: @{ idx = '1' } (uses the value for all lengths)
.PARAMETER Index
    Shorthand for overriding the idx token with a specific number.
    Automatically generates s/m/l variants: 0 → "0"/"01"/"001".
.PARAMETER FailoverKind
    Shorthand for overriding the fok token. Accepts "primary" or "secondary".
.EXAMPLE
    Resolve-ResourceName -NamingConfig $spec.naming -ResourceType 'resourceGroup'
    # → "rg-ax-lza-p-uks-shr-0"
.EXAMPLE
    Resolve-ResourceName -NamingConfig $spec.naming -ResourceType 'storageAccount' -Index 1
    # → "axlzapuksst1"
.EXAMPLE
    Resolve-ResourceName -NamingConfig $spec.naming -ResourceType 'virtualNetwork' -TokenOverrides @{ typ = @{ s='wl'; m='workload'; l='workload-instance' } }
    # → "vnet-ax-lza-p-uks-wl-0"
#>

function Resolve-ResourceName {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [hashtable]$NamingConfig,

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

        [hashtable]$TokenOverrides = @{},

        [int]$Index = -1,

        [ValidateSet('primary', 'secondary', 'tertiary')]
        [string]$FailoverKind
    )

    # ── Merge defaults with resource-specific overrides ──────────────────
    $defaults = @{
        separator     = $NamingConfig.separator     ?? '-'
        casing        = $NamingConfig.casing        ?? 'lower'
        defaultLength = $NamingConfig.defaultLength ?? 's'
        charset       = 'any'
        maxLength     = 0   # 0 = unlimited
    }

    $override = if ($NamingConfig.ContainsKey('overrides') -and
                    $NamingConfig.overrides.ContainsKey($ResourceType)) {
        $NamingConfig.overrides[$ResourceType]
    } else { @{} }

    $separator     = $override.separator     ?? $defaults.separator
    $casing        = $override.casing        ?? $defaults.casing
    $defaultLength = $override.defaultLength ?? $defaults.defaultLength
    $charset       = $override.charset       ?? $defaults.charset
    $maxLength     = [int]($override.maxLength ?? $defaults.maxLength)

    # ── Select pattern ───────────────────────────────────────────────────
    $patterns = $NamingConfig.patterns ?? @{}

    $pattern = if ($override.ContainsKey('pattern')) {
        $override.pattern
    }
    elseif ($patterns.ContainsKey($ResourceType)) {
        $patterns[$ResourceType]
    }
    elseif ($patterns.ContainsKey('default')) {
        $patterns['default']
    }
    else {
        throw "No naming pattern found for resource type '$ResourceType' and no default pattern defined."
    }

    # ── Build the token value map ────────────────────────────────────────
    # Start from configured tokens
    $tokenMap = @{}
    if ($NamingConfig.ContainsKey('tokens')) {
        foreach ($key in $NamingConfig.tokens.Keys) {
            $tokenMap[$key] = $NamingConfig.tokens[$key]
        }
    }

    # Add the special {rsc} token from the resources catalog
    if ($NamingConfig.ContainsKey('resources') -and
        $NamingConfig.resources.ContainsKey($ResourceType)) {
        $tokenMap['rsc'] = $NamingConfig.resources[$ResourceType]
    }
    else {
        # If resource type isn't cataloged, use the key itself
        $tokenMap['rsc'] = @{ s = $ResourceType; m = $ResourceType; l = $ResourceType }
    }

    # Apply shorthand Index override → generates s/m/l
    if ($Index -ge 0) {
        $TokenOverrides['idx'] = @{
            s = "$Index"
            m = $Index.ToString('D2')
            l = $Index.ToString('D3')
        }
    }

    # Apply shorthand FailoverKind override
    if ($FailoverKind) {
        $fokMap = @{
            primary   = @{ s = 'pri'; m = 'primary';   l = 'active-region' }
            secondary = @{ s = 'sec'; m = 'secondary'; l = 'standby-region' }
            tertiary  = @{ s = 'ter'; m = 'tertiary';  l = 'tertiary-region' }
        }
        $TokenOverrides['fok'] = $fokMap[$FailoverKind]
    }

    # Merge explicit overrides (they win)
    foreach ($key in $TokenOverrides.Keys) {
        $val = $TokenOverrides[$key]
        if ($val -is [hashtable]) {
            $tokenMap[$key] = $val
        }
        else {
            # Scalar shorthand → same value for all lengths
            $tokenMap[$key] = @{ s = "$val"; m = "$val"; l = "$val" }
        }
    }

    # ── Resolve tokens in the pattern ────────────────────────────────────
    # Regex matches {token} or {token:length}
    $resolved = [regex]::Replace($pattern, '\{(\w+)(?::([sml]))?\}', {
        param($match)
        $tokenName   = $match.Groups[1].Value
        $lengthHint  = $match.Groups[2].Value

        if (-not $lengthHint) { $lengthHint = $defaultLength }

        if ($tokenMap.ContainsKey($tokenName)) {
            $entry = $tokenMap[$tokenName]
            if ($entry -is [hashtable] -and $entry.ContainsKey($lengthHint)) {
                return $entry[$lengthHint]
            }
            elseif ($entry -is [hashtable]) {
                # Fall back: try s → m → l
                return ($entry['s'] ?? $entry['m'] ?? $entry['l'] ?? $tokenName)
            }
            else {
                return "$entry"
            }
        }

        # If token not found, leave placeholder for debugging
        Write-Warning "Naming token '{$tokenName}' not defined — left as placeholder."
        return "{$tokenName}"
    })

    # ── Post-processing ──────────────────────────────────────────────────

    # Replace any remaining separators from token values (e.g. "axeon-global")
    # with the configured separator, then clean up double separators
    if ($separator -eq '') {
        $resolved = $resolved -replace '-', ''
    }
    else {
        # Collapse multiple consecutive separators into one
        $resolved = $resolved -replace "[${separator}]{2,}", $separator
        # Trim leading/trailing separators
        $resolved = $resolved.Trim($separator)
    }

    # Casing
    $resolved = switch ($casing) {
        'lower' { $resolved.ToLower() }
        'upper' { $resolved.ToUpper() }
        default { $resolved }
    }

    # Charset filtering
    $resolved = switch ($charset) {
        'alphanumeric' {
            ($resolved -replace '[^a-zA-Z0-9]', '')
        }
        'alphanumericHyphen' {
            ($resolved -replace '[^a-zA-Z0-9-]', '')
        }
        default { $resolved }
    }

    # Max length (truncate from the right)
    if ($maxLength -gt 0 -and $resolved.Length -gt $maxLength) {
        $resolved = $resolved.Substring(0, $maxLength)
        # Don't end with a separator
        $resolved = $resolved.TrimEnd($separator)
    }

    return $resolved
}

<#
.SYNOPSIS
    Resolves all resource names in a single call and returns a hashtable.
.DESCRIPTION
    Iterates over all resource types defined in naming.resources and resolves
    each one, returning a lookup table. Useful for passing to Bicep parameters.
.PARAMETER NamingConfig
    The naming hashtable from platform-spec.
.PARAMETER TokenOverrides
    Optional overrides applied to all resource types.
.PARAMETER Index
    Instance index to apply globally.
.EXAMPLE
    $names = Resolve-AllResourceNames -NamingConfig $spec.naming -Index 0
    $names.resourceGroup # → "rg-ax-lza-p-uks-shr-0"
    $names.storageAccount # → "axlzapuksst0"
    $names.virtualNetwork # → "vnet-ax-lza-p-uks-shr-0"
#>

function Resolve-AllResourceNames {
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [hashtable]$NamingConfig,

        [hashtable]$TokenOverrides = @{},

        [int]$Index = -1,

        [string]$FailoverKind
    )

    $result = @{}
    $resources = $NamingConfig.resources ?? @{}

    foreach ($resourceType in $resources.Keys) {
        $params = @{
            NamingConfig   = $NamingConfig
            ResourceType   = $resourceType
            TokenOverrides = $TokenOverrides
        }
        if ($Index -ge 0) { $params['Index'] = $Index }
        if ($FailoverKind) { $params['FailoverKind'] = $FailoverKind }

        $result[$resourceType] = Resolve-ResourceName @params
    }

    return $result
}

<#
.SYNOPSIS
    Validates the naming configuration for completeness and correctness.
.DESCRIPTION
    Checks that all tokens referenced in patterns are defined, that required
    sections exist, and that length variants are consistent.
.PARAMETER NamingConfig
    The naming hashtable from platform-spec.
#>

function Test-NamingConfig {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$NamingConfig
    )

    $errors = @()

    # Must have tokens
    if (-not $NamingConfig.ContainsKey('tokens') -or $NamingConfig.tokens.Count -eq 0) {
        $errors += "naming.tokens is missing or empty"
    }

    # Must have at least a default pattern
    if (-not $NamingConfig.ContainsKey('patterns') -or
        -not $NamingConfig.patterns.ContainsKey('default')) {
        $errors += "naming.patterns.default is required"
    }

    # Validate each token has s/m/l
    if ($NamingConfig.ContainsKey('tokens')) {
        foreach ($key in $NamingConfig.tokens.Keys) {
            $token = $NamingConfig.tokens[$key]
            foreach ($len in @('s', 'm', 'l')) {
                if (-not $token.ContainsKey($len)) {
                    $errors += "Token '$key' is missing length variant '$len'"
                }
            }
        }
    }

    # Validate resource types have s/m/l
    if ($NamingConfig.ContainsKey('resources')) {
        foreach ($key in $NamingConfig.resources.Keys) {
            $rsc = $NamingConfig.resources[$key]
            foreach ($len in @('s', 'm', 'l')) {
                if (-not $rsc.ContainsKey($len)) {
                    $errors += "Resource type '$key' is missing length variant '$len'"
                }
            }
        }
    }

    # Validate that all tokens in patterns are resolvable
    $allTokenNames = @()
    if ($NamingConfig.ContainsKey('tokens'))    { $allTokenNames += $NamingConfig.tokens.Keys }
    $allTokenNames += 'rsc'  # always available

    $allPatterns = @()
    if ($NamingConfig.ContainsKey('patterns')) {
        $allPatterns += $NamingConfig.patterns.Values
    }
    if ($NamingConfig.ContainsKey('overrides')) {
        foreach ($ov in $NamingConfig.overrides.Values) {
            if ($ov.ContainsKey('pattern')) { $allPatterns += $ov.pattern }
        }
    }

    foreach ($pat in $allPatterns) {
        $matches = [regex]::Matches($pat, '\{(\w+)(?::[sml])?\}')
        foreach ($m in $matches) {
            $tName = $m.Groups[1].Value
            if ($tName -notin $allTokenNames) {
                $errors += "Pattern '$pat' references undefined token '{$tName}'"
            }
        }
    }

    # Report
    if ($errors.Count -gt 0) {
        foreach ($e in $errors) {
            Write-Warning "Naming config: $e"
        }
        return $false
    }

    return $true
}

<#
.SYNOPSIS
    Prints a formatted preview table of all resolved resource names.
.DESCRIPTION
    Useful for debugging/reviewing naming conventions before deployment.
.PARAMETER NamingConfig
    The naming hashtable from platform-spec.
.PARAMETER Index
    Instance index for preview.
#>

function Show-NamingPreview {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$NamingConfig,

        [int]$Index = 0
    )

    Write-Host ""
    Write-Host " Axeon Naming Convention Preview" -ForegroundColor Cyan
    Write-Host " ═══════════════════════════════════════════════════════" -ForegroundColor DarkCyan
    Write-Host ""

    $resources = $NamingConfig.resources ?? @{}

    # Column widths
    $maxKey = ($resources.Keys | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum
    $maxKey = [Math]::Max($maxKey, 20)

    Write-Host (" {0,-$maxKey} {1,-35} {2}" -f "Resource Type", "Name (short)", "Name (long)") -ForegroundColor DarkGray
    Write-Host (" {0,-$maxKey} {1,-35} {2}" -f ("-" * $maxKey), ("-" * 35), ("-" * 35)) -ForegroundColor DarkGray

    foreach ($resourceType in ($resources.Keys | Sort-Object)) {
        # Resolve with short default
        $shortConfig = Copy-NamingConfig $NamingConfig
        $shortConfig.defaultLength = 's'
        $shortName = Resolve-ResourceName -NamingConfig $shortConfig -ResourceType $resourceType -Index $Index

        # Resolve with long default
        $longConfig = Copy-NamingConfig $NamingConfig
        $longConfig.defaultLength = 'l'
        $longName = Resolve-ResourceName -NamingConfig $longConfig -ResourceType $resourceType -Index $Index

        Write-Host (" {0,-$maxKey} {1,-35} {2}" -f $resourceType, $shortName, $longName) -ForegroundColor White
    }

    Write-Host ""
}

# ── Internal helper: deep-copy a naming config hashtable ─────────────────
function Copy-NamingConfig {
    param([hashtable]$Source)

    $json = $Source | ConvertTo-Json -Depth 10
    return ($json | ConvertFrom-Json -AsHashtable)
}