# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.
#
# Config.ps1 — Configuration loading and management for the DevCage PowerShell
# launcher. Dot-sourced by devcage.ps1. Depends on: $ScriptDir, $ContainerEngine,
# $script:AllLangs, $script:ActiveProfiles (after Initialize-ConfigDirs sets dirs).

# ── Merge-ConfFiles: merge key=value config files (last value wins) ─
# Still used for versions.conf which keeps KEY=VALUE format.
function Merge-ConfFiles {
    param([string[]]$Files)
    $merged = [ordered]@{}
    foreach ($file in $Files) {
        if (-not (Test-Path $file)) { continue }
        foreach ($line in Get-Content $file) {
            $line = $line.Trim()
            if (-not $line -or $line.StartsWith("#")) { continue }
            $eqIdx = $line.IndexOf("=")
            if ($eqIdx -lt 1) { continue }
            $key = $line.Substring(0, $eqIdx).Trim()
            $val = $line.Substring($eqIdx + 1).Trim()
            if ($key) { $merged[$key] = $val }
        }
    }
    return $merged
}

# ── Parse-Switches: parse CLI-argument-style config files ──────────
# Each line: name value, name=value, --name value, --name=value, or bare boolean flag.
# Returns a hashtable with: .Switches (ordered dict, last-wins), .Env (array), .Volumes (array).
function Parse-Switches {
    param([string[]]$Files)
    $switches = [ordered]@{}
    $envAccum = @()
    $volAccum = @()
    foreach ($file in $Files) {
        if (-not (Test-Path $file)) { continue }
        foreach ($rawLine in Get-Content $file) {
            $line = $rawLine.Trim()
            if (-not $line -or $line.StartsWith("#")) { continue }
            # Strip optional -- or - prefix
            if ($line.StartsWith("--")) { $line = $line.Substring(2) }
            elseif ($line.StartsWith("-")) { $line = $line.Substring(1) }
            # Parse name=value or name value or bare flag
            $name = ""
            $val = ""
            $eqIdx = $line.IndexOf("=")
            if ($eqIdx -ge 1) {
                $name = $line.Substring(0, $eqIdx).Trim()
                $val = $line.Substring($eqIdx + 1).Trim()
            } else {
                $parts = $line -split '\s+', 2
                $name = $parts[0]
                if ($parts.Count -gt 1) {
                    $val = $parts[1]
                }
            }
            if (-not $name) { continue }
            # Accumulator keys
            if ($name -eq "env" -or $name -eq "e") {
                if ($val) { $envAccum += $val }
                continue
            }
            if ($name -eq "volume" -or $name -eq "V") {
                if ($val) { $volAccum += $val }
                continue
            }
            $switches[$name] = $val
        }
    }
    return @{ Switches = $switches; Env = $envAccum; Volumes = $volAccum }
}

# ── Profile directory matching ────────────────────────────────────
# Given a parent directory, find config.* directories whose profile
# set is a subset of ActiveProfiles. Returns matched dirs in loading order.
function Get-ProfileDirs {
    param([string]$ParentDir)
    if (-not $script:ActiveProfiles -or $script:ActiveProfiles.Count -eq 0) { return @() }
    $results = @()
    $configDirs = Get-ChildItem -Path $ParentDir -Directory -Filter "config.*" -ErrorAction SilentlyContinue
    foreach ($dir in $configDirs) {
        $profilePart = $dir.Name.Substring(7)  # skip "config."
        $dirProfiles = $profilePart -split '\.'
        $allMatch = $true
        $sortedKeys = ""
        foreach ($p in $dirProfiles) {
            $found = $false
            for ($idx = 0; $idx -lt $script:ActiveProfiles.Count; $idx++) {
                if ($script:ActiveProfiles[$idx] -eq $p) {
                    $found = $true
                    $sortedKeys += $idx.ToString("D4")
                    break
                }
            }
            if (-not $found) { $allMatch = $false; break }
        }
        if ($allMatch) {
            # Sort indices for canonical ordering
            $canonical = -join (($sortedKeys -split '(.{4})' | Where-Object { $_ }) | Sort-Object)
            $results += [PSCustomObject]@{
                Count = $dirProfiles.Count
                SortKey = $canonical
                Path = $dir.FullName
            }
        }
    }
    return ($results | Sort-Object Count, SortKey, Path | ForEach-Object { $_.Path })
}

