completers/psservice_completer/psservice_completer.ps1

# PsService tab completion for PowerShell
# Provides standalone native completion for psservice.exe with local service hints.

Set-StrictMode -Version 2.0

if (-not (Get-Variable -Name PsServiceCompletionCatalog -Scope Script -ErrorAction Ignore)) {
    $script:PsServiceCompletionCatalog = @{
        CommandName      = $null
        TopLevelCommands = @(
            @{ Name = 'query'; Description = 'Queries the status of a service.' }
            @{ Name = 'config'; Description = 'Queries the configuration.' }
            @{ Name = 'setconfig'; Description = 'Sets the configuration.' }
            @{ Name = 'start'; Description = 'Starts a service.' }
            @{ Name = 'stop'; Description = 'Stops a service.' }
            @{ Name = 'restart'; Description = 'Stops and then restarts a service.' }
            @{ Name = 'pause'; Description = 'Pauses a service.' }
            @{ Name = 'cont'; Description = 'Continues a paused service.' }
            @{ Name = 'depend'; Description = 'Enumerates the services that depend on the specified service.' }
            @{ Name = 'find'; Description = 'Searches for an instance of a service on the network.' }
            @{ Name = 'security'; Description = 'Reports the security permissions assigned to a service.' }
        )
        RootSwitches     = @(
            @{ Name = '-?'; Description = 'Show help.' }
            @{ Name = '/?'; Description = 'Show help.' }
            @{ Name = '-nobanner'; Description = 'Suppress the startup banner and copyright message.' }
            @{ Name = '-u'; Description = 'Username for the remote-auth preamble.' }
            @{ Name = '-p'; Description = 'Password for the remote-auth preamble.' }
        )
        CommandSwitches  = @(
            @{ Name = '-?'; Description = 'Show help for the selected command.' }
            @{ Name = '/?'; Description = 'Show help for the selected command.' }
        )
        QuerySwitches    = @(
            @{ Name = '-g'; Description = 'Restrict query results to a load-order group.' }
            @{ Name = '-t'; Description = 'Restrict query results by type.' }
            @{ Name = '-s'; Description = 'Restrict query results by state.' }
        )
        QueryTypeValues  = @('driver', 'service', 'interactive', 'all')
        QueryStateValues = @('active', 'inactive', 'all')
        StartTypeValues  = @('auto', 'demand', 'disabled')
    }
}

if (-not (Get-Variable -Name PsServiceServiceCache -Scope Script -ErrorAction Ignore)) {
    $script:PsServiceServiceCache = @{
        LastUpdated        = $null
        TtlSeconds         = 30
        ServiceNames       = @()
        DisplayNames       = @()
        ActiveNames        = @()
        InactiveNames      = @()
        DriverLastUpdated  = $null
        DriverNames        = @()
    }
}

function Resolve-PsServiceCommandName {
    if ($script:PsServiceCompletionCatalog.CommandName) {
        return $script:PsServiceCompletionCatalog.CommandName
    }

    $command = Get-Command -Name 'psservice.exe', 'psservice' -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($command) {
        if ($command.Source) {
            $script:PsServiceCompletionCatalog.CommandName = $command.Source
        } elseif ($command.Path) {
            $script:PsServiceCompletionCatalog.CommandName = $command.Path
        } else {
            $script:PsServiceCompletionCatalog.CommandName = $command.Name
        }
    }

    $script:PsServiceCompletionCatalog.CommandName
}

function Test-PsServiceCommandAvailable {
    [bool](Resolve-PsServiceCommandName)
}

function New-PsServiceCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType,
        [string]$ToolTip,
        [string]$ListItemText
    )

    if ([string]::IsNullOrWhiteSpace($ListItemText)) {
        $ListItemText = $CompletionText
    }

    if ([string]::IsNullOrWhiteSpace($ToolTip)) {
        $ToolTip = $CompletionText
    }

    [System.Management.Automation.CompletionResult]::new(
        $CompletionText,
        $ListItemText,
        $ResultType,
        $ToolTip
    )
}

