src/functions/qconf.ps1

function Invoke-QConf {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        $command,

        [ArgumentCompleter({
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                try {
                    $mapPath = if ($fakeBoundParameters.map) { $fakeBoundParameters.map } else { "./.configuration.map.ps1" }
                    $localMapExists = Test-Path $mapPath

                    $map = Import-ConfigMap -Map $fakeBoundParameters.map -Fallback ".configuration.map.ps1" -ErrorAction Ignore | Assert-ConfigMap

                    $completions = Get-EntryCompletion $map -language "conf" @PSBoundParameters
                    # Include !init if no local map file exists
                    if (!$localMapExists) {
                        $completions = @("!init" | ? { $_.startswith($wordToComplete) }) + $completions
                    }
                    return $completions
                }
                catch {
                    return "ERROR [-entry]: $($_.Exception.Message) $($_.ScriptStackTrace)"
                }
            })]
        $entry = $null,

        [ArgumentCompleter({
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                try {
                    if ($fakeBoundParameters.command -in @("init", "help")) {
                        return @()
                    }

                    $map = Import-ConfigMap -Map $fakeBoundParameters.map -Fallback ".configuration.map.ps1" | Assert-ConfigMap
                    $entryName = if ($fakeBoundParameters.entry) {
                        [string]$fakeBoundParameters.entry
                    }
                    elseif ($fakeBoundParameters.command -notin @('set', 'get', 'show', 'list', 'help', 'init', '!init', '!complete')) {
                        [string]$fakeBoundParameters.command
                    }
                    $entryValue = Get-MapEntry $map $entryName -language conf
                    if (!$entryValue) {
                        throw "entry '$entryName' not found"
                    }
                    $options = Get-CompletionList $entryValue -listKey "options" -language "conf" -maxDepth 1
                    return $options.Keys | ? { $_.startswith($wordToComplete) }
                }
                catch {
                    return "ERROR [-value]: $($_.Exception.Message) $($_.ScriptStackTrace)"
                }
            })]
        [string]$value = $null,

        [Parameter(Position = 1, DontShow)]
        [ArgumentCompleter({
                param ($commandName, $parameterName, $wordToComplete)
                @('get', 'set', 'show') | Where-Object { $_.StartsWith($wordToComplete, [StringComparison]::OrdinalIgnoreCase) }
            })]
        [string]$action,

        [Parameter(Position = 2, DontShow)]
        [ArgumentCompleter({
                param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                try {
                    if ($fakeBoundParameters.action -ne 'set') { return @() }
                    $verbs = @('set', 'get', 'show', 'list', 'help', 'init', '!init', '!complete')
                    $entryName = if ($fakeBoundParameters.entry) {
                        [string]$fakeBoundParameters.entry
                    }
                    elseif ($fakeBoundParameters.command -notin $verbs) {
                        [string]$fakeBoundParameters.command
                    }
                    if (-not $entryName) { return @() }

                    $map = Import-ConfigMap -Map $fakeBoundParameters.map -Fallback ".configuration.map.ps1" | Assert-ConfigMap
                    $entryValue = Get-MapEntry $map $entryName -language conf
                    if (-not $entryValue) { return @() }
                    $options = Get-CompletionList $entryValue -listKey "options" -language "conf" -maxDepth 1
                    return @($options.Keys | Where-Object { "$_".StartsWith($wordToComplete, [StringComparison]::OrdinalIgnoreCase) })
                }
                catch {
                    return @()
                }
            })]
        [string]$option,

        [Parameter(DontShow)]
        [string]$Word,
        [Parameter(DontShow)]
        [switch]$Params,

        $map = "./.configuration.map.ps1",

        [Parameter(ValueFromRemainingArguments = $true, DontShow = $true)]
        [string[]]$RemainingArguments
    )

    dynamicparam {
        try {
            $verbs = @('set', 'get', 'show', 'list', 'help', 'init', '!init', '!complete')
            $dynamicEntry = if ($entry) {
                $entry
            }
            elseif ($command -and $command -notin $verbs) {
                [string]$command
            }
            else {
                $null
            }
            if (-not $dynamicEntry) { return }

            $map = Import-ConfigMap -Map $map -Fallback ".configuration.map.ps1" | Assert-ConfigMap
            $dynamicOperation = if ($command -eq 'set' -or $action -eq 'set' -or $PSBoundParameters.ContainsKey('value')) { 'set' } else { 'get' }
            $skip = if ($dynamicOperation -eq 'set') { 3 } else { 0 }

            return Get-EntryDynamicParam $map "$dynamicEntry.$dynamicOperation" -skip $skip -bound $PSBoundParameters
        }
        catch {
            return "ERROR [dynamic]: $($_.Exception.Message) $($_.ScriptStackTrace)"
        }
    }

    process {
        $verbs = @('set', 'get', 'show', 'list', 'help', 'init', '!init', '!complete')
        $verb = $null
        $confEntry = $entry

        if ($command -in $verbs) {
            $verb = [string]$command
        }
        elseif ($command) {
            if (-not $confEntry) {
                $confEntry = [string]$command
            }
        }

        if ($verb -in @('get', 'set', 'show', 'list') -and -not $confEntry -and $action) {
            $confEntry = $action
            if ($verb -eq 'set' -and -not $PSBoundParameters.ContainsKey('value') -and $option) {
                $value = $option
            }
        }
        elseif (-not $verb -and $confEntry -and $action -in @('get', 'set', 'show')) {
            $verb = $action
            if ($verb -eq 'set' -and -not $PSBoundParameters.ContainsKey('value') -and $option) {
                $value = $option
            }
        }

        $operationArgs = @($RemainingArguments | Where-Object { $null -ne $_ -and "$_" -ne '' })
        if (-not $verb -and $confEntry -and $operationArgs.Count -gt 0) {
            switch ($operationArgs[0]) {
                'get' {
                    $verb = 'get'
                    $operationArgs = @($operationArgs | Select-Object -Skip 1)
                }
                'set' {
                    $verb = 'set'
                    if (-not $PSBoundParameters.ContainsKey('value') -and $operationArgs.Count -gt 1) {
                        $value = $operationArgs[1]
                    }
                    $operationArgs = @($operationArgs | Select-Object -Skip 2)
                }
                'show' {
                    $verb = 'show'
                    $operationArgs = @($operationArgs | Select-Object -Skip 1)
                }
            }
        }

        if (-not $verb -and $confEntry) {
            $verb = if ($PSBoundParameters.ContainsKey('value')) { 'set' } else { 'get' }
        }

        if ($verb -eq "help") {
            Write-Host "QCONF"
            Write-Host "A command line tool to manage configuration maps"
            Write-Host ""
            Write-Host "Usage:"
            Write-Host "qconf <entry> get"
            Write-Host "qconf <entry> set <option>"
            Write-Host "qconf <entry> show"
            Write-Host "qconf <entry> [-value <option>] # shorthand"
            Write-Host "qconf list [<entry>]"
            return
        }
        if ($verb -in @('init', '!init') -or $confEntry -eq "!init") {
            # Only check current directory - create new map regardless of parent configs
            if (Test-Path $map) {
                throw "map file '$map' already exists"
            }
            Initialize-ConfigMap -file $map
            return
        }

        if ($verb -eq '!complete') {
            $completeMap = Import-ConfigMap -Map $map -Fallback ".configuration.map.ps1" -ErrorAction Ignore
            if (-not $completeMap) {
                $global:LASTEXITCODE = 2
                return
            }

            $mode = if ($Params) { 'params' } elseif ($PSBoundParameters.ContainsKey('Word')) { 'word' } else { $null }
            $completeEntry = $confEntry
            $tokens = @($RemainingArguments | Where-Object { $null -ne $_ -and "$_" -ne '' })
            for ($index = 0; $index -lt $tokens.Count; $index++) {
                switch ($tokens[$index]) {
                    '-params' { $mode = 'params' }
                    '-word' {
                        if ($index + 1 -lt $tokens.Count) {
                            $Word = $tokens[++$index]
                            $mode = 'word'
                        }
                    }
                    '-entry' {
                        if ($index + 1 -lt $tokens.Count) {
                            $completeEntry = $tokens[++$index]
                        }
                    }
                    default {
                        if (-not $completeEntry) { $completeEntry = $tokens[$index] }
                    }
                }
            }

            if ($mode -eq 'params' -and $completeEntry) {
                $names = [System.Collections.Generic.List[string]]::new()
                $names.Add('value') | Out-Null
                $dynamic = Get-EntryDynamicParam $completeMap "$completeEntry.set" -skip 3 -bound @{}
                if ($dynamic) {
                    foreach ($name in @($dynamic.Keys)) {
                        if (-not $names.Contains($name)) { $names.Add($name) | Out-Null }
                    }
                }
                $names | ForEach-Object { $_ }
                $global:LASTEXITCODE = 0
                return
            }

            if ($mode -eq 'word') {
                $prefix = if ($null -eq $Word) { '' } else { [string]$Word }
                if ($completeEntry) {
                    if ($tokens.Count -eq 0) {
                        @('get', 'set', 'show') |
                            Where-Object { $_.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) }
                    }
                    elseif ($tokens[0] -eq 'set') {
                        $entryValue = Get-MapEntry $completeMap $completeEntry -language conf
                        if ($entryValue) {
                            $options = Get-CompletionList $entryValue -listKey 'options' -language conf -maxDepth 1
                            $options.Keys |
                                Where-Object { "$_".StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) }
                        }
                    }
                }
                else {
                    Get-EntryCompletion -map $completeMap -language conf -wordToComplete $prefix
                }
                $global:LASTEXITCODE = 0
                return
            }

            $global:LASTEXITCODE = 2
            return
        }

        $map = Import-ConfigMap -Map $map | Assert-ConfigMap

        if ($verb -in @('list', 'show')) {
            $listEntry = if ($confEntry) {
                $confEntry
            }
            else {
                @($RemainingArguments | Where-Object { $_ } | Select-Object -First 1)
            }

            if (-not $listEntry -or @($listEntry).Count -eq 0) {
                if ($verb -eq 'show') {
                    throw "The show command requires an entry. Example: qconf <entry> show"
                }
                Write-MapHelp -map $map -invocation $MyInvocation -language "conf"
                return
            }

            $entryName = [string]@($listEntry)[0]
            $operationContext = New-ConfigMapOperationContext
            $subEntry = Get-MapEntry $map $entryName -language "conf" -OperationContext $operationContext
            if (-not $subEntry) {
                throw "Entry '$entryName' not found. Run 'qconf list' to see all available entries."
            }

            Invoke-WithEntrySettings -Map $map -EntryKey $entryName -Entry $subEntry -OperationContext $operationContext -ScriptBlock {
                $options = Get-CompletionList $subEntry -listKey "options" -language "conf" -maxDepth 1
                $current = Invoke-Get $subEntry -bound @{ options = $options }
                $result = ConvertTo-MapResult $current $entryName $subEntry $options
                Write-QConfStatus `
                    -EntryName $entryName `
                    -Result $result `
                    -Description $subEntry.description `
                    -HasValidation ([bool]$subEntry.validate)
            }
            return
        }

        if (-not $verb) {
            Write-MapHelp -map $map -invocation $MyInvocation -language "conf"
            return
        }

        Write-Verbose "entry=$confEntry command=$verb"
        $operationContext = New-ConfigMapOperationContext

        switch ($verb) {
            "set" {
                if (-not $confEntry) {
                    throw "The set command requires an entry."
                }
                $subEntry = Get-MapEntry $map $confEntry -language "conf" -OperationContext $operationContext
                if (!$subEntry) {
                    throw "Entry '$confEntry' not found. Run 'qconf list' to see all available entries."
                }

                $optionKey = $value
                $options = Get-CompletionList $subEntry -listKey "options" -language "conf" -maxDepth 1
                if ([string]::IsNullOrWhiteSpace($optionKey) -or -not $options.Contains($optionKey)) {
                    throw "Unknown option '$optionKey' for entry '$confEntry'. Available options: $($options.Keys -join ', ')."
                }
                $optionValue = $options[$optionKey]

                $bound = @{}
                foreach ($boundKey in $PSBoundParameters.Keys) {
                    $bound[$boundKey] = $PSBoundParameters[$boundKey]
                }
                $bound.key = $optionKey
                $bound.value = $optionValue
                Invoke-WithEntrySettings -Map $map -EntryKey "$confEntry" -Entry $subEntry -OperationContext $operationContext -ScriptBlock {
                    Invoke-Set $subEntry -bound $bound
                }
            }
            "get" {
                $entries = $confEntry
                if (!$entries) {
                    # not passing -listKey "options" here, as we don't want to expand options - we just need top-level keys
                    $entries = (Get-CompletionList $map -language "conf").Keys
                }

                foreach ($entryName in @($entries)) {
                    $subEntry = Get-MapEntry $map $entryName -language "conf" -OperationContext $operationContext
                    $bound = @{}
                    foreach ($boundKey in $PSBoundParameters.Keys) {
                        $bound[$boundKey] = $PSBoundParameters[$boundKey]
                    }

                    Invoke-WithEntrySettings -Map $map -EntryKey "$entryName" -Entry $subEntry -OperationContext $operationContext -ScriptBlock {
                        try {
                            if (!$subEntry) {
                                throw "Entry '$entryName' not found. Run 'qconf list' to see all available entries."
                            }

                            $options = Get-CompletionList $subEntry -listKey "options" -language "conf" -maxDepth 1
                            $bound.options = $options

                            $value = Invoke-Get $subEntry -bound $bound

                            $result = ConvertTo-MapResult $value $entryName $subEntry $options
                            $result | Write-Output
                        }
                        catch {
                            if ((Get-ConfigMapSetting -Name Debug) -eq '1') {
                                throw $_
                            }
                            Write-Error "Error getting value for entry '$entryName': $($_.Exception.Message)"
                        }
                    }
                }
            }
            default {
                throw "command '$verb' not supported"
            }
        }

    }
}

