core/public/New-JaxEnvironment.ps1

function New-JaxEnvironment {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string] $Name,

        [Parameter(Mandatory = $false)]
        [string] $Client,

        [Parameter(Mandatory = $false)]
        [string] $DefaultFlow,

        [Parameter(Mandatory = $false)]
        [string] $Template = 'default',

        [Parameter(Mandatory = $false)]
        [string] $RepoRoot = (Get-JaxRepoRoot)
    )

    # $commonParams = Get-JaxCommonParameters -BoundParameters $PSBoundParameters
    $repoRoot = [IO.Path]::GetFullPath($RepoRoot)
    $config = Get-JaxConfig -RepoRoot $repoRoot

    # 1. Resolve Environment Name
    if ([string]::IsNullOrWhiteSpace($Name)) {
        $Name = Read-Host "Enter Environment Name"
        if ([string]::IsNullOrWhiteSpace($Name)) {
            Write-Error "Environment name cannot be empty."
            return
        }
    }

    # 2. Resolve Client Scope
    if ([string]::IsNullOrWhiteSpace($Client)) {
        # Check if user wants to specify client
        $hasClient = Read-JaxPromptBool -Prompt "Is this environment for a specific client?" -Default $false
        if ($hasClient) {
            $Client = Read-Host "Enter Client Name"
            if ([string]::IsNullOrWhiteSpace($Client)) {

                Write-Error "Client name cannot be empty if specified."
                return
            }
        }
    }

    # 3. Resolve Default Flow
    if ([string]::IsNullOrWhiteSpace($DefaultFlow)) {
        $DefaultFlow = Read-Host "Enter Default Flow Name (default: build)"
        if ([string]::IsNullOrWhiteSpace($DefaultFlow)) {
            $DefaultFlow = 'build'
        }
    }

    # 4. Resolve Template (if not explicitly provided)
    $templatesBaseDir = Join-Path (Get-JaxToolRoot) 'templates/env'
    if (-not $PSBoundParameters.ContainsKey('Template') -and (Test-Path $templatesBaseDir)) {
        # Discover available templates
        $availableTemplates = @(Get-ChildItem -Path $templatesBaseDir -Directory | Select-Object -ExpandProperty Name | Sort-Object)
        if ($availableTemplates.Count -gt 1) {
            Write-Host "Select template (↑/↓ to navigate, Enter to select):" -ForegroundColor Cyan

            # Find default index
            $selectedIndex = 0
            for ($i = 0; $i -lt $availableTemplates.Count; $i++) {
                if ($availableTemplates[$i] -eq 'default') {
                    $selectedIndex = $i
                    break
                }
            }

            # Interactive selection
            $done = $false
            while (-not $done) {
                # Clear and redraw options
                for ($i = 0; $i -lt $availableTemplates.Count; $i++) {
                    $marker = if ($availableTemplates[$i] -eq 'default') { ' (default)' } else { '' }
                    if ($i -eq $selectedIndex) {
                        Write-Host " > $($availableTemplates[$i])$marker" -ForegroundColor Green
                    } else {
                        Write-Host " $($availableTemplates[$i])$marker" -ForegroundColor Gray
                    }
                }

                # Read key
                $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')

                switch ($key.VirtualKeyCode) {
                    38 { # Up arrow
                        $selectedIndex = if ($selectedIndex -gt 0) { $selectedIndex - 1 } else { $availableTemplates.Count - 1 }
                    }
                    40 { # Down arrow
                        $selectedIndex = if ($selectedIndex -lt $availableTemplates.Count - 1) { $selectedIndex + 1 } else { 0 }
                    }
                    13 { # Enter
                        $done = $true
                    }
                }

                # Move cursor up to redraw (clear previous lines)
                if (-not $done) {
                    for ($i = 0; $i -lt $availableTemplates.Count; $i++) {
                        [Console]::SetCursorPosition(0, [Console]::CursorTop - 1)
                        Write-Host (" " * 60)
                        [Console]::SetCursorPosition(0, [Console]::CursorTop - 1)
                    }
                }
            }

            $Template = $availableTemplates[$selectedIndex]
            Write-Host "Selected: $Template" -ForegroundColor Green
        }
    }

    # 4. Determine Paths
    $envRootVar = 'env'
    if ($null -ne $config -and $config.ContainsKey('envRoot')) {
        $envRootVar = $config['envRoot']
    }
    $envRootFromConfig = $envRootVar

    $envRootPath = Join-Path $repoRoot $envRootFromConfig

    $targetEnvPath = if (-not [string]::IsNullOrWhiteSpace($Client)) {
        Join-Path $envRootPath $Client
    } else {
        $envRootPath # If no client, is it common or root?
        # User requested: "if specifid env... then common/scripts/scenarios-lib should be reated there"
        # Wait, usually environments are specific directories.
        # If no client, we probably want $EnvRoot/Name (like sample-app)
        # Assuming common environments exist at root of env/
    }

    # Correct path logic:
    # If client: env/client/name
    # If no client: env/name
    if (-not [string]::IsNullOrWhiteSpace($Client)) {
        $targetEnvPath = Join-Path $envRootPath $Client
        $targetEnvPath = Join-Path $targetEnvPath $Name
    } else {
        $targetEnvPath = Join-Path $envRootPath $Name
    }

    Write-Host "Creating environment '$Name' at: $targetEnvPath" -ForegroundColor Cyan

    if (Test-Path $targetEnvPath) {
        Write-Warning "Directory already exists at $targetEnvPath. Skipping creation."
        # Optional: Ask to overwrite? For now, abort to be safe.
        return
    }

    # 5. Create Directories
    $dirs = @('flows', 'scenarios-lib', 'library', 'scripts') # 'flows' per config default, can update if config differs
    foreach ($d in $dirs) {
        $p = Join-Path $targetEnvPath $d
        New-Item -ItemType Directory -Path $p -Force | Out-Null
    }

    # 6. Apply Templates
    $templateDir = Join-Path $templatesBaseDir $Template
    if (-not (Test-Path $templateDir)) {
        Write-Error "Template '$Template' not found at $templateDir"
        return
    }

    # Replacements
    $tokens = @{
        '{{EnvName}}'  = $Name
        '{{FlowName}}' = $DefaultFlow
    }

    # Helper to process file
    function Invoke-TemplateFileProcessing {
        param($Src, $Dst, $Tokens)
        $content = Get-Content -Path $Src -Raw
        foreach ($key in $Tokens.Keys) {
            $content = $content.Replace($key, $Tokens[$key])
        }
        Set-Content -Path $Dst -Value $content -Force -Encoding utf8
    }

    function ConvertTo-TemplateParamInt {
        param(
            [string] $Value
        )
        if ([string]::IsNullOrWhiteSpace($Value)) {
            return $null
        }
        $parsed = 0
        $isOk = [int]::TryParse($Value.Trim(), [ref]$parsed)
        if (-not $isOk) {
            return $null
        }
        return $parsed
    }

    function Test-TemplateNodePort {
        param(
            [int] $Port,
            [int] $MinPort,
            [int] $MaxPort
        )
        if ($Port -lt $MinPort) { return $false }
        if ($Port -gt $MaxPort) { return $false }
        return $true
    }

    function Resolve-TemplateParameters {
        param(
            [System.Collections.IDictionary] $TemplateDef,
            [hashtable] $Tokens
        )

        if (-not $TemplateDef) { return $Tokens }
        if (-not ($TemplateDef.Contains('parameters') -and $TemplateDef.parameters)) { return $Tokens }

        $minNodePort = 30000
        $maxNodePort = 32767
        if ($TemplateDef.Contains('nodePortRangeStart')) {
            $minCandidate = ConvertTo-TemplateParamInt -Value ([string]$TemplateDef.nodePortRangeStart)
            if ($minCandidate) { $minNodePort = $minCandidate }
        }
        if ($TemplateDef.Contains('nodePortRangeEnd')) {
            $maxCandidate = ConvertTo-TemplateParamInt -Value ([string]$TemplateDef.nodePortRangeEnd)
            if ($maxCandidate) { $maxNodePort = $maxCandidate }
        }

        $uniqueGroups = @{}
        foreach ($paramDef in @($TemplateDef.parameters)) {
            if (-not $paramDef) { continue }

            $paramName = [string]$paramDef.name
            if ([string]::IsNullOrWhiteSpace($paramName)) { continue }

            $token = if ($paramDef.token) { [string]$paramDef.token } else { "{{${paramName}}}" }
            $prompt = if ($paramDef.prompt) { [string]$paramDef.prompt } else { $paramName }
            $type = if ($paramDef.type) { ([string]$paramDef.type).ToLowerInvariant() } else { 'string' }
            $required = $false
            if ($paramDef.required -is [bool]) { $required = [bool]$paramDef.required }

            $defaultRaw = $null
            if ($paramDef.Contains('default')) { $defaultRaw = $paramDef.default }

            $validators = @()
            if ($paramDef.validators) { $validators = @($paramDef.validators) }

            while ($true) {
                $value = $null

                if ($type -eq 'int') {
                    $raw = $null
                    $defaultText = $null
                    if ($null -ne $defaultRaw) { $defaultText = [string]$defaultRaw }
                    $raw = Read-Host ("{0}{1}" -f $prompt, $(if ($defaultText) { " (default: $defaultText)" } else { "" }))
                    if ([string]::IsNullOrWhiteSpace($raw) -and $defaultText) { $raw = $defaultText }
                    $value = ConvertTo-TemplateParamInt -Value $raw
                } else {
                    $raw = Read-Host $prompt
                    if ([string]::IsNullOrWhiteSpace($raw) -and $null -ne $defaultRaw) { $raw = [string]$defaultRaw }
                    $value = $raw
                }

                $isMissingRequired = $required -and ($null -eq $value -or ([string]$value).Trim() -eq '')
                if ($isMissingRequired) {
                    Write-Host "Value is required." -ForegroundColor Yellow
                    continue
                }

                $failedValidation = $false
                foreach ($validator in $validators) {
                    $v = ([string]$validator).Trim()
                    if ([string]::IsNullOrWhiteSpace($v)) { continue }

                    if ($v -eq 'nodePort') {
                        if ($null -eq $value) { continue }
                        if (-not (Test-TemplateNodePort -Port ([int]$value) -MinPort $minNodePort -MaxPort $maxNodePort)) {
                            Write-Host "Invalid NodePort. Must be in range $minNodePort-$maxNodePort." -ForegroundColor Yellow
                            $failedValidation = $true
                            break
                        }
                        continue
                    }

                    if ($v.StartsWith('unique:', [System.StringComparison]::OrdinalIgnoreCase)) {
                        $group = $v.Substring('unique:'.Length).Trim()
                        if ([string]::IsNullOrWhiteSpace($group)) { $group = 'default' }
                        if (-not $uniqueGroups.ContainsKey($group)) { $uniqueGroups[$group] = @() }
                        if ($null -ne $value) {
                            if ($uniqueGroups[$group] -contains $value) {
                                Write-Host "Value must be unique within group '$group'." -ForegroundColor Yellow
                                $failedValidation = $true
                                break
                            }
                        }
                        continue
                    }
                }
                if ($failedValidation) { continue }

                foreach ($validator in $validators) {
                    $v = ([string]$validator).Trim()
                    if ($v.StartsWith('unique:', [System.StringComparison]::OrdinalIgnoreCase)) {
                        $group = $v.Substring('unique:'.Length).Trim()
                        if ([string]::IsNullOrWhiteSpace($group)) { $group = 'default' }
                        if (-not $uniqueGroups.ContainsKey($group)) { $uniqueGroups[$group] = @() }
                        if ($null -ne $value) { $uniqueGroups[$group] += $value }
                    }
                }

                # Support token formatting variants (robust against accidental spacing).
                $tokenValue = [string]$value
                $Tokens[$token] = $tokenValue
                if ($token -match '^\{\{(.+)\}\}$') {
                    $inner = $Matches[1]
                    $Tokens["{{ $inner }}"] = $tokenValue
                    $Tokens["{ { $inner } }"] = $tokenValue
                }
                break
            }
        }

        return $Tokens
    }

    # Determine featured task + resolve template parameter tokens
    $featuredTask = 'Info'
    $templateDef = $null
    $templateDefPath = Join-Path $templateDir 'template.yml'
    if (Test-Path $templateDefPath) {
        $templateDef = Get-Content -Path $templateDefPath -Raw | ConvertFrom-Yaml
        if ($templateDef -and $templateDef.featuredTask) {
            $featuredTask = $templateDef.featuredTask
        }
    }
    if ($templateDef -and $templateDef -is [System.Collections.IDictionary]) {
        $tokens = Resolve-TemplateParameters -TemplateDef $templateDef -Tokens $tokens
    }

    # jaxfile.yml
    Invoke-TemplateFileProcessing -Src (Join-Path $templateDir 'jaxfile.yml') -Dst (Join-Path $targetEnvPath 'jaxfile.yml') -Tokens $tokens

    # flow.yml
    Invoke-TemplateFileProcessing -Src (Join-Path $templateDir 'flow.yml') -Dst (Join-Path $targetEnvPath "flows/$DefaultFlow.yml") -Tokens $tokens

    # psakefile.ps1
    Invoke-TemplateFileProcessing -Src (Join-Path $templateDir 'psakefile.ps1') -Dst (Join-Path $targetEnvPath 'psakefile.ps1') -Tokens $tokens

    # scenario.yml (disabled)
    Invoke-TemplateFileProcessing -Src (Join-Path $templateDir 'scenario.yml') -Dst (Join-Path $targetEnvPath 'scenarios-lib/example.yml.disabled') -Tokens $tokens

    # gitkeeps
    New-Item -ItemType File -Path (Join-Path $targetEnvPath 'library/.gitkeep') -Force | Out-Null
    New-Item -ItemType File -Path (Join-Path $targetEnvPath 'scripts/.gitkeep') -Force | Out-Null

    Write-Host "Environment '$Name' initialization complete!" -ForegroundColor Green
    Write-Host ""
    Write-Host "Quick start:" -ForegroundColor Cyan
    $environmentKey = if ([string]::IsNullOrWhiteSpace($Client)) { $Name } else { "$Client/$Name" }
    Write-Host " jax -e $environmentKey/$DefaultFlow $featuredTask" -ForegroundColor Gray


    # Reload info
    # Invoke-JaxModulesReload?
}