function New-PsServiceLiteralValueResults {
    param(
        [string]$CurrentValue,
        [string]$ToolTip,
        [string]$Placeholder = '<value>'
    )

    if ([string]::IsNullOrEmpty($CurrentValue)) {
        return @(
            New-PsServiceCompletionResult -CompletionText $Placeholder -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip
        )
    }

    @(
        New-PsServiceCompletionResult -CompletionText $CurrentValue -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip
    )
}

function Test-PsServiceStartsWith {
    param(
        [string]$Value,
        [string]$Prefix
    )

    if ([string]::IsNullOrEmpty($Prefix)) {
        return $true
    }

    if ([string]::IsNullOrEmpty($Value)) {
        return $false
    }

    $Value.StartsWith($Prefix, [System.StringComparison]::OrdinalIgnoreCase)
}

function Get-PsServiceUniqueCompletions {
    param([System.Management.Automation.CompletionResult[]]$Results)

    $seen = @{}
    $unique = @()
    foreach ($result in $Results) {
        if ($null -eq $result) {
            continue
        }

        if ($seen.ContainsKey($result.CompletionText)) {
            continue
        }

        $seen[$result.CompletionText] = $true
        $unique += $result
    }

    $unique
}

function Update-PsServiceServiceCache {
    $lastUpdated = $script:PsServiceServiceCache.LastUpdated
    if ($null -ne $lastUpdated) {
        $age = (Get-Date) - $lastUpdated
        if ($script:PsServiceServiceCache.ServiceNames.Count -gt 0 -and $age.TotalSeconds -lt $script:PsServiceServiceCache.TtlSeconds) {
            return
        }
    }

    try {
        $services = @(Get-Service -ErrorAction Stop | Sort-Object -Property Name)
        $script:PsServiceServiceCache.ServiceNames = @(
            $services |
                Select-Object -ExpandProperty Name |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                Sort-Object -Unique
        )
        $script:PsServiceServiceCache.DisplayNames = @(
            $services |
                Select-Object -ExpandProperty DisplayName |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                Sort-Object -Unique
        )
        # PsService's -s active is every non-stopped SCM state; inactive is stopped only.
        $script:PsServiceServiceCache.ActiveNames = @(
            $services |
                Where-Object { $_.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Stopped } |
                ForEach-Object { $_.Name; $_.DisplayName } |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                Sort-Object -Unique
        )
        $script:PsServiceServiceCache.InactiveNames = @(
            $services |
                Where-Object { $_.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Stopped } |
                ForEach-Object { $_.Name; $_.DisplayName } |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                Sort-Object -Unique
        )
        $script:PsServiceServiceCache.LastUpdated = Get-Date
    } catch {
        if (-not $script:PsServiceServiceCache.ServiceNames) {
            $script:PsServiceServiceCache.ServiceNames = @()
        }
        if (-not $script:PsServiceServiceCache.DisplayNames) {
            $script:PsServiceServiceCache.DisplayNames = @()
        }
    }
}

function Get-PsServiceDriverNameList {
    $lastUpdated = $script:PsServiceServiceCache.DriverLastUpdated
    if ($null -ne $lastUpdated) {
        $age = (Get-Date) - $lastUpdated
        if ($script:PsServiceServiceCache.DriverNames.Count -gt 0 -and $age.TotalSeconds -lt $script:PsServiceServiceCache.TtlSeconds) {
            return @($script:PsServiceServiceCache.DriverNames)
        }
    }

    # 'query -t driver' lists kernel (Type 1) and file-system (Type 2) drivers; Get-Service
    # never returns them, and Win32_SystemDriver costs seconds, so read the SCM registry keys.
    $names = New-Object System.Collections.Generic.List[string]
    $root = $null
    try {
        $root = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Services')
        if ($root) {
            foreach ($subKeyName in $root.GetSubKeyNames()) {
                $subKey = $null
                try {
                    $subKey = $root.OpenSubKey($subKeyName)
                    if (-not $subKey) { continue }
                    $type = $subKey.GetValue('Type')
                    if ($type -ne 1 -and $type -ne 2) { continue }
                    $names.Add($subKeyName)
                    $displayName = [string]$subKey.GetValue('DisplayName')
                    if (-not [string]::IsNullOrWhiteSpace($displayName) -and -not $displayName.StartsWith('@')) {
                        $names.Add($displayName)
                    }
                } finally {
                    if ($subKey) { $subKey.Close() }
                }
            }
        }
    } catch {
        Write-Debug "psservice driver enumeration failed: $($_.Exception.Message)"
    } finally {
        if ($root) { $root.Close() }
    }

    $script:PsServiceServiceCache.DriverNames = @($names | Sort-Object -Unique)
    $script:PsServiceServiceCache.DriverLastUpdated = Get-Date
    @($script:PsServiceServiceCache.DriverNames)
}