# ── Collect config files across a scope (base + profiles) ────────
function Get-ScopeFiles {
    param([string]$ScopeDir, [string]$FileName)
    if (-not $ScopeDir) { return @() }    # skip if deduped
    $files = @()
    $baseFile = Join-Path $ScopeDir $FileName
    if (Test-Path $baseFile) { $files += $baseFile }
    if ($script:ActiveProfiles -and $script:ActiveProfiles.Count -gt 0) {
        $parentDir = Split-Path $ScopeDir -Parent
        foreach ($pdir in (Get-ProfileDirs -ParentDir $parentDir)) {
            $pfile = Join-Path $pdir $FileName
            if (Test-Path $pfile) { $files += $pfile }
        }
    }
    return $files
}

# ── Apply parsed switches from a Parse-Switches result ──────────────
# Helper: test if a value is boolean-true (empty/true/1/yes) or boolean-false (false/0/no)
function Test-BoolSwitch {
    param([string]$Val)
    if (-not $Val -or $Val -eq "true" -or $Val -eq "1" -or $Val -eq "yes") { return $true }
    if ($Val -eq "false" -or $Val -eq "0" -or $Val -eq "no") { return $false }
    return $true  # non-empty value = truthy
}

function Apply-Switches {
    param(
        [hashtable]$Parsed,
        [string]$SourceLabel = "switches.conf",
        [switch]$SkipWorkspace
    )
    $sw = $Parsed.Switches
    # Accumulator arrays from Parse-Switches
    foreach ($ep in $Parsed.Env) {
        $ep = $ep.Trim()
        if (-not $ep) { continue }
        if ($ep -notmatch '^[a-zA-Z_][a-zA-Z0-9_]*=') { Write-Host "Warning: ignoring invalid env entry '$ep' in $SourceLabel" -ForegroundColor Yellow; continue }
        if ($ep -match '[\x00-\x1f\x7f]') { Write-Host "Warning: ignoring env entry with control characters in $SourceLabel" -ForegroundColor Yellow; continue }
        $script:EnvVars += $ep
    }
    foreach ($ve in $Parsed.Volumes) {
        $ve = $ve.Trim()
        if ($ve) { $script:CustomVolumes += $ve }
    }
    foreach ($key in $sw.Keys) {
        $val = $sw[$key]
        # Reject values containing shell metacharacters (matches bash launcher's filter).
        # Path-type keys: normalize backslash before checking (Windows path separator).
        if ($val) {
            $valToCheck = $val
            if ($key -in @("path", "script")) { $valToCheck = $valToCheck -replace '\\', '/' }
            if ($valToCheck -match '[\x60\x24\x28\x29\x7B\x7D\x7C\x3C\x3E\x26\x21\x5C\x22\x27\x5B\x5D\x3B\x7E\x23\x0A\x0D]') {
                Write-Host "Warning: rejecting '$key' -- value contains shell metacharacters" -ForegroundColor Yellow
                continue
            }
        }
        switch ($key) {
            { $_ -eq "memory" }                     { if ($val) { $script:MemoryLimit = $val } }
            { $_ -eq "cpus" }                       { if ($val) { $script:CpuLimit = $val } }
            { $_ -eq "embed-docker" }               { if (Test-BoolSwitch $val) { $script:EmbedDocker = $true } else { $script:EmbedDocker = $false } }
            { $_ -in "no-cache-volumes", "ncv" }    { if (Test-BoolSwitch $val) { $script:NoCacheVolumes = $true } else { $script:NoCacheVolumes = $false } }
            { $_ -eq "cache-dirs" }                 { $script:CacheDirs = if ($val) { $val } else { "" } }
            { $_ -in "lang", "langs" }              { if ($val) { $script:Langs = $val } }
            { $_ -in "host-agents-config", "hac" }  { if ($val -in "ro", "rw", "no", "disabled") { $script:HostAgentsConfig = $val } elseif (Test-BoolSwitch $val) { $script:HostAgentsConfig = "rw" } }
            { $_ -eq "editable-config" }             { if (Test-BoolSwitch $val) { $script:HostAgentsConfig = "rw" } else { $script:HostAgentsConfig = "ro" } }
            { $_ -in "apt-packages", "apt" }        { if ($val) {
                $pkgOk = $true; foreach ($p in ($val -split '[,\s]+')) { if ($p -and $p -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.+\-]*(:[a-zA-Z0-9]+)?$') { Write-Host "Warning: rejecting invalid package name '$p' in apt ($SourceLabel)" -ForegroundColor Yellow; $pkgOk = $false; break } }
                if ($pkgOk) { $script:AptPackages = $val }
            } }
            { $_ -in "brew-packages", "brew" }      { if ($val) {
                $pkgOk = $true; foreach ($p in ($val -split '[,\s]+')) { if ($p -and $p -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_./-]*$') { Write-Host "Warning: rejecting invalid package name '$p' in brew ($SourceLabel)" -ForegroundColor Yellow; $pkgOk = $false; break } }
                if ($pkgOk) { $script:BrewPackages = $val }
            } }
            { $_ -in "snap-packages", "snap" }      { if ($val) {
                $pkgOk = $true; foreach ($p in ($val -split '[,\s]+')) { if ($p -and $p -notmatch '^[a-z][a-z0-9\-]*$') { Write-Host "Warning: rejecting invalid snap name '$p' in snap ($SourceLabel)" -ForegroundColor Yellow; $pkgOk = $false; break } }
                if ($pkgOk) { $script:SnapPackages = $val }
            } }
            { $_ -in "port", "ports" }               { if ($val) { $script:PortList = $val } }
            { $_ -eq "bind" }                       { if ($val -and (Test-IPv4Address $val)) { $script:BindAddr = $val; if ($val -eq "0.0.0.0") { [Console]::Error.WriteLine("WARNING: bind=0.0.0.0 in $SourceLabel will expose forwarded ports to ALL network interfaces (LAN-accessible).") } } elseif ($val) { Write-Host "Warning: invalid bind address '$val' in $SourceLabel; ignoring" -ForegroundColor Yellow } }
            { $_ -eq "dns" }                        { if ($val) { $dnsOk = $true; foreach ($dip in ($val -split ',')) { $dip = $dip.Trim(); if (-not (Test-IPv4Address $dip)) { Write-Host "Warning: invalid DNS server '$dip' in $SourceLabel; ignoring entire dns setting" -ForegroundColor Yellow; $dnsOk = $false; break } }; if ($dnsOk) { $script:DnsServers = $val } } }
            { $_ -in "script", "s" }                { if ($SkipWorkspace) { Write-Host "Warning: script in project-level config ignored (requires --yolo-config)" -ForegroundColor Yellow } elseif ($val) { $script:ExtraInitScript = $val } }
            { $_ -in "yolo-config", "yc" }          { if ($SkipWorkspace) { <# project-level cannot auto-approve its own config #> } elseif (Test-BoolSwitch $val) { $script:YoloConfig = $true } }
            { $_ -eq "skip-build" }                 { if (Test-BoolSwitch $val) { $script:SkipBuild = $true } }
            { $_ -eq "gpu" }                        { if ($val -eq "nvidia" -or $val -eq "amd") { $script:GpuType = $val; $script:NoGpu = $false } elseif ($val -eq "false" -or $val -eq "0" -or $val -eq "no") { $script:NoGpu = $true } elseif (-not $val -or $val -eq "true" -or $val -eq "1" -or $val -eq "yes" -or $val -eq "auto") { $script:GpuType = "auto"; $script:NoGpu = $false } }
            { $_ -eq "no-gpu" }                     { if (Test-BoolSwitch $val) { $script:NoGpu = $true } }
            { $_ -in "git-auth", "git" }            { if (Test-BoolSwitch $val) { $script:GitAuth = $true } else { $script:GitAuth = $false } }
            { $_ -eq "verbose" }                    { if (Test-BoolSwitch $val) { $script:Verbose = $true } }
            { $_ -eq "skip-sandbox-warning" }       { <# Linux-only; silently ignored on Windows #> }
            { $_ -in "profiles", "profile", "p" }   { if (-not $SkipWorkspace -and $val) { $script:ProfilesFromConf = $val } }
            { $_ -eq "path" }                       { if ($SkipWorkspace) { Write-Host "Warning: path in project-level switches.conf is ignored (circular)" -ForegroundColor Yellow } elseif ($val) { $script:WorkspacePath = $val } }
            { $_ -eq "browse" }                     { if (Test-BoolSwitch $val) { $script:BrowseForWorkspace = $true } }
            { $_ -eq "no-reuse" }                   { if (Test-BoolSwitch $val) { $script:NoReuse = $true } else { $script:NoReuse = $false } }
            { $_ -eq "idle-timeout" }               { if ($val -match '^\d+$') { $script:IdleTimeout = [int]$val } else { Write-Host "Warning: ignoring invalid idle-timeout value '$val' in $SourceLabel" -ForegroundColor Yellow } }
            { $_ -eq "log-keep" }                   { if ($val -match '^\d+$') { $script:LogKeep = [int]$val } }
            { $_ -eq "agent" }                      { if (-not $script:LaunchAgent) { $script:LaunchAgent = $val } }
            { $_ -eq "agent-args" }                 { <# GUI-only; launcher gets agent-args as positional CLI args #> }
            default                                 { Write-Host "Warning: unknown key '$key' in $SourceLabel" -ForegroundColor Yellow }
        }
    }
}

# ── Config directory initialization ──────────────────────────────
# Sets $script:GlobalConfigDir, $script:UserConfigDir, and dedup real paths.
# Must be called before any config loading.
function Initialize-ConfigDirs {
    $script:GlobalConfigDir = Join-Path $ScriptDir "config"
    $script:UserConfigDir   = Join-Path (Join-Path $HOME ".devcage") "config"
    $script:GlobalReal = ""
    $script:UserReal = ""
    if (Test-Path $script:GlobalConfigDir) {
        $script:GlobalReal = (Resolve-Path $script:GlobalConfigDir).Path
    }
    if (Test-Path $script:UserConfigDir) {
        $script:UserReal = (Resolve-Path $script:UserConfigDir).Path
    }
    # Skip user config if it resolves to the same physical dir as global
    if ($script:GlobalReal -and ($script:GlobalReal -eq $script:UserReal)) {
        $script:UserConfigDir = ""
        $script:UserReal = ""
    }
}

# ── Pass 1: load global + user switches.conf ─────────────────────
# Must be called after Initialize-ConfigDirs and before CLI arg parsing.
# Sets WORKSPACE_PATH and other early defaults from switches.conf.
function Invoke-DiscoverConfig {
    $defaultsFirstPass = @()
    $script:_DefaultsFirstPass = @()
    $globalDefaults = Join-Path $GlobalConfigDir "switches.conf"
    $userDefaults   = if ($UserConfigDir) { Join-Path $UserConfigDir "switches.conf" } else { "" }
    if (Test-Path $globalDefaults) { $defaultsFirstPass += $globalDefaults }
    if ($userDefaults -and (Test-Path $userDefaults)) { $defaultsFirstPass += $userDefaults }
    if ($defaultsFirstPass.Count -gt 0) {
        $parsed = Parse-Switches -Files $defaultsFirstPass
        Apply-Switches -Parsed $parsed -SourceLabel "switches.conf (global+user)"
    }
    # Save list for Pass 2 dedup
    $script:_DefaultsFirstPass = $defaultsFirstPass
}

# ── Resolve active profiles ───────────────────────────────────────
# CLI --profile/--profiles overrides PROFILES from switches.conf.
# Sets $script:ActiveProfiles (deduplicated).
function Resolve-ActiveProfiles {
    if ($ProfileList.Count -gt 0) {
        $script:ActiveProfiles = $ProfileList
    } elseif ($ProfilesFromConf) {
        $script:ActiveProfiles = @()
        foreach ($pn in ($ProfilesFromConf -split ',')) {
            $pn = $pn.Trim()
            if ($pn -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
                Write-Host "Error: invalid profile name '$pn' in PROFILES config" -ForegroundColor Red
                exit 1
            }
            $script:ActiveProfiles += $pn
        }
    }
    # Deduplicate while preserving order
    if ($script:ActiveProfiles.Count -gt 0) {
        $deduped = @()
        $seen = @{}
        foreach ($p in $script:ActiveProfiles) {
            if (-not $seen.ContainsKey($p)) {
                $deduped += $p
                $seen[$p] = $true
            }
        }
        $script:ActiveProfiles = $deduped
        Write-Info "Active profiles: $($script:ActiveProfiles -join ',')"
    }
}

# ── Workspace validation and resolution ──────────────────────────
# Resolves $WorkspacePath or cwd, sets $script:Cwd and $env:WORKSPACE.
# Also handles GPU auto-detection and self-directory warning.
function Resolve-Workspace {
    if ($WorkspacePath) {
        $script:WorkspacePath = Normalize-WorkspacePath $WorkspacePath
        $script:Cwd = $script:WorkspacePath
    } else {
        $script:Cwd = (Get-Location).Path
        if (-not (Test-Path $script:Cwd)) {
            Write-Host "Error: Current directory '$($script:Cwd)' is not accessible." -ForegroundColor Red
            exit 1
        }
    }

    if (-not (Test-Path $ComposeFile)) {
        Write-Host "Error: Compose file not found at $ComposeFile" -ForegroundColor Red
        exit 1
    }

    # Auto-skip DinD sidecar when using Podman (Docker-specific image)
    if ($ContainerEngine -eq "podman" -and $EmbedDocker) {
        Write-Info "Note: Podman detected - auto-skipping Docker-in-Docker sidecar."
        $script:EmbedDocker = $false
    }

    # GPU auto-detection
    if (-not $NoGpu -and (-not $GpuType -or $GpuType -eq "auto")) {
        if ($ContainerEngine -eq "docker") {
            try {
                $runtimes = & $ContainerEngine info --format '{{json .Runtimes}}' 2>$null
                if ($runtimes -match '"nvidia"') {
                    $script:GpuType = "nvidia"
                } elseif ($runtimes -match '"amd"') {
                    $script:GpuType = "amd"
                }
            } catch {}
        } elseif ($ContainerEngine -eq "podman") {
            if (Get-Command nvidia-ctk -ErrorAction SilentlyContinue) {
                $script:GpuType = "nvidia"
            } elseif (Test-Path "/dev/kfd") {
                $script:GpuType = "amd"
            }
        }
    }
    # --gpu without a type triggers auto-detect; clear "auto" if nothing found
    if ($GpuType -eq "auto") {
        Write-Warning "--gpu requested but no GPU runtime detected - proceeding without GPU"
        $script:GpuType = ""
    }

    $env:WORKSPACE = $script:Cwd

    # Self-directory detection: warn if running from DevCage source dir with no command.
    # Suppressed when a command is given — the user knows what they're running.
    if (-not $WorkspacePath -and $ContainerArgs.Count -eq 0 -and $script:Cwd.TrimEnd('\') -ieq $ScriptDir.TrimEnd('\')) {
        Write-Info ""
        Write-Warning "You are running DevCage from its own directory."
        Write-Info "  $($script:Cwd)"
        Write-Info ""
        Write-Info "This will mount the DevCage source tree as /workspace in the container."
        Write-Info "You probably want to specify a project directory instead."
        Write-Info ""
        Write-Info "Tip: devcage --path ~\my-project [COMMAND]"
        Write-Info ""
        # Interactive prompt only when both stdio are console and not quiet
        if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected -and -not [Console]::IsOutputRedirected -and -not $script:Quiet) {
            while ($true) {
                $userPath = Read-Host "Enter project path (or press Enter to continue anyway)"
                [Console]::Error.WriteLine("")
                if (-not $userPath) { break }
                if (-not [System.IO.Path]::IsPathRooted($userPath)) {
                    $userPath = Join-Path $ScriptDir $userPath
                }
                $userPath = [System.IO.Path]::GetFullPath($userPath)
                if (-not (Test-Path $userPath -PathType Container)) {
                    [Console]::Error.WriteLine("  '$userPath' does not exist or is not a directory. Try again.")
                    continue
                }
                $script:Cwd = $userPath
                $env:WORKSPACE = $script:Cwd
                Write-Info "Using workspace: $($script:Cwd)"
                break
            }
        }
    }
}

# ── Compute workspace slug and container name ─────────────────────
# Sets $script:WorkspaceSlug, $env:WORKSPACE_SLUG, $env:COMPOSE_PROJECT_NAME,
# and $script:ContainerName.
function Compute-WorkspaceSlug {
    $script:WorkspaceSlug = (Split-Path $Cwd -Leaf).ToLower() -replace '[^a-z0-9-]', '-' -replace '^-+|-+$', ''
    if (-not $script:WorkspaceSlug) { $script:WorkspaceSlug = "default" }
    if ($script:WorkspaceSlug.Length -gt 54) { $script:WorkspaceSlug = $script:WorkspaceSlug.Substring(0, 54) }
    $wsSha = [System.Security.Cryptography.SHA256]::Create()
    try {
        $wsHash = ($wsSha.ComputeHash(
            [System.Text.Encoding]::UTF8.GetBytes($Cwd)) |
            ForEach-Object { $_.ToString("x2") }) -join ''
    } finally {
        $wsSha.Dispose()
    }
    $wsHash = $wsHash.Substring(0, 8)
    $script:WorkspaceSlug = "$($script:WorkspaceSlug)-${wsHash}"
    $env:WORKSPACE_SLUG       = $script:WorkspaceSlug
    $env:COMPOSE_PROJECT_NAME = "devcage_$($script:WorkspaceSlug)"

    # Container name for reuse mode
    $homePath = [Environment]::GetFolderPath("UserProfile")
    $relPath = $Cwd
    if ($relPath.StartsWith($homePath, [StringComparison]::OrdinalIgnoreCase)) {
        $relPath = $relPath.Substring($homePath.Length).TrimStart('\', '/')
    }
    $containerSlug = $relPath.ToLower() -replace '^([a-z]):', '$1' -replace '[/\\]', '-' -replace '[^a-z0-9-]', '_' -replace '-{2,}', '-' -replace '^[-_]', '' -replace '[-_]$', ''
    if ($containerSlug.Length -gt 55) {
        $containerSlug = $containerSlug.Substring($containerSlug.Length - 55)
        $containerSlug = $containerSlug -replace '^[-_]', ''
    }
    $script:ContainerName = "devcage-${containerSlug}"
}

# ── Set project config dir (after workspace is resolved) ─────────
# Sets $script:ProjectConfigDir (empty if deduped with global or user).
function Set-ProjectConfigDir {
    $script:ProjectConfigDir = Join-Path (Join-Path $Cwd ".devcage") "config"
    $projectReal = ""
    if (Test-Path $script:ProjectConfigDir) {
        $projectReal = (Resolve-Path $script:ProjectConfigDir).Path
    }
    if ($projectReal) {
        if (($projectReal -eq $GlobalReal) -or ($projectReal -eq $UserReal)) {
            $script:ProjectConfigDir = ""
        }
    }
}

# ── Pass 2: merge project-level + profile switches.conf ──────────
# Re-merges with project level and profiles. CLI-added env/volume entries
# are restored after the re-merge so CLI always overrides config.
function Invoke-FullConfigApply {
    # Save CLI-added accumulators (--env, --volume) before resetting.
    $cliEnvVars = $EnvVars
    $cliCustomVolumes = $CustomVolumes
    $script:EnvVars = @()
    $script:CustomVolumes = @()
    # Build second-pass file list, skipping global/user base files already
    # processed in the first pass (prevents config from overriding CLI scalar flags).
    $defaultsFullPass = @()
    foreach ($scopeDir in @($GlobalConfigDir, $UserConfigDir)) {
        if (-not $scopeDir) { continue }
        foreach ($f in (Get-ScopeFiles -ScopeDir $scopeDir -FileName "switches.conf")) {
            if ($f -notin $script:_DefaultsFirstPass) {
                $defaultsFullPass += $f
            }
        }
    }
    if ($ProjectConfigDir) {
        $defaultsFullPass += Get-ScopeFiles -ScopeDir $ProjectConfigDir -FileName "switches.conf"
    }
    if ($defaultsFullPass.Count -gt 0) {
        $parsed = Parse-Switches -Files $defaultsFullPass
        Apply-Switches -Parsed $parsed -SourceLabel "switches.conf (merged)" -SkipWorkspace
    }
    # Re-append CLI-added env/volume entries (CLI overrides all config levels)
    $script:EnvVars += $cliEnvVars
    $script:CustomVolumes += $cliCustomVolumes
}

# ── Merge versions.conf from all config levels ───────────────────
# Sets $script:VersionBuildArgs, $script:versionsFiles, $script:MergedVersionsContent.
function Load-Versions {
    $script:versionsFiles = @()
    $script:versionsFiles += Get-ScopeFiles -ScopeDir $GlobalConfigDir -FileName "versions.conf"
    if ($UserConfigDir) {
        $script:versionsFiles += Get-ScopeFiles -ScopeDir $UserConfigDir -FileName "versions.conf"
    }
    if ($ProjectConfigDir) {
        $script:versionsFiles += Get-ScopeFiles -ScopeDir $ProjectConfigDir -FileName "versions.conf"
    }
    $script:VersionBuildArgs = @()
    $script:MergedVersionsContent = ""
    if ($script:versionsFiles.Count -gt 0) {
        $mergedVersions = Merge-ConfFiles -Files $script:versionsFiles
        $versionLines = @()
        foreach ($key in $mergedVersions.Keys) {
            $val = $mergedVersions[$key]
            # Reject legacy DEV_* flags — use --lang instead
            if ($key -match '^DEV_') {
                Write-Host "Warning: '${key}' in versions.conf is no longer supported -- use --lang flag instead" -ForegroundColor Yellow
                continue
            }
            if ($val) {
                if ($val -match '^[a-zA-Z0-9._+:@/-]+$') {
                    # DIND_VERSION is runtime-only (not a Dockerfile ARG)
                    if ($key -eq "DIND_VERSION") {
                        $env:DIND_IMAGE = "docker:${val}-dind"
                    } else {
                        $script:VersionBuildArgs += @("--build-arg", "${key}=${val}")
                    }
                    $versionLines += "${key}=${val}"
                } else {
                    Write-Host "Warning: ignoring versions.conf entry '${key}' -- value contains invalid characters" -ForegroundColor Yellow
                }
            }
        }
        $script:MergedVersionsContent = $versionLines -join "`n"
    }
    # Inject --lang build args
    if ($Langs) {
        $script:VersionBuildArgs += Get-LangBuildArgs $Langs
    }
}

# ── Resolve image name ────────────────────────────────────────────
# Uses workspace-specific image if project has build-time version overrides.
# Sets $env:DEVCAGE_IMAGE (if needed).
function Resolve-ImageName {
    if ($env:DEVCAGE_IMAGE) { return }  # already set externally
    $BuildTimeKeys = @(
        'UBUNTU_TAG',
        'NODE_VERSION','JDK_VERSION','MAVEN_VERSION','GRADLE_VERSION','GO_VERSION','RUST_VERSION','DOTNET_VERSION',
        'CMAKE_VERSION','NINJA_VERSION','CCACHE_VERSION','YQ_VERSION','SHFMT_VERSION','SHELLHARDEN_VERSION',
        'PLAYWRIGHT_VERSION','PLAYWRIGHT_MCP_VERSION'
    )
    $hasBuildTimeOverride = $false
    # Check base project versions.conf and all project-level profile versions.conf
    if ($ProjectConfigDir) {
        foreach ($pvf in (Get-ScopeFiles -ScopeDir $ProjectConfigDir -FileName "versions.conf")) {
            $projectLines = Get-Content $pvf -ErrorAction SilentlyContinue |
                Where-Object { $_ -match '^\s*[^#]' }
            foreach ($line in $projectLines) {
                $lineKey = ($line -split '=', 2)[0].Trim()
                if ($BuildTimeKeys -contains $lineKey) {
                    $hasBuildTimeOverride = $true
                    break
                }
            }
            if ($hasBuildTimeOverride) { break }
        }
    }
    # Check project switches.conf for lang/langs override (affects build image)
    if ($ProjectConfigDir -and -not $hasBuildTimeOverride) {
        foreach ($swf in (Get-ScopeFiles -ScopeDir $ProjectConfigDir -FileName "switches.conf")) {
            $swLines = Get-Content $swf -ErrorAction SilentlyContinue |
                Where-Object { $_ -match '(?i)^\s*(lang|langs)\b' }
            if ($swLines) {
                $hasBuildTimeOverride = $true
                break
            }
        }
    }
    if ($hasBuildTimeOverride) {
        $env:DEVCAGE_IMAGE = "devcage-workspace-$WorkspaceSlug"
        Write-Info "Project has build-time version overrides -> using workspace-specific image: $($env:DEVCAGE_IMAGE)"
    }
    # Otherwise DEVCAGE_IMAGE stays unset -> docker-compose.yml defaults to "devcage-workspace"
}
