Functions/GenXdev.AI/Invoke-AILLMSettingsPrompt.ps1

###############################################################################
<#
.SYNOPSIS
Launches an interactive Spectre.Console prompt to configure LLM settings.

.DESCRIPTION
Provides an interactive terminal UI using Spectre.Console SelectionPrompt
(arrow keys + mouse) to guide the user through LLM provider configuration.
The user can pick a default provider (requiring only an API key) or enter all
settings manually. API keys are stored per provider family (e.g. "DeepSeek")
with user-given names like "Work" or "Personal", and are reused across query
types.

When -LLMQueryType is omitted, a type-selection menu is shown first with an
"ALL" option to configure all query types at once or in sequence.

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.PARAMETER LLMQueryType
The type of LLM query to configure. Valid values are SimpleIntelligence,
Knowledge, Pictures, TextTranslation, Coding, and ToolUse. If omitted, an
interactive type-selection menu is displayed.

.PARAMETER SessionOnly
Use alternative settings stored in session for AI preferences.

.PARAMETER ClearSession
Clear the session setting (Global variable) before saving.

.PARAMETER SkipSession
When specified, skips session settings and writes only to persistent
preferences or defaults.

.PARAMETER AllMachines
Also write LLM settings to OneDrive for syncing across multiple machines.

.PARAMETER PreferencesDatabasePath
Database path for preference data files.

.EXAMPLE
Invoke-AILLMSettingsPrompt -LLMQueryType "ToolUse"

Launches interactive setup for the ToolUse query type.

.EXAMPLE
Invoke-AILLMSettingsPrompt

Shows a type-selection menu, then launches interactive setup.

.EXAMPLE
Invoke-AILLMSettingsPrompt "Coding" -PreferencesDatabasePath "C:\custom\prefs"

Launches interactive setup for Coding with a custom preferences database.
#>