function Get-PsServiceCurrentTokenState {
    param(
        [string]$Line,
        [int]$CursorPosition
    )

    if ($null -eq $Line) {
        $Line = ''
    }

    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length)
    $prefix = $Line.Substring(0, $safeCursor)
    $tokens = New-Object System.Collections.Generic.List[string]
    $builder = New-Object System.Text.StringBuilder
    $quoteChar = [char]0

    foreach ($character in $prefix.ToCharArray()) {
        if (($character -eq [char]34) -or ($character -eq [char]39)) {
            if ($quoteChar -eq [char]0) {
                $quoteChar = $character
            } elseif ($quoteChar -eq $character) {
                $quoteChar = [char]0
            }

            [void]$builder.Append($character)
            continue
        }

        if ([char]::IsWhiteSpace($character) -and $quoteChar -eq [char]0) {
            if ($builder.Length -gt 0) {
                $tokens.Add($builder.ToString())
                [void]$builder.Clear()
            }

            continue
        }

        [void]$builder.Append($character)
    }

    $hasTrailingSpace = $prefix -match '\s$'
    if ($builder.Length -gt 0) {
        $tokens.Add($builder.ToString())
    }

    if ($hasTrailingSpace) {
        return [pscustomobject]@{
            TokensBeforeCurrent = @($tokens)
            CurrentToken        = ''
            HasTrailingSpace    = $true
        }
    }

    if ($tokens.Count -gt 0) {
        return [pscustomobject]@{
            TokensBeforeCurrent = @($tokens | Select-Object -First ($tokens.Count - 1))
            CurrentToken        = $tokens[$tokens.Count - 1]
            HasTrailingSpace    = $false
        }
    }

    [pscustomobject]@{
        TokensBeforeCurrent = @()
        CurrentToken        = ''
        HasTrailingSpace    = $false
    }
}

function Get-PsServiceArgumentsFromTokenState {
    param([pscustomobject]$TokenState)

    if (-not $TokenState) {
        return [pscustomobject]@{
            ArgumentsBeforeCurrent = @()
            CurrentWord            = ''
        }
    }

    $tokensBeforeCurrent = @($TokenState.TokensBeforeCurrent)
    if ($tokensBeforeCurrent.Count -eq 0) {
        return [pscustomobject]@{
            ArgumentsBeforeCurrent = @()
            CurrentWord            = $TokenState.CurrentToken
        }
    }

    $arguments = @()
    if ($tokensBeforeCurrent.Count -gt 1) {
        $arguments = @($tokensBeforeCurrent | Select-Object -Skip 1)
    }

    [pscustomobject]@{
        ArgumentsBeforeCurrent = $arguments
        CurrentWord            = $TokenState.CurrentToken
    }
}

function Get-PsServiceStringValueCompletions {
    param(
        [string[]]$Values,
        [string]$CurrentWord,
        [string]$ToolTip,
        [string]$Prefix = '',
        [bool]$QuoteWhitespace = $false
    )

    # The engine hands over a quoted word with both quotes intact ("Spot Ver" or 'Spot Ver'), so strip both.
    $matchPrefix = $CurrentWord
    $typedQuote = $null
    if ($matchPrefix.Length -gt 0 -and (($matchPrefix[0] -eq [char]34) -or ($matchPrefix[0] -eq [char]39))) {
        $typedQuote = [string]$matchPrefix[0]
        $matchPrefix = $matchPrefix.Substring(1)
        if ($matchPrefix.Length -gt 0 -and $matchPrefix.EndsWith($typedQuote)) {
            $matchPrefix = $matchPrefix.Substring(0, $matchPrefix.Length - 1)
        }

        $matchPrefix = if ($typedQuote -eq "'") { $matchPrefix.Replace("''", "'") } else { $matchPrefix.Replace('`"', '"') }
    }

    $results = @()
    foreach ($value in ($Values | Sort-Object -Unique)) {
        if (Test-PsServiceStartsWith -Value $value -Prefix $matchPrefix) {
            $completionText = $value
            if ($QuoteWhitespace -and ($value -match '\s' -or $typedQuote)) {
                $quoteCharacter = if ($typedQuote -eq '"') { '"' } else { "'" }
                $escapedValue = if ($quoteCharacter -eq "'") {
                    $value -replace "'", "''"
                } else {
                    $value -replace '"', '`"'
                }

                $completionText = '{0}{1}{0}' -f $quoteCharacter, $escapedValue
            }

            $results += New-PsServiceCompletionResult -CompletionText ($Prefix + $completionText) -ListItemText $value -ResultType 'ParameterValue' -ToolTip $ToolTip
        }
    }

    $results
}

