def get_powershell_completion_script(name: str = "") -> str:
    """
    Generate a PowerShell completion script for poe.

    The script provides completion for:
    - Global CLI options (--help, -C, etc.)
    - Task names
    - Task-specific arguments (options and positionals)
    - Argument choices when defined
    - -C/--directory passthrough for alternate project paths
    """
    from pathlib import Path

    from ..app import PoeThePoet

    name = name or "poe"

    # Get global options from argparse
    app = PoeThePoet(cwd=Path().resolve())
    parser = app.ui.build_parser()

    global_opts: list[str] = []
    opts_with_values: list[str] = []
    for opt in parser._action_groups[1]._group_actions:
        if opt.help == "==SUPPRESS==":
            continue
        global_opts.extend(opt.option_strings)
        # Track options that take values
        takes_value = opt.nargs != 0 and not (
            opt.const is not None and opt.nargs is None and opt.type is None
        )
        if takes_value:
            opts_with_values.extend(opt.option_strings)

    # Build per-option exclusion map for global options filtering
    repeatable_action_types = {"_CountAction", "_AppendAction"}

    # Gather mutual exclusion groups from argparse
    argparse_excl_groups = [
        list(g._group_actions) for g in parser._mutually_exclusive_groups
    ]
    # Add implicit mutual exclusion: all count-actions conflict with each other
    count_actions = [
        opt
        for opt in parser._action_groups[1]._group_actions
        if opt.help != "==SUPPRESS==" and type(opt).__name__ == "_CountAction"
    ]
    if len(count_actions) > 1:
        argparse_excl_groups.append(count_actions)

    exclusion_map: dict[str, list[str]] = {}
    for opt in parser._action_groups[1]._group_actions:
        if opt.help == "==SUPPRESS==":
            continue
        is_repeatable = type(opt).__name__ in repeatable_action_types
        for opt_str in opt.option_strings:
            excluded: set[str] = set()
            if not is_repeatable:
                excluded.update(opt.option_strings)
            for excl_group in argparse_excl_groups:
                if opt in excl_group:
                    for partner in excl_group:
                        if partner is not opt:
                            excluded.update(partner.option_strings)
            exclusion_map[opt_str] = sorted(excluded)

    # Format as PowerShell hashtable entries
    excl_entries = []
    for opt_str, excl_list in exclusion_map.items():
        if excl_list:
            values = ", ".join(f"'{e}'" for e in excl_list)
            excl_entries.append(f"    '{opt_str}' = @({values})")
        else:
            excl_entries.append(f"    '{opt_str}' = @()")
    excl_map_str = "\n".join(excl_entries)

    global_opts_str = "', '".join(global_opts)
    opts_with_values_str = "', '".join(opts_with_values) if opts_with_values else ""

    return f"""# PowerShell completion for {name}
# Generated by poethepoet
#
# To enable, add to your PowerShell profile ($PROFILE):
#   {name} _powershell_completion | Out-String | Invoke-Expression

$script:PoeGlobalOptions = @('{global_opts_str}')
$script:PoeOptionsWithValues = @('{opts_with_values_str}')
$script:PoeGlobalOptionExclusions = @{{
{excl_map_str}
}}

function script:Get-PoeTargetPath {{
    param([string[]]$Words)

    for ($i = 1; $i -lt $Words.Count; $i++) {{
        $w = $Words[$i]
        if ($w.StartsWith('-')) {{
            # Handle --opt=value: extract base option and inline value
            $optBase = if ($w.Contains('=')) {{ $w.Substring(0, $w.IndexOf('=')) }} else {{ $w }}
            $hasEquals = $w.Contains('=')
            if ($optBase -in @('-C', '--directory', '--root')) {{
                if ($hasEquals) {{
                    return $w.Substring($w.IndexOf('=') + 1)
                }} elseif ($i + 1 -lt $Words.Count) {{
                    return $Words[$i + 1]
                }}
            }}
            # Skip value for options that take values (only if no =)
            if (-not $hasEquals -and $optBase -in $script:PoeOptionsWithValues -and ($i + 1) -lt $Words.Count) {{ $i++ }}
        }} else {{
            # First non-option word is the task — stop scanning global options
            break
        }}
    }}
    return $null
}}

function script:Get-PoeCurrentTask {{
    param([string[]]$Words)

    for ($i = 1; $i -lt $Words.Count; $i++) {{
        $word = $Words[$i]
        # Skip options
        if ($word.StartsWith('-')) {{
            $optBase = if ($word.Contains('=')) {{ $word.Substring(0, $word.IndexOf('=')) }} else {{ $word }}
            $hasEquals = $word.Contains('=')
            # Skip value for options that take values (only if no =)
            if (-not $hasEquals -and $optBase -in $script:PoeOptionsWithValues -and ($i + 1) -lt $Words.Count) {{ $i++ }}
            continue
        }}
        # First non-option word is the task
        return $word
    }}
    return $null
}}

function script:Get-PoeTaskArgs {{
    param(
        [string]$TaskName,
        [string]$TargetPath
    )

    if (-not $TaskName) {{
        return @()
    }}

    $taskArgList = @()
    try {{
        $output = if ($TargetPath) {{
            & {name} _describe_task_args $TaskName $TargetPath 2>$null
        }} else {{
            & {name} _describe_task_args $TaskName 2>$null
        }}

        if ($output) {{
            foreach ($line in $output -split "`n") {{
                $parts = $line -split "`t"
                if ($parts.Count -ge 4) {{
                    $taskArgList += @{{
                        Options = $parts[0] -split ','
                        Type = $parts[1]
                        Help = $parts[2]
                        Choices = if ($parts[3] -eq '_') {{ @() }} else {{ $parts[3] -split ' ' }}
                    }}
                }}
            }}
        }}
    }} catch {{
        # Silently fail
    }}
    return $taskArgList
}}

function script:Get-PoeTasks {{
    param([string]$TargetPath)

    try {{
        $output = if ($TargetPath) {{
            & {name} _list_tasks $TargetPath 2>$null
        }} else {{
            & {name} _list_tasks 2>$null
        }}

        if ($output) {{
            return $output -split ' ' | Where-Object {{ $_ }}
        }}
    }} catch {{
        # Silently fail
    }}
    return @()
}}

function script:Get-UsedOptions {{
    param(
        [string[]]$Words,
        [int]$TaskPosition
    )

    $used = @{{}}
    for ($i = $TaskPosition + 1; $i -lt $Words.Count; $i++) {{
        $word = $Words[$i]
        if ($word.StartsWith('-')) {{
            $used[$word] = $true
        }}
    }}
    return $used
}}

Register-ArgumentCompleter -CommandName {name} -Native -ScriptBlock {{
    param($wordToComplete, $commandAst, $cursorPosition)

    $words = $commandAst.CommandElements | ForEach-Object {{ $_.Extent.Text }}
    $curWord = $wordToComplete

    # Get target path from -C/--directory/--root
    $targetPath = Get-PoeTargetPath -Words $words

    # Find the current task (first non-option word after command)
    $currentTask = $null
    $taskPosition = -1
    for ($i = 1; $i -lt $words.Count; $i++) {{
        $word = $words[$i]
        if ($word.StartsWith('-')) {{
            $optBase = if ($word.Contains('=')) {{ $word.Substring(0, $word.IndexOf('=')) }} else {{ $word }}
            $hasEquals = $word.Contains('=')
            # Skip value for options that take values (only if no =)
            if (-not $hasEquals -and $optBase -in $script:PoeOptionsWithValues -and ($i + 1) -lt $words.Count) {{ $i++ }}
            continue
        }}
        $currentTask = $word
        $taskPosition = $i
        break
    }}

    # Determine cursor position in tokens
    $cursorIndex = $words.Count - 1
    if ($wordToComplete -eq '') {{
        $cursorIndex = $words.Count
    }}

    # Check for -- separator after task position
    $afterSeparator = $false
    if ($taskPosition -ge 0) {{
        for ($j = $taskPosition + 1; $j -lt $words.Count; $j++) {{
            if ($words[$j] -eq '--' -and $j -lt $cursorIndex) {{
                $afterSeparator = $true
                break
            }}
        }}
    }}

    # After --, only offer file completions (pass-through args to task)
    if ($afterSeparator) {{
        Get-ChildItem -Path "$curWord*" -ErrorAction SilentlyContinue |
            ForEach-Object {{
                $type = if ($_.PSIsContainer) {{ 'ProviderContainer' }} else {{ 'ProviderItem' }}
                [System.Management.Automation.CompletionResult]::new(
                    $_.FullName, $_.Name, $type, $_.FullName
                )
            }}
        return
    }}

    # Handle global option value completion — only before the task
    if ($wordToComplete -eq '' -and $words.Count -ge 1) {{ $prevWord = $words[-1] }}
    elseif ($words.Count -ge 2) {{ $prevWord = $words[-2] }}
    else {{ $prevWord = $null }}

    # Handle --opt=value style for global options
    if ((-not $currentTask -or $cursorIndex -le $taskPosition) -and $curWord -like '-*=*') {{
        $eqIdx = $curWord.IndexOf('=')
        $optPart = $curWord.Substring(0, $eqIdx)
        $valPart = $curWord.Substring($eqIdx + 1)
        $prefix = "$optPart="

        if ($optPart -in @('-C', '--directory', '--root')) {{
            Get-ChildItem -Path "$valPart*" -Directory -ErrorAction SilentlyContinue |
                ForEach-Object {{
                    [System.Management.Automation.CompletionResult]::new(
                        "$prefix$($_.FullName)", $_.Name, 'ParameterValue', $_.FullName
                    )
                }}
            return
        }}
        if ($optPart -in @('-e', '--executor')) {{
            @('auto', 'poetry', 'simple', 'uv', 'virtualenv') |
                Where-Object {{ $_ -like "$valPart*" }} |
                ForEach-Object {{
                    [System.Management.Automation.CompletionResult]::new(
                        "$prefix$_", $_, 'ParameterValue', "Executor: $_"
                    )
                }}
            return
        }}
    }}

    if ($prevWord -and (-not $currentTask -or $cursorIndex -le $taskPosition)) {{
        switch ($prevWord) {{
            {{ $_ -in @('-C', '--directory', '--root') }} {{
                # Directory completion
                Get-ChildItem -Path "$curWord*" -Directory -ErrorAction SilentlyContinue |
                    ForEach-Object {{
                        [System.Management.Automation.CompletionResult]::new(
                            $_.FullName,
                            $_.Name,
                            'ParameterValue',
                            $_.FullName
                        )
                    }}
                return
            }}
            {{ $_ -in @('-e', '--executor') }} {{
                @('auto', 'poetry', 'simple', 'uv', 'virtualenv') |
                    Where-Object {{ $_ -like "$curWord*" }} |
                    ForEach-Object {{
                        [System.Management.Automation.CompletionResult]::new(
                            $_,
                            $_,
                            'ParameterValue',
                            "Executor: $_"
                        )
                    }}
                return
            }}
        }}
    }}

    # If we haven't found a task yet, or cursor is at/before task position
    if (-not $currentTask -or $cursorIndex -le $taskPosition) {{
        # Complete global options if word starts with -
        if ($curWord.StartsWith('-')) {{
            $excludedGlobalOpts = @{{}}
            for ($j = 1; $j -lt $words.Count; $j++) {{
                $w = $words[$j]
                $optBase = if ($w.Contains('=')) {{ $w.Substring(0, $w.IndexOf('=')) }} else {{ $w }}
                if ($optBase.StartsWith('-') -and $script:PoeGlobalOptionExclusions.ContainsKey($optBase)) {{
                    foreach ($ex in $script:PoeGlobalOptionExclusions[$optBase]) {{
                        $excludedGlobalOpts[$ex] = $true
                    }}
                }}
            }}
            $script:PoeGlobalOptions |
                Where-Object {{ $_ -like "$curWord*" -and -not $excludedGlobalOpts.ContainsKey($_) }} |
                ForEach-Object {{
                    [System.Management.Automation.CompletionResult]::new(
                        $_,
                        $_,
                        'ParameterName',
                        "Global option: $_"
                    )
                }}
            return
        }}

        # Complete task names
        $tasks = Get-PoeTasks -TargetPath $targetPath
        $tasks |
            Where-Object {{ $_ -like "$curWord*" }} |
            ForEach-Object {{
                [System.Management.Automation.CompletionResult]::new(
                    $_,
                    $_,
                    'Command',
                    "Task: $_"
                )
            }}
        return
    }}

    # We're past the task - complete task-specific args
    $taskArgs = Get-PoeTaskArgs -TaskName $currentTask -TargetPath $targetPath
    $usedOpts = Get-UsedOptions -Words $words -TaskPosition $taskPosition

    # Handle --opt=value style for task options
    if ($curWord -like '-*=*') {{
        $eqIdx = $curWord.IndexOf('=')
        $optPart = $curWord.Substring(0, $eqIdx)
        $valPart = $curWord.Substring($eqIdx + 1)
        $prefix = "$optPart="
        foreach ($arg in $taskArgs) {{
            if ($optPart -in $arg.Options) {{
                if ($arg.Choices.Count -gt 0) {{
                    $arg.Choices | Where-Object {{ $_ -like "$valPart*" }} | ForEach-Object {{
                        $value = $_ -replace "^'|'$", ""
                        [System.Management.Automation.CompletionResult]::new(
                            "$prefix$value", $value, 'ParameterValue', $value
                        )
                    }}
                }}
                return
            }}
        }}
        return
    }}

    # If previous word is an option, try to complete its value
    if ($prevWord -and $prevWord.StartsWith('-')) {{
        foreach ($arg in $taskArgs) {{
            if ($prevWord -in $arg.Options) {{
                if ($arg.Type -eq 'boolean') {{
                    # Boolean flag - show remaining task options
                    break
                }}
                if ($arg.Choices.Count -gt 0) {{
                    $arg.Choices |
                        Where-Object {{ $_ -like "$curWord*" }} |
                        ForEach-Object {{
                            # Remove surrounding quotes if present
                            $value = $_ -replace "^'|'$", ""
                            [System.Management.Automation.CompletionResult]::new(
                                $value,
                                $value,
                                'ParameterValue',
                                $value
                            )
                        }}
                    return
                }}
                # No choices - don't offer completions for free-form text
                # Let the user type their custom value without interference
                return
            }}
        }}
    }}

    # Show task options if word starts with -
    if ($curWord.StartsWith('-')) {{
        # Build list of all option strings, excluding already used ones
        # Only scan words after the task position to avoid global options
        $usedGroups = @{{}}
        for ($j = $taskPosition + 1; $j -lt $words.Count; $j++) {{
            $w = $words[$j]
            if (-not $w.StartsWith('-')) {{ continue }}
            $optBase = if ($w.Contains('=')) {{ $w.Substring(0, $w.IndexOf('=')) }} else {{ $w }}
            foreach ($arg in $taskArgs) {{
                if ($optBase -in $arg.Options) {{
                    foreach ($opt in $arg.Options) {{
                        $usedGroups[$opt] = $true
                    }}
                }}
            }}
        }}

        foreach ($arg in $taskArgs) {{
            if ($arg.Type -eq 'positional') {{ continue }}
            foreach ($opt in $arg.Options) {{
                if ($usedGroups.ContainsKey($opt)) {{ continue }}
                if ($opt -like "$curWord*") {{
                    $helpText = if ($arg.Help -and $arg.Help -ne ' ') {{ $arg.Help }} else {{ "Option: $opt" }}
                    [System.Management.Automation.CompletionResult]::new(
                        $opt,
                        $opt,
                        'ParameterName',
                        $helpText
                    )
                }}
            }}
        }}
        return
    }}

    # Try positional argument choices
    $positionalIndex = 0
    for ($i = $taskPosition + 1; $i -lt $words.Count; $i++) {{
        $word = $words[$i]
        if ($word.StartsWith('-')) {{
            $optBase = if ($word.Contains('=')) {{ $word.Substring(0, $word.IndexOf('=')) }} else {{ $word }}
            $hasEquals = $word.Contains('=')
            # Check if this is a known option with value
            $isValuedOpt = $false
            foreach ($arg in $taskArgs) {{
                if ($optBase -in $arg.Options -and $arg.Type -ne 'boolean') {{
                    $isValuedOpt = $true
                    break
                }}
            }}
            if ($isValuedOpt -and -not $hasEquals) {{ $i++ }}
            continue
        }}
        # This is a positional arg (or the current word we're completing)
        if ($i -lt ($words.Count - 1) -or $wordToComplete -eq '') {{
            $positionalIndex++
        }}
    }}

    # Find positional arg at this index
    # Wrap in @() to ensure it's always an array (Where-Object returns single item directly)
    $positionalArgs = @($taskArgs | Where-Object {{ $_.Type -eq 'positional' }})
    if ($positionalIndex -lt $positionalArgs.Count) {{
        $posArg = $positionalArgs[$positionalIndex]
        if ($posArg.Choices.Count -gt 0) {{
            $posArg.Choices |
                Where-Object {{ $_ -like "$curWord*" }} |
                ForEach-Object {{
                    $value = $_ -replace "^'|'$", ""
                    [System.Management.Automation.CompletionResult]::new(
                        $value,
                        $value,
                        'ParameterValue',
                        $value
                    )
                }}
            return
        }}
    }}

    # Fall back to file completion
    Get-ChildItem -Path "$curWord*" -ErrorAction SilentlyContinue |
        ForEach-Object {{
            $type = if ($_.PSIsContainer) {{ 'ProviderContainer' }} else {{ 'ProviderItem' }}
            [System.Management.Automation.CompletionResult]::new(
                $_.FullName,
                $_.Name,
                $type,
                $_.FullName
            )
        }}
}}
"""  # noqa: E501