###############################################################################
function Invoke-AILLMSettingsPrompt {

    [CmdletBinding(SupportsShouldProcess)]
    [Alias('llmset', 'promptllmsettings')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseDeclaredVarsMoreThanAssignments', '')]

    param(
        #######################################################################
        [Parameter(
            Position = 0,
            Mandatory = $false,
            HelpMessage = 'The type of LLM query to configure (menu shown if ' +
                'omitted)'
        )]
        [ValidateSet(
            'SimpleIntelligence',
            'Knowledge',
            'Pictures',
            'TextTranslation',
            'Coding',
            'ToolUse'
        )]
        [string] $LLMQueryType,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Use alternative settings stored in session for ' +
                'AI preferences')
        )]
        [switch] $SessionOnly,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Clear the session setting (Global variable) ' +
                'before saving')
        )]
        [switch] $ClearSession,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('When specified, skips session settings and ' +
                'writes only to persistent preferences or defaults')
        )]
        [Alias('FromPreferences')]
        [switch] $SkipSession,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Also write LLM settings to OneDrive for syncing ' +
                'across multiple machines')
        )]
        [switch] $AllMachines,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Database path for preference data files'
        )]
        [Alias('DatabasePath')]
        [string] $PreferencesDatabasePath,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Force non-interactive mode. Used for testing ' +
                'to verify guard clause behavior.')
        )]
        [switch] $NonInteractive
        #######################################################################
    )

    begin {
        if (-not [Environment]::UserInteractive -or $NonInteractive) {
            throw 'Cannot prompt for settings in a non-interactive session'
        }

        $allValidTypes = @(
            'SimpleIntelligence',
            'Knowledge',
            'Pictures',
            'TextTranslation',
            'Coding',
            'ToolUse'
        )

        # capture caller's bound parameters
        $callerParams = $PSBoundParameters

        $cancelMessage = 'User cancelled LLMSettings prompt'

        #######################################################################
        # Helper: build a SelectionPrompt with choices + Cancel + optional Back
        #######################################################################
        function NewSelectionMenu {
            param(
                [string] $Title,
                [string[]] $Choices,
                [string[]] $HighlightedChoices = @(),
                [bool] $ShowCancel = $true,
                [bool] $ShowBack = $false,
                [string] $BackLabel = '.. Back'
            )

            $prompt = Microsoft.PowerShell.Utility\New-Object Spectre.Console.SelectionPrompt[string] `
                -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
            $prompt.Title = $Title

            # Use plain strings only — Spectre.Console.Markup objects display
            # as their type name in SelectionPrompt<string> (CLR .ToString()),
            # and inline markup tags like [red] interact badly with the
            # prompt's own ANSI escape codes, corrupting adjacent characters.
            foreach ($choice in $Choices) {
                if ($choice -in $HighlightedChoices) {
                    # Prepend a marker so highlighted items stand out
                    $null = $prompt.AddChoice("> ${choice}")
                } else {
                    $null = $prompt.AddChoice($choice)
                }
            }

            if ($ShowBack) {
                $null = $prompt.AddChoice($BackLabel)
            }

            if ($ShowCancel) {
                $null = $prompt.AddChoice('.. Cancel')
            }

            return $prompt
        }

        #######################################################################
        # Helper: show a SelectionPrompt and get the chosen value
        #######################################################################
        function InvokeSelectionMenu {
            param(
                [Spectre.Console.SelectionPrompt[string]] $Prompt,
                [hashtable] $LabelToValue = @{}
            )

            $chosen = GenXdev\Invoke-SpectrePrompt -Prompt $Prompt

            $plain = if ($chosen.StartsWith('> ')) {
                $chosen.Substring(2)
            } else {
                $chosen
            }

            if ($plain -eq '.. Cancel' -or $plain -eq 'Cancel') {
                throw $cancelMessage
            }

            if ($plain -eq '.. Back' -or $plain -eq 'Back') {
                return [PSCustomObject]@{ IsBack = $true; Value = $null }
            }

            if ($LabelToValue.ContainsKey($plain)) {
                return [PSCustomObject]@{
                    IsBack = $false; Value = $LabelToValue[$plain]
                }
            }

            return [PSCustomObject]@{ IsBack = $false; Value = $plain }
        }

        #######################################################################
        # Helper: get saved API keys for a provider family, with their names
        #######################################################################
        function GetSavedProviderKeys {
            param(
                [string] $Provider
            )

            $prefix = "AILLMProviderApiKey_${Provider}_"
            $keys = @()

            # build splatted params for Get-GenXdevPreference to forward
            # PreferencesDatabasePath, SessionOnly, SkipSession etc.
            $getPrefParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Get-GenXdevPreference'

            # build splatted params for Get-GenXdevPreferenceNames
            $getNamesParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Get-GenXdevPreferenceNames'

            try {
                # discover saved key names dynamically matching this provider prefix
                $allNames = @(GenXdev\Get-GenXdevPreferenceNames @getNamesParams)
                $matchingNames = $allNames | Microsoft.PowerShell.Core\Where-Object {
                    $_.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)
                }

                foreach ($fullKey in $matchingNames) {
                    $name = $fullKey.Substring($prefix.Length)
                    if ([string]::IsNullOrEmpty($name)) { continue }

                    $val = GenXdev\Get-GenXdevPreference @getPrefParams `
                        -Name $fullKey `
                        -ErrorAction SilentlyContinue
                    if ($val) {
                        $maskedLen = [Math]::Min(4, $val.Length)
                        $keys += @{
                            Display = "${name} ($($val.Substring(0, $maskedLen))****)"
                            Name      = $name
                            Key       = $val
                            PreferenceKey = $fullKey
                        }
                    }
                }
            } catch {
                Microsoft.PowerShell.Utility\Write-Verbose "Error scanning provider keys: $($_.Exception.Message)"
            }

            return $keys
        }

        #######################################################################
        # Helper: save an API key under a provider family + user name
        #######################################################################
        function SaveProviderKey {
            param(
                [string] $Provider,
                [string] $KeyName,
                [string] $ApiKey
            )

            $prefKey = "AILLMProviderApiKey_${Provider}_${KeyName}"

            # forward caller's PreferencesDatabasePath, SessionOnly etc.
            $setPrefParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Set-GenXdevPreference'
            GenXdev\Set-GenXdevPreference @setPrefParams `
                -Name $prefKey `
                -Value $ApiKey
        }

        #######################################################################
        # Helper: render a summary table of what's currently configured
        #######################################################################
        function ShowCurrentSettings {
            GenXdev\Write-SpectreLine
            GenXdev\Write-SpectreMarkupLine -Message `
                '[grey]Current settings:[/]'
            GenXdev\Write-SpectreLine

            # Pad plain text first, then wrap in markup — PowerShell's -f
            # operator counts markup tags as characters, which breaks
            # column alignment when Spectre.Console strips them at render.
            $typeWidth = ($allValidTypes |
                Microsoft.PowerShell.Core\ForEach-Object { $_.Length } |
                Microsoft.PowerShell.Utility\Measure-Object -Maximum).Maximum
            $typeWidth = [Math]::Max($typeWidth, 4)
            $modelWidth = 36

            $hdrType = 'Type'.PadRight($typeWidth)
            $hdrModel = 'Model'.PadRight($modelWidth)
            GenXdev\Write-SpectreMarkupLine -Message `
                " [grey]${hdrType}[/] [grey]${hdrModel}[/] [grey]Key[/]"

            # Pre-read the key-value preference stores ONCE to avoid
            # ~186 JSON file reads (6 types x 6 props x ~3 stores/prop).
            # Both stores live under %LOCALAPPDATA%\GenXdev.PowerShell\
            # KeyValueStore\ and use the "Local_<StoreName>.json" pattern.
            $storeBase = Microsoft.PowerShell.Management\Join-Path `
                ([Environment]::GetEnvironmentVariable('LOCALAPPDATA')) `
                'GenXdev.PowerShell\KeyValueStore'

            $prefStore = $null
            $prefStorePath = Microsoft.PowerShell.Management\Join-Path `
                $storeBase 'Local_GenXdev.PowerShell.Preferences.json'
            if (Microsoft.PowerShell.Management\Test-Path `
                    -LiteralPath $prefStorePath) {
                try {
                    $prefStore = Microsoft.PowerShell.Management\Get-Content `
                        -LiteralPath $prefStorePath -Raw -ErrorAction Stop |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json `
                        -ErrorAction Stop
                } catch { }
            }

            $defStore = $null
            $defStorePath = Microsoft.PowerShell.Management\Join-Path `
                $storeBase 'Local_GenXdev.PowerShell.Defaults.json'
            if (Microsoft.PowerShell.Management\Test-Path `
                    -LiteralPath $defStorePath) {
                try {
                    $defStore = Microsoft.PowerShell.Management\Get-Content `
                        -LiteralPath $defStorePath -Raw -ErrorAction Stop |
                        Microsoft.PowerShell.Utility\ConvertFrom-Json `
                        -ErrorAction Stop
                } catch { }
            }

            # Build a flat hashtable of all AILLMSettings_* key → value
            # (keys are stored lowercase in the preference store).
            # Each store entry is either {value: "...", deletedDate: null}
            # (new format) or a plain string (legacy format).
            $aiPrefs = @{}
            foreach ($store in @($prefStore, $defStore)) {
                if ($null -eq $store) { continue }
                foreach ($prop in $store.PSObject.Properties) {
                    if (-not $prop.Name.StartsWith('AILLMSettings_',
                            [StringComparison]::OrdinalIgnoreCase)) {
                        continue
                    }
                    $entry = $prop.Value
                    $val = $null
                    if ($entry -is [System.Management.Automation.PSCustomObject]) {
                        if ($entry.PSObject.Properties['deletedDate'] -and
                            $entry.deletedDate) { continue }
                        if ($entry.PSObject.Properties['value']) {
                            $val = $entry.value
                        } else {
                            $val = $entry.ToString()
                        }
                    } else {
                        $val = $entry.ToString()
                    }
                    if ($null -ne $val) {
                        $aiPrefs[$prop.Name.ToLowerInvariant()] = $val
                    }
                }
            }

            # Pre-warm the defaults cache for all 6 types so only one
            # Get-AIDefaultLLMSettings call is needed per type (cached
            # after the first call due to $script:DefaultsCache).
            $defParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Get-AIDefaultLLMSettings'
            $cachedDefaults = @{}
            foreach ($showType in $allValidTypes) {
                $defParams['LLMQueryType'] = $showType
                $cachedDefaults[$showType] = @(
                    GenXdev\Get-AIDefaultLLMSettings @defParams)
            }

            foreach ($showType in $allValidTypes) {
                $typeLower = $showType.ToLowerInvariant()

                # Model: preferences first, then cached defaults
                $modelLookup = "aillmsettings_${typeLower}_model"
                $model = $aiPrefs[$modelLookup]
                if (-not $model) {
                    $defs = $cachedDefaults[$showType]
                    $model = if ($defs.Count -gt 0 -and $defs[0].Model) {
                        $defs[0].Model
                    } else { '-' }
                }

                # ApiKey: check if configured
                $keyLookup = "aillmsettings_${typeLower}_apikey"
                $keyStatus = if ($aiPrefs[$keyLookup]) {
                    '[green]configured[/]'
                } else {
                    '[grey]-[/]'
                }

                $tPadded = $showType.PadRight($typeWidth)
                $mPadded = $model.PadRight($modelWidth)
                GenXdev\Write-SpectreMarkupLine -Message `
                    " [cyan]${tPadded}[/] ${mPadded} ${keyStatus}"
            }
            GenXdev\Write-SpectreLine
        }

        #######################################################################
        # Helper: render a summary table of what was just configured
        #######################################################################
        function ShowConfiguredSummary {
            param(
                [hashtable[]] $ConfiguredItems
            )

            GenXdev\Write-SpectreLine
            GenXdev\Write-SpectreMarkupLine -Message `
                '[green]=== Configuration Complete ===[/]'
            GenXdev\Write-SpectreLine

            # Pad plain text first, then wrap in markup to avoid alignment
            # drift from invisible markup characters (same rationale as
            # ShowCurrentSettings above).
            $typeWidth = 22
            $provWidth = 15
            $modelWidth = 30

            $hdrType = 'Query Type'.PadRight($typeWidth)
            $hdrProv = 'Provider'.PadRight($provWidth)
            $hdrModel = 'Model'.PadRight($modelWidth)
            GenXdev\Write-SpectreMarkupLine -Message `
                " [grey]${hdrType}[/] [grey]${hdrProv}[/] [grey]${hdrModel}[/] [grey]Key[/]"

            foreach ($item in $ConfiguredItems) {
                $tPadded = $item.Type.PadRight($typeWidth)
                $pPadded = $item.Provider.PadRight($provWidth)
                $mPadded = $item.Model.PadRight($modelWidth)
                GenXdev\Write-SpectreMarkupLine -Message `
                    " [cyan]${tPadded}[/] ${pPadded} ${mPadded} $($item.KeyName)"
            }

            GenXdev\Write-SpectreLine
            GenXdev\Write-SpectreMarkupLine -Message `
                '[grey]Tip: Change anytime with[/] [yellow]llmset[/]'
        }

        #######################################################################
        # Helper: configure a single query type interactively
        # Returns a hashtable with Type, Provider, Model, KeyName on success,
        # or $null if skipped.
        #######################################################################
        function ConfigureSingleType {
            param(
                [string] $Type,
                [int] $CurrentIndex,
                [int] $TotalCount,
                [bool] $AllowSkip = $false
            )
            $header = if ($TotalCount -gt 1) {
                "[cyan]=== Configuring '${Type}' (${CurrentIndex}/${TotalCount}) ===[/]"
            } else {
                "[cyan]=== GenXdev LLM Configuration ===[/]`n[yellow]Configuring '${Type}' query type[/]"
            }

            GenXdev\Write-SpectreLine
            GenXdev\Write-SpectreMarkupLine -Message $header
            GenXdev\Write-SpectreLine

            # Step 1: pick mode — provider / manual / skip / cancel
            $modeChoices = [System.Collections.Generic.List[string]]::new()
            $modeChoices.Add('Pick a default provider (API key only)')
            $modeChoices.Add('Enter all settings manually')
            if ($AllowSkip) {
                $modeChoices.Add('.. Skip this type (keep current settings)')
            }
            $modeMenu = NewSelectionMenu `
                -Title 'How would you like to configure this type?' `
                -Choices $modeChoices.ToArray() `
                -ShowBack $false `
                -ShowCancel $true

            $modeResult = InvokeSelectionMenu -Prompt $modeMenu
            if ($modeResult.IsBack) { return $null }

            $modeChosen = $modeResult.Value.ToString()

            if ($modeChosen -like '*Skip*') {
                GenXdev\Write-SpectreMarkupLine -Message `
                    "[grey]Skipped '${Type}'[/]"
                return $null
            }

            $isPickProvider = $modeChosen -like '*default provider*'

            if ($isPickProvider) {
                return (ConfigureViaProviderPick -Type $Type)
            } else {
                return (ConfigureViaManualEntry -Type $Type)
            }
        }

        #######################################################################
        # Sub-helper: pick from default providers
        #######################################################################
        function ConfigureViaProviderPick {
            param([string] $Type)

            $defaultParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Get-AIDefaultLLMSettings'
            $defaultParams['LLMQueryType'] = $Type
            $providers = @(GenXdev\Get-AIDefaultLLMSettings @defaultParams)

            if ($providers.Count -eq 0) {
                throw "No default LLM providers found for type '${Type}'"
            }

            $displayNames = [System.Collections.Generic.List[string]]::new()
            $providerMap = @{}
            foreach ($p in $providers) {
                $ht = if ($p -is [hashtable]) {
                    $p
                } else {
                    $p | GenXdev\ConvertTo-HashTable
                }

                $dn = if ($ht.ContainsKey('DisplayName') -and $ht.DisplayName) {
                    $ht.DisplayName
                } else {
                    "$($ht.Model) @ $($ht.ApiEndpoint)"
                }

                # grey out providers incompatible with this query type
                if ($Type -eq 'Pictures' -and
                    $ht.NoSupportForImageUpload -eq $true) {
                    $dn = "[grey]${dn} (no image support)[/]"
                }
                if ($Type -eq 'ToolUse' -and
                    $ht.NoSupportForToolCalls -eq $true) {
                    $dn = "[grey]${dn} (no tool support)[/]"
                }

                $displayNames.Add($dn)
                # use the plain name (without markup) as the map key
                $plainDn = $dn -replace '\[/?[^\]]+\]', ''
                $providerMap[$plainDn] = $ht
            }

            GenXdev\Write-SpectreMarkupLine -Message `
                '[green]Available providers:[/]'
            $providerMenu = NewSelectionMenu `
                -Title 'Select a provider:' `
                -Choices $displayNames.ToArray() `
                -ShowBack $true `
                -ShowCancel $true

            $providerResult = InvokeSelectionMenu -Prompt $providerMenu `
                -LabelToValue $providerMap
            if ($providerResult.IsBack) {
                # go back to mode selection — recurse into ConfigureSingleType
                return (ConfigureSingleType -Type $Type -CurrentIndex 1 `
                        -TotalCount 1 -AllowSkip $false)
            }

            $selectedProvider = $providerResult.Value

            # determine the provider family
            $providerFamily = if ($selectedProvider.ContainsKey('Provider') -and
                $selectedProvider.Provider) {
                $selectedProvider.Provider
            } else {
                # fallback: derive from endpoint if Provider field missing
                if ($selectedProvider.ApiEndpoint -like '*googleapis*') {
                    'Google'
                } elseif ($selectedProvider.ApiEndpoint -like '*deepseek*') {
                    'DeepSeek'
                } elseif ($selectedProvider.ApiEndpoint -like '*openai*') {
                    'OpenAI'
                } else {
                    'Custom'
                }
            }

            # look for saved keys for this provider family
            $savedKeys = GetSavedProviderKeys -Provider $providerFamily

            $apiKey = $null
            $keyName = $null

            if ($savedKeys.Count -gt 0) {
                # build a menu of saved keys + "enter new"
                $keyChoices = [System.Collections.Generic.List[string]]::new()
                $keyMap = @{}
                foreach ($sk in $savedKeys) {
                    $keyChoices.Add("Use saved key: $($sk.Display)")
                    $keyMap["Use saved key: $($sk.Display)"] = $sk
                }
                $keyChoices.Add('Enter a new API key')

                $keyMenu = NewSelectionMenu `
                    -Title 'Select an API key:' `
                    -Choices $keyChoices.ToArray() `
                    -ShowBack $true `
                    -ShowCancel $true

                $keyResult = InvokeSelectionMenu -Prompt $keyMenu
                if ($keyResult.IsBack) {
                    return (ConfigureViaProviderPick -Type $Type)
                }

                $keyChosen = $keyResult.Value.ToString()

                if ($keyChosen -like '*Use saved key:*') {
                    $selectedKey = $keyMap[$keyChosen]
                    $apiKey = $selectedKey.Key
                    $keyName = $selectedKey.Name
                } else {
                    # enter new key
                    $keyName = GenXdev\Invoke-SpectreAsk -Message `
                        'Give this key a name (e.g. Work, Personal):'
                    if ([string]::IsNullOrWhiteSpace($keyName)) {
                        $keyName = 'Default'
                    }
                    $apiKey = GenXdev\Invoke-SpectreAsk -Message `
                        'Enter your API key:'
                    if ([string]::IsNullOrWhiteSpace($apiKey)) {
                        throw 'API key cannot be empty'
                    }
                }
            } else {
                # no saved keys — enter new with a name
                GenXdev\Write-SpectreLine
                $keyName = GenXdev\Invoke-SpectreAsk -Message `
                    'Give this key a name (e.g. Work, Personal):'
                if ([string]::IsNullOrWhiteSpace($keyName)) {
                    $keyName = 'Default'
                }
                $apiKey = GenXdev\Invoke-SpectreAsk -Message `
                    'Enter your API key:'
                if ([string]::IsNullOrWhiteSpace($apiKey)) {
                    throw 'API key cannot be empty'
                }
            }

            # save the key under provider family
            SaveProviderKey -Provider $providerFamily `
                -KeyName $keyName -ApiKey $apiKey

            # save settings via Set-AILLMSettings
            $setParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Set-AILLMSettings'
            $setParams['LLMQueryType'] = $Type
            $setParams['Model'] = $selectedProvider.Model
            $setParams['ApiEndpoint'] = $selectedProvider.ApiEndpoint
            $setParams['ApiKey'] = $apiKey
            $setParams['NoSupportForJsonSchema'] = `
                $selectedProvider.NoSupportForJsonSchema -eq $true
            $setParams['NoSupportForImageUpload'] = `
                $selectedProvider.NoSupportForImageUpload -eq $true
            $setParams['NoSupportForToolCalls'] = `
                $selectedProvider.NoSupportForToolCalls -eq $true
            GenXdev\Set-AILLMSettings @setParams

            return @{
                Type     = $Type
                Provider = $providerFamily
                Model    = $selectedProvider.Model
                KeyName  = $keyName
            }
        }

        #######################################################################
        # Sub-helper: manual entry
        #######################################################################
        function ConfigureViaManualEntry {
            param([string] $Type)

            GenXdev\Write-SpectreMarkupLine -Message `
                '[grey]Common endpoints:[/]'

            # dynamically resolve unique endpoints from defaults;
            # silently skip hints if the JSON can't be loaded
            try {
                $epDefaultsParams = GenXdev\Copy-IdenticalParamValues `
                    -BoundParameters $callerParams `
                    -FunctionName 'Get-AIDefaultLLMSettings'
                $epDefaultsParams['LLMQueryType'] = $Type
                $epDefaults = @(GenXdev\Get-AIDefaultLLMSettings @epDefaultsParams)

                if ($epDefaults.Count -gt 0) {
                    $seenEndpoints = [ordered]@{}
                    foreach ($epDef in $epDefaults) {
                        $epHt = if ($epDef -is [hashtable]) {
                            $epDef
                        } else {
                            $epDef | GenXdev\ConvertTo-HashTable
                        }
                        $ep = $epHt.ApiEndpoint
                        if ($ep -and -not $seenEndpoints.Contains($ep)) {
                            $seenEndpoints[$ep] = if ($epHt.Provider) {
                                $epHt.Provider } else { 'Custom' }
                        }
                    }
                    foreach ($epKey in $seenEndpoints.Keys) {
                        $prov = $seenEndpoints[$epKey]
                        GenXdev\Write-SpectreMarkupLine -Message `
                            " ${prov}: ${epKey}"
                    }
                }
            } catch {
                Microsoft.PowerShell.Utility\Write-Verbose `
                    "Could not load endpoint hints: $($_.Exception.Message)"
            }
            GenXdev\Write-SpectreLine

            $model = GenXdev\Invoke-SpectreAsk -Message `
                'Enter model name:'
            if ([string]::IsNullOrWhiteSpace($model)) {
                throw 'Model name cannot be empty'
            }

            $endpoint = GenXdev\Invoke-SpectreAsk -Message `
                'Enter API endpoint URL:'
            if ([string]::IsNullOrWhiteSpace($endpoint)) {
                throw 'API endpoint cannot be empty'
            }

            # determine provider family from endpoint
            $providerFamily = if ($endpoint -like '*googleapis*') {
                'Google'
            } elseif ($endpoint -like '*deepseek*') {
                'DeepSeek'
            } elseif ($endpoint -like '*openai*') {
                'OpenAI'
            } else {
                'Custom'
            }

            # look for saved keys for this provider family
            $savedKeys = GetSavedProviderKeys -Provider $providerFamily

            $apiKey = $null
            $keyName = $null

            if ($savedKeys.Count -gt 0) {
                $keyChoices = [System.Collections.Generic.List[string]]::new()
                $keyMap = @{}
                foreach ($sk in $savedKeys) {
                    $keyChoices.Add("Use saved key: $($sk.Display)")
                    $keyMap["Use saved key: $($sk.Display)"] = $sk
                }
                $keyChoices.Add('Enter a new API key')

                $keyMenu = NewSelectionMenu `
                    -Title 'Select an API key:' `
                    -Choices $keyChoices.ToArray() `
                    -ShowBack $true `
                    -ShowCancel $true

                $keyResult = InvokeSelectionMenu -Prompt $keyMenu
                if ($keyResult.IsBack) {
                    return (ConfigureViaManualEntry -Type $Type)
                }

                $keyChosen = $keyResult.Value.ToString()

                if ($keyChosen -like '*Use saved key:*') {
                    $selectedKey = $keyMap[$keyChosen]
                    $apiKey = $selectedKey.Key
                    $keyName = $selectedKey.Name
                } else {
                    $keyName = GenXdev\Invoke-SpectreAsk -Message `
                        'Give this key a name (e.g. Work, Personal):'
                    if ([string]::IsNullOrWhiteSpace($keyName)) {
                        $keyName = 'Default'
                    }
                    $apiKey = GenXdev\Invoke-SpectreAsk -Message `
                        'Enter your API key:'
                    if ([string]::IsNullOrWhiteSpace($apiKey)) {
                        throw 'API key cannot be empty'
                    }
                }
            } else {
                GenXdev\Write-SpectreLine
                $keyName = GenXdev\Invoke-SpectreAsk -Message `
                    'Give this key a name (e.g. Work, Personal):'
                if ([string]::IsNullOrWhiteSpace($keyName)) {
                    $keyName = 'Default'
                }
                $apiKey = GenXdev\Invoke-SpectreAsk -Message `
                    'Enter your API key:'
                if ([string]::IsNullOrWhiteSpace($apiKey)) {
                    throw 'API key cannot be empty'
                }
            }

            # save the key under provider family
            SaveProviderKey -Provider $providerFamily `
                -KeyName $keyName -ApiKey $apiKey

            # ask about capabilities
            GenXdev\Write-SpectreLine
            GenXdev\Write-SpectreMarkupLine -Message `
                '[yellow]Endpoint capabilities:[/]'
            $supportsJsonSchema = GenXdev\Invoke-SpectreConfirm -Message `
                'Does this endpoint support JSON Schema (structured output)?'
            $supportsImageUpload = GenXdev\Invoke-SpectreConfirm -Message `
                'Does this endpoint support image upload?'
            $supportsToolCalls = GenXdev\Invoke-SpectreConfirm -Message `
                'Does this endpoint support tool calling?'

            $setParams = GenXdev\Copy-IdenticalParamValues `
                -BoundParameters $callerParams `
                -FunctionName 'Set-AILLMSettings'
            $setParams['LLMQueryType'] = $Type
            $setParams['Model'] = $model
            $setParams['ApiEndpoint'] = $endpoint
            $setParams['ApiKey'] = $apiKey
            $setParams['NoSupportForJsonSchema'] = -not $supportsJsonSchema
            $setParams['NoSupportForImageUpload'] = -not $supportsImageUpload
            $setParams['NoSupportForToolCalls'] = -not $supportsToolCalls
            GenXdev\Set-AILLMSettings @setParams

            return @{
                Type     = $Type
                Provider = $providerFamily
                Model    = $model
                KeyName  = $keyName
            }
        }

        #######################################################################
        # Helper: configure all types with the same provider in one shot
        #######################################################################
        function ConfigureAllWithSameProvider {
            # first pick ONE provider for a reference type
            GenXdev\Write-SpectreMarkupLine -Message `
                '[yellow]Pick the provider to apply to ALL query types:[/]'
            GenXdev\Write-SpectreLine

            $result = ConfigureViaProviderPick -Type 'SimpleIntelligence'
            if (-not $result) { return @() }

            $configuredItems = @($result)

            # apply to remaining 5 types
            for ($i = 1; $i -lt $allValidTypes.Count; $i++) {
                $type = $allValidTypes[$i]
                $setParams = GenXdev\Copy-IdenticalParamValues `
                    -BoundParameters $callerParams `
                    -FunctionName 'Set-AILLMSettings'

                # get the settings we just saved for SimpleIntelligence
                $getAiParams = GenXdev\Copy-IdenticalParamValues `
                    -BoundParameters $callerParams `
                    -FunctionName 'Get-AILLMSettings'
                $getAiParams['LLMQueryType'] = 'SimpleIntelligence'
                $refSettings = GenXdev\Get-AILLMSettings @getAiParams `
                    -SuppressAutoPrompt:$true

                $setParams['LLMQueryType'] = $type
                $setParams['Model'] = $refSettings.Model
                $setParams['ApiEndpoint'] = $refSettings.ApiEndpoint
                $setParams['ApiKey'] = $refSettings.ApiKey
                $setParams['NoSupportForJsonSchema'] = `
                    $refSettings.NoSupportForJsonSchema
                $setParams['NoSupportForImageUpload'] = `
                    $refSettings.NoSupportForImageUpload
                $setParams['NoSupportForToolCalls'] = `
                    $refSettings.NoSupportForToolCalls
                GenXdev\Set-AILLMSettings @setParams

                $configuredItems += @{
                    Type     = $type
                    Provider = $result.Provider
                    Model    = $result.Model
                    KeyName  = $result.KeyName
                }
            }

            return $configuredItems
        }
    }

    process {
        # show current settings before any prompts
        ShowCurrentSettings

        # resolve target type(s)
        $cancelRequested = $false
        $typesToConfigure = if ($PSBoundParameters.ContainsKey('LLMQueryType')) {
            @($LLMQueryType)
        } else {
            try {
                $typeChoices = [System.Collections.Generic.List[string]]::new()
                foreach ($t in $allValidTypes) { $typeChoices.Add($t) }
                $typeChoices.Add('ALL')

                $typeMenu = NewSelectionMenu `
                    -Title 'Select which query type(s) to configure:' `
                    -Choices $typeChoices.ToArray() `
                    -HighlightedChoices @('ALL') `
                    -ShowBack $false `
                    -ShowCancel $true

                $typeResult = InvokeSelectionMenu -Prompt $typeMenu
                if ($typeResult.IsBack) { return }

                $typeChosen = $typeResult.Value.ToString()

                if ($typeChosen -eq 'ALL') {
                    # ask: all at once or individually?
                    $allModeChoices = @(
                        'Apply one provider to ALL types at once',
                        'Configure each type individually'
                    )
                    $allModeMenu = NewSelectionMenu `
                        -Title 'How would you like to configure ALL types?' `
                        -Choices $allModeChoices `
                        -HighlightedChoices @($allModeChoices[0]) `
                        -ShowBack $true `
                        -ShowCancel $true

                    $allModeResult = InvokeSelectionMenu -Prompt $allModeMenu
                    if ($allModeResult.IsBack) {
                        # re-show type selection by recursing
                        $PSBoundParameters.Remove('LLMQueryType')
                        return (GenXdev\Invoke-AILLMSettingsPrompt @PSBoundParameters)
                    }

                    $allModeChosen = $allModeResult.Value.ToString()

                    if ($allModeChosen -like '*at once*') {
                        $configuredItems = ConfigureAllWithSameProvider
                        ShowConfiguredSummary -ConfiguredItems $configuredItems
                        return
                    } else {
                        $allValidTypes
                    }
                } else {
                    @($typeChosen)
                }
            } catch {
                if ($_.Exception.Message -eq $cancelMessage) {
                    $cancelRequested = $true
                } else {
                    throw
                }
            }
        }

        if ($cancelRequested) { return }

        # Ensure typesToConfigure is always an array
        $typesToConfigure = @($typesToConfigure)

        # configure types (individually, with progress)
        $configuredItems = [System.Collections.Generic.List[hashtable]]::new()
        $total = $typesToConfigure.Count

        for ($i = 0; $i -lt $total; $i++) {
            try {
                $queryType = $typesToConfigure[$i]

            # check if we should offer "apply to remaining" shortcut
            if ($i -eq 1 -and $total -gt 2 -and $configuredItems.Count -gt 0) {
                $lastProvider = $configuredItems[-1]
                GenXdev\Write-SpectreLine
                $shortcutMsg = "[grey]Apply '$($lastProvider.Model)' to all remaining types?[/]"
                GenXdev\Write-SpectreMarkupLine -Message $shortcutMsg

                $shortcutChoices = @(
                    'Yes, apply to all remaining types',
                    'No, pick individually'
                )
                $shortcutMenu = NewSelectionMenu `
                    -Title '' `
                    -Choices $shortcutChoices `
                    -HighlightedChoices @($shortcutChoices[1]) `
                    -ShowBack $false `
                    -ShowCancel $true

                $shortcutResult = InvokeSelectionMenu `
                    -Prompt $shortcutMenu
                $shortcutChosen = $shortcutResult.Value.ToString()

                if ($shortcutChosen -like '*Yes*') {
                    # apply this provider to remaining types
                    for ($j = $i; $j -lt $total; $j++) {
                        $remType = $typesToConfigure[$j]
                        $getAiShortcutParams = GenXdev\Copy-IdenticalParamValues `
                            -BoundParameters $callerParams `
                            -FunctionName 'Get-AILLMSettings'
                        $getAiShortcutParams['LLMQueryType'] = $typesToConfigure[0]
                        $refSettings = GenXdev\Get-AILLMSettings @getAiShortcutParams `
                            -SuppressAutoPrompt:$true

                        $setParams = GenXdev\Copy-IdenticalParamValues `
                            -BoundParameters $callerParams `
                            -FunctionName 'Set-AILLMSettings'
                        $setParams['LLMQueryType'] = $remType
                        $setParams['Model'] = $refSettings.Model
                        $setParams['ApiEndpoint'] = $refSettings.ApiEndpoint
                        $setParams['ApiKey'] = $refSettings.ApiKey
                        $setParams['NoSupportForJsonSchema'] = `
                            $refSettings.NoSupportForJsonSchema
                        $setParams['NoSupportForImageUpload'] = `
                            $refSettings.NoSupportForImageUpload
                        $setParams['NoSupportForToolCalls'] = `
                            $refSettings.NoSupportForToolCalls
                        GenXdev\Set-AILLMSettings @setParams

                        $configuredItems.Add(@{
                            Type     = $remType
                            Provider = $configuredItems[0].Provider
                            Model    = $configuredItems[0].Model
                            KeyName  = $configuredItems[0].KeyName
                        })
                    }
                    break
                }
                }

                $item = ConfigureSingleType -Type $queryType `
                    -CurrentIndex ($i + 1) -TotalCount $total `
                    -AllowSkip ($total -gt 1)

                if ($item) {
                    $configuredItems.Add($item)
                }
            } catch {
                if ($_.Exception.Message -eq $cancelMessage) {
                    if ($configuredItems.Count -gt 0) {
                        GenXdev\Write-SpectreLine
                        $partialMsg = "[yellow]${configuredItems.Count} type(s) were already saved:[/]"
                        GenXdev\Write-SpectreMarkupLine -Message $partialMsg
                        $saved = ($configuredItems |
                            Microsoft.PowerShell.Core\ForEach-Object { $_.Type }) -join ', '
                        GenXdev\Write-SpectreMarkupLine -Message `
                            "[yellow]${saved}[/]"
                        $rerunMsg = "[grey]Run [yellow]llmset[/] again to configure the remaining types.[/]"
                        GenXdev\Write-SpectreMarkupLine -Message $rerunMsg
                    }
                    return
                }
                throw
            }
        }

        # show summary of what was configured
        if ($configuredItems.Count -gt 0) {

            ShowConfiguredSummary -ConfiguredItems $configuredItems.ToArray()
        }
    }

    end {
    }
}
###############################################################################