function Get-PsServiceServiceNameCompletions {
    param([string]$CurrentWord)

    Update-PsServiceServiceCache
    Get-PsServiceStringValueCompletions -Values $script:PsServiceServiceCache.ServiceNames -CurrentWord $CurrentWord -ToolTip 'Local service name.' -QuoteWhitespace $true
}

function Get-PsServiceDisplayNameCompletions {
    param([string]$CurrentWord)

    Update-PsServiceServiceCache
    Get-PsServiceStringValueCompletions -Values $script:PsServiceServiceCache.DisplayNames -CurrentWord $CurrentWord -ToolTip 'Local service display name.' -QuoteWhitespace $true
}

function Get-PsServiceCombinedServiceCompletions {
    param([string]$CurrentWord)

    $results = @()
    $results += @(Get-PsServiceServiceNameCompletions -CurrentWord $CurrentWord)
    $results += @(Get-PsServiceDisplayNameCompletions -CurrentWord $CurrentWord)
    Get-PsServiceUniqueCompletions -Results $results
}

function Get-PsServiceServerPlaceholderCompletions {
    param([string]$CurrentWord)

    if ([string]::IsNullOrWhiteSpace($CurrentWord)) {
        return @(
            New-PsServiceCompletionResult -CompletionText '\\Computer' -ListItemText '\\Computer' -ResultType 'ParameterValue' -ToolTip 'Remote computer name in \\Computer form.'
        )
    }

    if ('\\Computer'.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
        return @(
            New-PsServiceCompletionResult -CompletionText '\\Computer' -ListItemText '\\Computer' -ResultType 'ParameterValue' -ToolTip 'Remote computer name in \\Computer form.'
        )
    }

    @(
        New-PsServiceCompletionResult -CompletionText $CurrentWord -ListItemText $CurrentWord -ResultType 'ParameterValue' -ToolTip 'Remote computer name in \\Computer form.'
    )
}

function Get-PsServiceSwitchCompletions {
    param(
        [object[]]$Switches,
        [string]$CurrentWord
    )

    $results = @()
    foreach ($switchSpec in $Switches) {
        if (Test-PsServiceStartsWith -Value $switchSpec.Name -Prefix $CurrentWord) {
            $results += New-PsServiceCompletionResult -CompletionText $switchSpec.Name -ListItemText $switchSpec.Name -ResultType 'ParameterName' -ToolTip $switchSpec.Description
        }
    }

    $results
}

function Get-PsServiceCommandCompletions {
    param(
        [object[]]$Commands,
        [string]$CurrentWord
    )

    $results = @()
    foreach ($commandSpec in $Commands) {
        if (Test-PsServiceStartsWith -Value $commandSpec.Name -Prefix $CurrentWord) {
            $results += New-PsServiceCompletionResult -CompletionText $commandSpec.Name -ListItemText $commandSpec.Name -ResultType 'ParameterValue' -ToolTip $commandSpec.Description
        }
    }

    $results
}