function Write-QConfStatus {
    param(
        [Parameter(Mandatory = $true)]
        [string]$EntryName,
        [Parameter(Mandatory = $true)]
        $Result,
        [string]$Description,
        [bool]$HasValidation
    )

    Write-Host ""
    Write-Host " " -NoNewline
    Write-Host $EntryName -NoNewline -ForegroundColor Green
    if ($Description) {
        Write-Host " $Description" -ForegroundColor White
    }
    else {
        Write-Host ""
    }

    $active = @($Result.Active) -join ', '
    if (-not $active) { $active = '<none>' }

    $value = if ($Result.Value -is [System.Collections.IDictionary] -or $Result.Value -is [array]) {
        $Result.Value | ConvertTo-Json -Compress -Depth 4
    }
    elseif ($null -eq $Result.Value) {
        '<none>'
    }
    else {
        [string]$Result.Value
    }

    $rows = [System.Collections.Generic.List[object]]::new()
    $rows.Add([pscustomobject]@{
            Label = 'Active'
            Value = $active
            Glyph = if ($active -eq '<none>') { [char]0x25CB } else { [char]0x25CF }
            Color = if ($active -eq '<none>') { 'Yellow' } else { 'Green' }
        })
    $rows.Add([pscustomobject]@{
            Label = 'Value'
            Value = $value
            Glyph = ' '
            Color = 'White'
        })
    $rows.Add([pscustomobject]@{
            Label = 'Options'
            Value = @($Result.Options) -join ', '
            Glyph = ' '
            Color = 'White'
        })

    if ($HasValidation) {
        $rows.Add([pscustomobject]@{
                Label = 'Valid'
                Value = if ($Result.IsValid -eq $true) { 'yes' } elseif ($Result.IsValid -eq $false) { 'no' } else { 'unknown' }
                Glyph = if ($Result.IsValid -eq $true) { [char]0x2713 } elseif ($Result.IsValid -eq $false) { [char]0x2717 } else { [char]0x25CB }
                Color = if ($Result.IsValid -eq $true) { 'Green' } elseif ($Result.IsValid -eq $false) { 'Red' } else { 'Yellow' }
            })
    }

    for ($index = 0; $index -lt $rows.Count; $index++) {
        $row = $rows[$index]
        $branch = if ($index -eq $rows.Count - 1) { [char]0x2514 } else { [char]0x251C }
        Write-Host " $branch─ " -NoNewline -ForegroundColor DarkGray
        Write-Host "$($row.Glyph) " -NoNewline -ForegroundColor $row.Color
        Write-Host ("{0,-8}" -f $row.Label) -NoNewline -ForegroundColor DarkGray
        Write-Host $row.Value -ForegroundColor $row.Color
    }
    Write-Host ""
}

function ConvertTo-MapResult($value, $entryName, $entry, $options, $validate = $true) {
    $result = $null
    if ($value -is [Hashtable]) {
        $hash = @{
            Path = "$entryName/$subPath"
        }
        $hash += $value

        $result = $hash
    }
    else {
        $result = @{
            Path  = "$entryName/$subPath"
            Value = $value
        }
    }

    if (!$result.Active) {
        $result.Active = $options.keys | where { $options.$_ -eq $value }
    }
    $result.Options = $options.keys

    $isvalid = "?"
    if ($validate -and $entry.validate) {
        if (!$result.Active) {
            Write-Warning "no active option found for $entryName/$subPath"
            $isvalid = $null
        }
        else {
            $optionvalue = $options.$($result.Active)
            $isvalid = Invoke-EntryCommand $entry validate -ordered @($path, $optionvalue, $result.Active)
        }
    }

    $result = [PSCustomObject]@{
        Path    = $result.Path
        Value   = $result.Value
        Active  = $result.Active
        Options = $result.Options
        IsValid = $isvalid
    }

    return $result
}

Set-Alias -Name "qconf" -Value "Invoke-QConf" -Force