function Get-PsServicePreambleContext {
    param([string[]]$ArgumentsBeforeCurrent)

    $args = @($ArgumentsBeforeCurrent)
    $index = 0
    $server = $null
    $pendingValueKind = $null
    $command = $null
    $commandArguments = @()
    $seenRootSwitches = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)

    if ($args.Count -gt 0 -and $args[0] -like '\\*') {
        $server = $args[0]
        $index++
    }

    while ($index -lt $args.Count) {
        $token = $args[$index]

        switch ($token.ToLowerInvariant()) {
            '-u' {
                if (($index + 1) -ge $args.Count) {
                    $pendingValueKind = 'Username'
                    $index = $args.Count
                    continue
                }

                $index += 2
                continue
            }
            '-p' {
                if (($index + 1) -ge $args.Count) {
                    $pendingValueKind = 'Password'
                    $index = $args.Count
                    continue
                }

                $index += 2
                continue
            }
            '-?' {
                [void]$seenRootSwitches.Add($token)
                $index++
                continue
            }
            '/?' {
                [void]$seenRootSwitches.Add($token)
                $index++
                continue
            }
            '-nobanner' {
                [void]$seenRootSwitches.Add($token)
                $index++
                continue
            }
            default {
                $command = $token
                if (($index + 1) -lt $args.Count) {
                    $commandArguments = @($args | Select-Object -Skip ($index + 1))
                }
                $index = $args.Count
            }
        }
    }

    [pscustomobject]@{
        Server            = $server
        PendingValueKind  = $pendingValueKind
        Command           = $command
        CommandKey        = if ($command) { $command.ToLowerInvariant() } else { $null }
        CommandArguments  = $commandArguments
        SeenRootSwitches  = @($seenRootSwitches)
    }
}

function Get-PsServiceQueryState {
    param([string[]]$ArgumentsBeforeCurrent)

    $positionals = New-Object System.Collections.Generic.List[string]
    $usedSwitches = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
    $filters = @{}
    $pendingSwitch = $null

    foreach ($argument in $ArgumentsBeforeCurrent) {
        if ($pendingSwitch) {
            [void]$usedSwitches.Add($pendingSwitch)
            $filters[$pendingSwitch] = $argument.Trim('"', "'").ToLowerInvariant()
            $pendingSwitch = $null
            continue
        }

        switch ($argument.ToLowerInvariant()) {
            '-g' {
                $pendingSwitch = '-g'
                continue
            }
            '-t' {
                $pendingSwitch = '-t'
                continue
            }
            '-s' {
                $pendingSwitch = '-s'
                continue
            }
            '-?' { continue }
            '/?' { continue }
            default {
                $positionals.Add($argument)
            }
        }
    }

    [pscustomobject]@{
        PendingSwitch = $pendingSwitch
        Positionals   = @($positionals)
        UsedSwitches  = @($usedSwitches)
        Filters       = $filters
    }
}

function Get-PsServiceQueryServiceCompletionList {
    param(
        [string]$CurrentWord,
        [hashtable]$Filters
    )

    # Mirror what 'psservice query -t <type> -s <state>' would list: services default to
    # type 'service' and state 'all'; drivers come from the SCM registry and carry no state.
    $type = if ($Filters.ContainsKey('-t')) { $Filters['-t'] } else { 'service' }
    $state = if ($Filters.ContainsKey('-s')) { $Filters['-s'] } else { 'all' }

    $values = @()
    if ($type -ne 'driver') {
        Update-PsServiceServiceCache
        $values += switch ($state) {
            'active' { @($script:PsServiceServiceCache.ActiveNames) }
            'inactive' { @($script:PsServiceServiceCache.InactiveNames) }
            default { @($script:PsServiceServiceCache.ServiceNames) + @($script:PsServiceServiceCache.DisplayNames) }
        }
    }

    if ($type -eq 'driver' -or $type -eq 'all') {
        $values += @(Get-PsServiceDriverNameList)
    }

    $toolTip = if ($type -eq 'driver') { 'Local driver name.' } else { 'Local service name.' }
    @(Get-PsServiceStringValueCompletions -Values @($values) -CurrentWord $CurrentWord -ToolTip $toolTip -QuoteWhitespace $true)
}

function Get-PsServiceUnusedSwitches {
    param(
        [object[]]$Switches,
        [string[]]$UsedNames
    )

    $used = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($usedName in $UsedNames) {
        [void]$used.Add($usedName)
    }

    @(
        foreach ($switchSpec in $Switches) {
            if (-not $used.Contains($switchSpec.Name)) {
                $switchSpec
            }
        }
    )
}

function Complete-PsServiceSingleServiceName {
    param(
        [string]$CurrentWord,
        [string[]]$ArgumentsBeforeCurrent
    )

    if ($ArgumentsBeforeCurrent.Count -eq 0) {
        $results = @()
        $results += @(Get-PsServiceCombinedServiceCompletions -CurrentWord $CurrentWord)
        $results += @(Get-PsServiceSwitchCompletions -Switches $script:PsServiceCompletionCatalog.CommandSwitches -CurrentWord $CurrentWord)
        return @(Get-PsServiceUniqueCompletions -Results $results)
    }

    @()
}

function Complete-PsServiceQuery {
    param(
        [string]$CurrentWord,
        [string[]]$ArgumentsBeforeCurrent
    )

    $state = Get-PsServiceQueryState -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent
    if ($state.PendingSwitch) {
        switch ($state.PendingSwitch) {
            '-t' {
                return @(Get-PsServiceStringValueCompletions -Values $script:PsServiceCompletionCatalog.QueryTypeValues -CurrentWord $CurrentWord -ToolTip 'Query type.')
            }
            '-s' {
                return @(Get-PsServiceStringValueCompletions -Values $script:PsServiceCompletionCatalog.QueryStateValues -CurrentWord $CurrentWord -ToolTip 'Query state.')
            }
            '-g' {
                return @(New-PsServiceLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<group>' -ToolTip 'Load-order group name.')
            }
        }
    }

    $availableSwitches = @(
        Get-PsServiceUnusedSwitches -Switches $script:PsServiceCompletionCatalog.QuerySwitches -UsedNames $state.UsedSwitches
    )

    $results = @()
    $results += @(Get-PsServiceSwitchCompletions -Switches $script:PsServiceCompletionCatalog.CommandSwitches -CurrentWord $CurrentWord)
    $results += @(Get-PsServiceSwitchCompletions -Switches $availableSwitches -CurrentWord $CurrentWord)

    if ($state.Positionals.Count -eq 0 -and -not (($CurrentWord -like '-*') -or ($CurrentWord -like '/*'))) {
        $results += @(Get-PsServiceQueryServiceCompletionList -CurrentWord $CurrentWord -Filters $state.Filters)
    }

    @(Get-PsServiceUniqueCompletions -Results $results)
}

function Complete-PsServiceSetConfig {
    param(
        [string]$CurrentWord,
        [string[]]$ArgumentsBeforeCurrent
    )

    if ($ArgumentsBeforeCurrent.Count -eq 0) {
        $results = @()
        $results += @(Get-PsServiceCombinedServiceCompletions -CurrentWord $CurrentWord)
        $results += @(Get-PsServiceSwitchCompletions -Switches $script:PsServiceCompletionCatalog.CommandSwitches -CurrentWord $CurrentWord)
        return @(Get-PsServiceUniqueCompletions -Results $results)
    }

    if ($ArgumentsBeforeCurrent.Count -eq 1) {
        return @(Get-PsServiceStringValueCompletions -Values $script:PsServiceCompletionCatalog.StartTypeValues -CurrentWord $CurrentWord -ToolTip 'Service start type.')
    }

    @()
}

function Complete-PsServiceFind {
    param(
        [string]$CurrentWord,
        [string[]]$ArgumentsBeforeCurrent
    )

    if ($ArgumentsBeforeCurrent.Count -eq 0) {
        $results = @()
        $results += @(Get-PsServiceCombinedServiceCompletions -CurrentWord $CurrentWord)
        $results += @(Get-PsServiceSwitchCompletions -Switches $script:PsServiceCompletionCatalog.CommandSwitches -CurrentWord $CurrentWord)
        return @(Get-PsServiceUniqueCompletions -Results $results)
    }

    if ($ArgumentsBeforeCurrent.Count -eq 1) {
        return @(Get-PsServiceStringValueCompletions -Values @('all') -CurrentWord $CurrentWord -ToolTip 'Search the entire network.')
    }

    @()
}

function Complete-PsServiceByCommand {
    param(
        [string]$CommandKey,
        [string]$CurrentWord,
        [string[]]$ArgumentsBeforeCurrent
    )

    switch ($CommandKey) {
        'query' { return @(Complete-PsServiceQuery -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'config' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'security' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'setconfig' { return @(Complete-PsServiceSetConfig -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'start' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'stop' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'restart' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'pause' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'cont' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'depend' { return @(Complete-PsServiceSingleServiceName -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
        'find' { return @(Complete-PsServiceFind -CurrentWord $CurrentWord -ArgumentsBeforeCurrent $ArgumentsBeforeCurrent) }
    }

    @()
}

function Complete-PsService {
    param(
        [string]$wordToComplete,
        [System.Management.Automation.Language.CommandAst]$commandAst,
        [int]$cursorPosition
    )

    if (-not (Test-PsServiceCommandAvailable)) {
        return @()
    }

    # $cursorPosition is a whole-line offset; the extent text is command-relative.
    $tokenState = Get-PsServiceCurrentTokenState -Line $commandAst.Extent.Text -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset)
    if ([string]::IsNullOrEmpty($wordToComplete) -and $cursorPosition -gt $commandAst.Extent.EndOffset) {
        $allTokens = @($tokenState.TokensBeforeCurrent)
        if (-not [string]::IsNullOrEmpty($tokenState.CurrentToken)) {
            $allTokens += $tokenState.CurrentToken
        }

        $tokenState = [pscustomobject]@{
            TokensBeforeCurrent = $allTokens
            CurrentToken        = ''
            HasTrailingSpace    = $true
        }
    } elseif (-not [string]::IsNullOrEmpty($wordToComplete)) {
        $tokenState = [pscustomobject]@{
            TokensBeforeCurrent = $tokenState.TokensBeforeCurrent
            CurrentToken        = $wordToComplete
            HasTrailingSpace    = $tokenState.HasTrailingSpace
        }
    }

    $argumentState = Get-PsServiceArgumentsFromTokenState -TokenState $tokenState
    $preambleContext = Get-PsServicePreambleContext -ArgumentsBeforeCurrent $argumentState.ArgumentsBeforeCurrent
    $currentWord = $argumentState.CurrentWord

    if ($preambleContext.PendingValueKind) {
        switch ($preambleContext.PendingValueKind) {
            'Username' { return @(New-PsServiceLiteralValueResults -CurrentValue $currentWord -Placeholder '<username>' -ToolTip 'Username for the remote-auth preamble.') }
            'Password' { return @(New-PsServiceLiteralValueResults -CurrentValue $currentWord -Placeholder '<password>' -ToolTip 'Password for the remote-auth preamble.') }
        }
    }

    if (-not $preambleContext.Command) {
        $results = @()
        $results += @(Get-PsServiceSwitchCompletions -Switches $script:PsServiceCompletionCatalog.RootSwitches -CurrentWord $currentWord)
        $results += @(Get-PsServiceCommandCompletions -Commands $script:PsServiceCompletionCatalog.TopLevelCommands -CurrentWord $currentWord)

        if (($null -eq $preambleContext.Server) -and ([string]::IsNullOrWhiteSpace($currentWord) -or ($currentWord -like '\\*'))) {
            $results += @(Get-PsServiceServerPlaceholderCompletions -CurrentWord $currentWord)
        }

        return @(Get-PsServiceUniqueCompletions -Results $results)
    }

    $knownCommand = $false
    foreach ($command in $script:PsServiceCompletionCatalog.TopLevelCommands) {
        if ($command.Name.Equals($preambleContext.Command, [System.StringComparison]::OrdinalIgnoreCase)) {
            $knownCommand = $true
            break
        }
    }

    if (-not $knownCommand) {
        return @(Get-PsServiceCommandCompletions -Commands $script:PsServiceCompletionCatalog.TopLevelCommands -CurrentWord $currentWord)
    }

    @(Get-PsServiceUniqueCompletions -Results (Complete-PsServiceByCommand -CommandKey $preambleContext.CommandKey -CurrentWord $currentWord -ArgumentsBeforeCurrent $preambleContext.CommandArguments))
}

Register-ArgumentCompleter -Native -CommandName 'psservice', 'psservice.exe' -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Complete-PsService -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}