"""Custom backend for parsing command-line arguments directly from ParserSpecification.

This backend bypasses argparse entirely and parses arguments directly using the
ParserSpecification. This can be significantly faster for complex command structures
with many subcommands.
"""

from __future__ import annotations

import itertools
import sys
import warnings
from collections import deque
from dataclasses import dataclass
from typing import Any, Iterable, Literal, NoReturn, Sequence, cast

from typing_extensions import assert_never

from tyro.conf._markers import CascadeSubcommandArgs

from .. import _arguments, _errors, _parsers, _singleton, _strings, conf
from ..constructors._primitive_spec import UnsupportedTypeAnnotationError
from . import _tyro_help_formatting
from ._argparse_formatter import TyroArgumentParser
from ._base import ParserBackend

_FLAG_ACTIONS = frozenset(
    {"store_true", "store_false", "boolean_optional_action", "count"}
)


class _LiteralValue(str):
    """A token that was explicitly attached to a flag with ``=`` (e.g. the
    ``--y`` in ``--x=--y``). Such a token must be consumed as a value verbatim,
    even when it looks like a flag -- unlike a space-separated token, which a
    fixed/variable-nargs argument must not silently swallow."""


# Special float spellings that ``float()`` accepts but argparse's
# negative-number matcher misses. Matched case-insensitively.
_SPECIAL_FLOAT_BODIES = frozenset({"inf", "infinity", "nan"})


def _looks_like_negative_number(token: str) -> bool:
    """Whether ``token`` is a negative numeric value (so it should be consumed as
    a value, not treated as a flag): a single leading ``-`` followed by a digit
    or ``.`` (e.g. ``-5``, ``-.5``, ``-1e9``, ``-1j``), or one of the special
    float spellings ``-inf`` / ``-nan`` / ``-infinity`` (any capitalization). A
    ``--`` prefix is excluded, since that genuinely looks like a long flag."""
    if not (token.startswith("-") and len(token) > 1 and token[1] != "-"):
        return False
    rest = token[1:]
    return rest[0].isdigit() or rest[0] == "." or rest.lower() in _SPECIAL_FLOAT_BODIES


def _is_value_token(arg: "_arguments.ArgumentDefinition", token: str) -> bool:
    """Whether ``token`` is a known value for this argument (e.g. a dash-prefixed
    ``Literal`` value like ``-b``). Such a token is unambiguous and must be
    consumed as a value even though it looks flag-like.

    ``value_tokens`` is the complete value set for the argument -- including the
    per-position values of a heterogeneous ``Tuple[Literal[...], ...]`` that
    ``choices`` cannot represent -- so a single membership test suffices."""
    return token in arg.lowered.value_tokens


def _blocks_value_consumption(token: str, kwarg_map: "KwargMap") -> bool:
    """Whether ``token`` should NOT be consumed as an argument value: the
    end-of-options ``--`` marker, a registered flag, or a flag-like token
    (leading dash followed by an alpha character, so negative numbers like
    ``-5`` -- and special floats like ``-inf``/``-nan`` -- are still treated as
    values).

    Note: this is deliberately stricter than ``argparse``. Stock ``argparse``
    consumes whatever follows a ``nargs``-taking flag, so ``--x --verbose``
    yields ``x='--verbose'`` even when ``--verbose`` is a registered flag. We
    instead refuse a flag-like token to catch the common typo case (``--x
    --vrebose``) -- callers carve out the tokens that are genuinely values for
    the argument (negative numbers, ``=``-attached values, and registered
    choice values; see ``_is_value_token``).

    ``_LiteralValue`` tokens (supplied via ``=``) are never blocking."""
    if isinstance(token, _LiteralValue):
        return False
    if token == "--":
        return True
    token_key = token.partition("=")[0]
    if kwarg_map.contains_normalized(token_key):
        return True
    if _looks_like_negative_number(token_key):
        return False
    return (
        token_key.startswith("-")
        and len(token_key) > 1
        and token_key.lstrip("-")[:1].isalpha()
    )


class KwargMap:
    """Look-up table for tracking keyword arguments. Due to aliases, each
    argument can have multiple string representations, like -v and
    --verbose."""

    def __init__(self) -> None:
        self._arg_from_kwarg: dict[str, _arguments.ArgumentDefinition] = {}
        self._value_from_boolean_flag: dict[str, bool] = {}

        # This should be indexed with `arg.get_output_key()`, not
        # `arg.lowered.dest`. This is because `lowered.dest` is `None` for
        # positional arguments.
        self._arg_from_dest: dict[str | None, _arguments.ArgumentDefinition] = {}

    def args(self) -> Iterable[_arguments.ArgumentDefinition]:
        return self._arg_from_dest.values()

    def contains(self, kwarg: str) -> bool:
        return kwarg in self._arg_from_kwarg

    def get_kwarg(self, kwarg: str) -> _arguments.ArgumentDefinition | None:
        return self._arg_from_kwarg.get(kwarg, None)

    def push(self, arg: _arguments.ArgumentDefinition) -> None:
        self._arg_from_dest[arg.get_output_key()] = arg
        for kwarg in arg.lowered.name_or_flags:
            assert kwarg not in self._arg_from_kwarg, "Name conflict"
            self._arg_from_kwarg[kwarg] = arg

            if arg.lowered.action == "store_true":
                self._value_from_boolean_flag[kwarg] = True
            elif arg.lowered.action == "store_false":
                self._value_from_boolean_flag[kwarg] = False
            elif arg.lowered.action == "boolean_optional_action":
                self._value_from_boolean_flag[kwarg] = True
                # Short flags (like -f) cannot be inverted.
                inv_kwarg = _arguments.flag_to_inverse(kwarg)
                if inv_kwarg is not None:
                    self._value_from_boolean_flag[inv_kwarg] = False
                    assert inv_kwarg not in self._arg_from_kwarg, "Name conflict"
                    self._arg_from_kwarg[inv_kwarg] = arg

    def contains_normalized(self, token_key: str) -> bool:
        """Check if a flag key matches a known kwarg, considering
        underscore/hyphen normalization for long flags."""
        if token_key in self._arg_from_kwarg:
            return True
        if len(token_key) > 2 and token_key.startswith("--"):
            normalized = "--" + _strings.swap_delimiters(token_key[2:])
            return normalized in self._arg_from_kwarg
        return False

    def expand_short_cluster(self, token: str) -> list[str] | None:
        """POSIX-style expansion of clustered short flags.

        ``-abc`` -> ``[-a, -b, -c]``; ``-nfoo`` -> ``[-n, foo]`` when ``-n``
        takes a value; ``-vvv`` -> ``[-v, -v, -v]`` (the count-action handler
        increments once per character). Returns ``None`` if the token isn't
        a short cluster or contains an unknown character.

        The full token (including any ``=value``) should be passed in: for a
        value-taking short, the rest of the token becomes the value, and a
        single ``=`` immediately after the flag character is treated as a
        separator (``-n=foo`` -> ``foo``). The caller must already have ruled
        out an exact alias match (so explicit multi-char shorts like
        ``-cail`` are preferred).
        """
        if not (token.startswith("-") and len(token) > 2 and token[1] != "-"):
            return None
        expanded: list[str] = []
        for i, ch in enumerate(token[1:], start=1):
            arg = self._arg_from_kwarg.get("-" + ch)
            if arg is None:
                return None
            expanded.append("-" + ch)
            if arg.lowered.action not in _FLAG_ACTIONS:
                # Value-taking short: the rest of the token is its value. A
                # single `=` immediately after the value-taking flag character
                # acts as a separator (``-n=foo`` -> ``foo`` -- this matches
                # argparse; ``-abn=foo`` -> ``foo`` -- tyro strips the `=` here
                # too for consistency, whereas argparse would keep ``=foo``).
                # An `=` that is not right after the flag is part of the value
                # (``-na=foo`` -> ``a=foo``).
                rest = token[i + 1 :]
                # The glued value is an explicit value attached to the flag
                # (just like ``--x=value``), so mark it `_LiteralValue`: it must
                # be consumed verbatim even when it looks like a flag, e.g.
                # ``-n-x`` -> ``-n`` with value ``-x``.
                if rest.startswith("="):
                    # Explicit separator: the value is whatever follows, even
                    # if empty.
                    expanded.append(_LiteralValue(rest[1:]))
                elif rest:
                    expanded.append(_LiteralValue(rest))
                break
        return expanded

    def get_boolean_value(self, kwarg: str) -> bool | None:
        return self._value_from_boolean_flag.get(kwarg, None)

    def pop(self, arg: _arguments.ArgumentDefinition) -> _arguments.ArgumentDefinition:
        self._arg_from_dest.pop(arg.get_output_key())
        for kwarg_ in arg.lowered.name_or_flags:
            self._arg_from_kwarg.pop(kwarg_)
            if arg.lowered.action == "store_true":
                self._value_from_boolean_flag.pop(kwarg_)
            elif arg.lowered.action == "store_false":
                self._value_from_boolean_flag.pop(kwarg_)
            elif arg.lowered.action == "boolean_optional_action":
                # Short flags (like -f) cannot be inverted.
                inv_kwarg_ = _arguments.flag_to_inverse(kwarg_)
                self._value_from_boolean_flag.pop(kwarg_)
                if inv_kwarg_ is not None:
                    self._arg_from_kwarg.pop(inv_kwarg_)
                    self._value_from_boolean_flag.pop(inv_kwarg_)
        return arg


@dataclass
class TyroBackend(ParserBackend):
    """Backend that parses arguments directly from ParserSpecification.

    This implementation avoids the overhead of constructing an argparse parser,
    which can be significant for complex command structures with many subcommands.
    It parses command-line arguments directly using the ParserSpecification tree.
    """

    def parse_args(
        self,
        parser_spec: _parsers.ParserSpecification,
        args: Sequence[str],
        prog: str,
        return_unknown_args: bool,
        console_outputs: bool,
        add_help: bool,
        compact_help: bool = False,
    ) -> tuple[dict[str | None, Any], list[str] | None]:
        """Parse command-line arguments directly from the specification."""

        out, unknown_args_and_progs = self._parse_args_recursive(
            parser_spec,
            args,
            prog,
            console_outputs=console_outputs,
            add_help=add_help,
            return_unknown_args=return_unknown_args,
            compact_help=compact_help,
        )
        if return_unknown_args:
            return out, [x[0] for x in unknown_args_and_progs]
        else:
            # Error would have been caught earlier.
            assert len(unknown_args_and_progs) == 0
            return out, None

    def _parse_args_recursive(
        self,
        parser_spec: _parsers.ParserSpecification,
        args: Sequence[str],
        prog: str,
        console_outputs: bool,
        add_help: bool,
        return_unknown_args: bool,
        compact_help: bool = False,
    ) -> tuple[dict[str | None, Any], list[tuple[str, str]]]:
        # We'll start by setting up global values that persist across recursive calls.
        output: dict[str | None, Any] = {}
        unknown_args_and_progs: list[tuple[str, str]] = []
        subparser_frontier: dict[str, _parsers.SubparsersSpecification] = {}
        subparser_implicit_selectors: dict[str, set[str]] = {}

        def _missing_args_error(
            prog: str,
            missing_required_args: list[_tyro_help_formatting.ArgWithContext],
        ) -> NoReturn:
            """Fire the missing-args hook and report the error. Used at both the
            missing-argument sweeps below."""
            _errors._fire_and_exit(
                _errors.MissingArgs(
                    prog=prog,
                    missing_arguments=missing_required_args,
                    unrecognized_tokens=[t for t, _ in unknown_args_and_progs],
                    partial_output=cast("dict[str, Any]", dict(output)),
                ),
                console_outputs=console_outputs,
                add_help=add_help,
            )

        arg_ctx_from_dest: dict[str, _parsers.ArgWithContext] = {}

        cascaded_args: list[_tyro_help_formatting.ArgWithContext] = []

        kwarg_map = KwargMap()
        positional_args: deque[_arguments.ArgumentDefinition] = deque()

        # Track implicit subcommand selections for better error messages.
        # Maps subcommand_name -> (selected_subcommand_name, trigger_flag).
        implicit_arg_from_subcommand_name: dict[str, tuple[str, str]] = {}

        args_deque: deque[str] = deque(args)

        # Helpers for enforcing mutex groups.
        required_mutex_args: dict[
            conf._mutex_group._MutexGroupConfig, list[_arguments.ArgumentDefinition]
        ] = {}
        observed_mutex_groups: dict[
            conf._mutex_group._MutexGroupConfig,
            tuple[str, _arguments.ArgumentDefinition],
        ] = {}

        def enforce_mutex_group(
            arg: _arguments.ArgumentDefinition | None, arg_str: str
        ) -> None:
            if arg is None or arg.field.mutex_group is None:
                return

            # TODO: write some tests for combining mutually exclusive positional and keyword args.
            if arg.field.mutex_group in observed_mutex_groups:
                existing_arg_str, existing_arg = observed_mutex_groups[
                    arg.field.mutex_group
                ]
                if existing_arg is not arg:
                    _errors._fire_and_exit(
                        _errors.MutexConflict(
                            prog=prog,
                            first=existing_arg,
                            second=arg,
                            first_token=existing_arg_str,
                            second_token=arg_str,
                        ),
                        console_outputs=console_outputs,
                        add_help=add_help,
                    )
            observed_mutex_groups[arg.field.mutex_group] = (arg_str, arg)

        def _get_selectors(
            subparser_spec: _parsers.SubparsersSpecification,
            out: set[str] | None = None,
        ) -> set[str]:
            if out is None:
                out = set()
            if subparser_spec.default_name is not None:
                default_parser = subparser_spec.parser_from_name[
                    subparser_spec.default_name
                ].evaluate()
                # Error should have been caught earlier.
                assert not isinstance(default_parser, UnsupportedTypeAnnotationError), (
                    "Unexpected UnsupportedTypeAnnotationError in backend"
                )
                for arg_ctx in default_parser.get_args_including_children():
                    if arg_ctx.arg.is_positional():
                        continue
                    out.update(arg_ctx.arg.lowered.name_or_flags)
                for (
                    inner_name,
                    inner_subparsers,
                ) in default_parser.subparsers_from_intern_prefix.items():
                    # Add all the subcommand selector names from this level.
                    out.update(inner_subparsers.parser_from_name.keys())
                    # Recursively collect selectors from nested defaults.
                    _get_selectors(inner_subparsers, out=out)
            return out

        def _recurse(
            parser_spec: _parsers.ParserSpecification, local_prog: str
        ) -> None:
            # Update the subparser frontier.
            subparser_frontier.update(parser_spec.subparsers_from_intern_prefix)

            cascade = CascadeSubcommandArgs in parser_spec.markers
            for (
                intern_prefix,
                subparser_spec,
            ) in parser_spec.subparsers_from_intern_prefix.items():
                # A subparser group is eligible for implicit selection when:
                # - CascadeSubcommandArgs is set on the parent (cascade flag),
                #   in which case any flag belonging to the default subparser
                #   triggers it; or
                # - tyro.conf.subcommand(is_default=True) is set on a branch
                #   with no field default (default_instance is missing), so
                #   the user can omit the subcommand and pass its flags
                #   directly.
                if subparser_spec.default_name is None:
                    if cascade:
                        subparser_implicit_selectors[intern_prefix] = set()
                    continue
                if cascade or _singleton.is_missing(subparser_spec.default_instance):
                    subparser_implicit_selectors[intern_prefix] = _get_selectors(
                        subparser_spec
                    )

            local_args: list[_tyro_help_formatting.ArgWithContext] = []

            # Register arguments in this parser level.
            for arg_ctx in parser_spec.get_args_including_children():
                arg = arg_ctx.arg
                if arg.is_suppressed():
                    continue

                arg_ctx_from_dest[arg.get_output_key()] = arg_ctx

                # Record in full arg list. This is used for helptext generation.
                (
                    cascaded_args
                    if CascadeSubcommandArgs in arg.field.markers
                    else local_args
                ).append(arg_ctx)

                if arg.field.mutex_group is not None and arg.field.mutex_group.required:
                    required_mutex_args.setdefault(arg.field.mutex_group, []).append(
                        arg
                    )

                # Default value for special action types.
                if arg.lowered.action == "append":
                    output[
                        arg.lowered.name_or_flags[-1]
                        if arg.is_positional()
                        else arg.lowered.dest
                    ] = []
                elif arg.lowered.action == "count":
                    output[arg.lowered.dest] = arg.lowered.default

                # Register argument.
                if arg.is_positional():
                    if len(arg.lowered.name_or_flags) != 1:
                        warnings.warn(
                            f"Positional argument {arg.lowered.name_or_flags} "
                            "should have exactly one name.",
                            category=UserWarning,
                        )
                    positional_args.append(arg)
                else:
                    kwarg_map.push(arg)

            # Consume strings and use them to populate the output dict.
            subparser_found: _parsers.ParserSpecification | None = None
            subparser_found_name: str = ""
            args_to_pop: list[_arguments.ArgumentDefinition] = []
            seen_double_dash = False  # Track if we've seen '--' end-of-options marker.
            while len(args_deque) > 0:
                arg_value = args_deque.popleft()

                # Handle '--' end-of-options marker. After this, all args are
                # treated as positional (even if they look like flags).
                if arg_value == "--" and not seen_double_dash:
                    seen_double_dash = True
                    continue

                # After '--', skip all flag processing and go directly to
                # positional argument handling (or unknown args if none left).
                if seen_double_dash:
                    if len(positional_args) == 0:
                        unknown_args_and_progs.append((arg_value, local_prog))
                        continue
                    arg = positional_args.popleft()
                    args_deque.appendleft(arg_value)
                    self._consume_argument(
                        arg,
                        args_deque,
                        output,
                        kwarg_map,
                        subparser_frontier,
                        local_prog,
                        add_help=add_help,
                        console_outputs=console_outputs,
                        seen_double_dash=True,
                        # Reserve trailing values for later required positionals.
                        min_remaining_positional=self._min_positional_consumption(
                            positional_args
                        ),
                    )
                    continue

                # Support --flag_name for --flag-name by swapping delimiters.
                # Also extract the value if this is a --flag=value assignment.
                maybe_flag_delimiter_swapped: str
                equals_value: str | None = None

                if len(arg_value) > 2 and arg_value.startswith("--"):
                    if "=" in arg_value:
                        flag_part, _, equals_value = arg_value[2:].partition("=")
                        maybe_flag_delimiter_swapped = "--" + _strings.swap_delimiters(
                            flag_part
                        )
                    else:
                        maybe_flag_delimiter_swapped = "--" + _strings.swap_delimiters(
                            arg_value[2:]
                        )
                else:
                    maybe_flag_delimiter_swapped = arg_value
                    # Also handle short flags with equals, e.g., -f=value.
                    if arg_value.startswith("-") and "=" in arg_value:
                        flag_part, _, equals_value = arg_value.partition("=")
                        maybe_flag_delimiter_swapped = flag_part

                # Helptext.
                # -H and --help-verbose are only recognized when compact_help is enabled.
                help_flags = (
                    ("-h", "--help", "-H", "--help-verbose")
                    if compact_help
                    else ("-h", "--help")
                )
                if arg_value in help_flags and add_help:
                    # When compact_help is enabled, -H/--help-verbose shows full help.
                    verbose = arg_value in ("-H", "--help-verbose") or not compact_help
                    if console_outputs:
                        print(
                            *_tyro_help_formatting.format_help(
                                prog=local_prog,
                                parser_spec=parser_spec,
                                args=[
                                    arg_ctx_from_dest[arg.get_output_key()]
                                    for arg in itertools.chain(
                                        positional_args, kwarg_map.args()
                                    )
                                ],
                                subparser_frontier=subparser_frontier,
                                verbose=verbose,
                            ),
                            sep="\n",
                        )
                    sys.exit(0)

                # Handle assignments formatted as --flag=value.
                if equals_value is not None and kwarg_map.contains(
                    maybe_flag_delimiter_swapped
                ):
                    # This should also handle nargs!=1 cases like tuple[int, int].
                    # ["--tuple=1", "2"] will be broken into ["--tuple", "1", "2"].
                    # The `=`-attached value is marked literal so it is consumed
                    # verbatim even if it looks like a flag (e.g. `--x=--y`).
                    args_deque.appendleft(_LiteralValue(equals_value))
                    args_deque.appendleft(maybe_flag_delimiter_swapped)
                    continue

                # Check for subparsers in the frontier.
                intern_prefix = None
                for intern_prefix, subparser_spec in subparser_frontier.items():
                    # (1) Backwards compatibility: `None` subcommands were
                    # automatically converted to `none` in tyro<0.10.0.
                    #
                    # (2) For consistency with `--flag-name` and `--flag_name`:
                    # assuming hyphen delimiter, if the actual subcommand is
                    # `subcommand-name`, we support both `subcommand-name` and
                    # `subcommand_name`.
                    #
                    # If the actual subcommand is `subcommand_name` (via manual
                    # override) and the delimiter is `-`, we don't currently
                    # support `subcommand-name`.
                    canonical_lookup = subparser_spec.canonical_from_alias()
                    for arg_value_shim in (
                        (arg_value, _strings.swap_delimiters(arg_value))
                        if not arg_value.endswith("None")
                        else (
                            # This is backwards compatibility shim from before
                            # we supported delimiter swapping in subcommands,
                            # so we can skip the delimiter swap.
                            arg_value,
                            arg_value[:-4] + "none",
                        )
                    ):
                        if arg_value_shim not in canonical_lookup:
                            swapped = _strings.swap_delimiters(arg_value_shim)
                            if swapped in canonical_lookup:
                                arg_value_shim = swapped
                        arg_value_shim = canonical_lookup.get(
                            arg_value_shim, arg_value_shim
                        )

                        if arg_value_shim in subparser_spec.parser_from_name:
                            evaluated = subparser_spec.parser_from_name[
                                arg_value_shim
                            ].evaluate()
                            # Error should have been caught earlier.
                            assert not isinstance(
                                evaluated, UnsupportedTypeAnnotationError
                            ), "Unexpected UnsupportedTypeAnnotationError in backend"
                            subparser_found = evaluated
                            subparser_found_name = arg_value_shim
                            output[_strings.make_subparser_dest(intern_prefix)] = (
                                arg_value_shim
                            )
                            break
                    if subparser_found is not None:
                        break

                if subparser_found is not None:
                    assert intern_prefix is not None
                    subparser_frontier.pop(intern_prefix)
                    break

                # Handle normal flags.
                boolean_value = kwarg_map.get_boolean_value(
                    maybe_flag_delimiter_swapped
                )
                full_arg = kwarg_map.get_kwarg(maybe_flag_delimiter_swapped)
                enforce_mutex_group(full_arg, maybe_flag_delimiter_swapped)
                if boolean_value is not None:
                    assert full_arg is not None
                    output[full_arg.lowered.dest] = boolean_value
                    args_to_pop.append(full_arg)
                    continue
                elif full_arg is not None:
                    # Counter argument.
                    # TODO: add tests for counter arguments that are also
                    # marked as positional. The positional marker should be
                    # ignored.
                    if full_arg.lowered.action == "count":
                        dest = full_arg.lowered.dest
                        output[dest] = cast(int, output[dest]) + 1
                        continue

                    # Standard kwarg.
                    self._consume_argument(
                        full_arg,
                        args_deque,
                        output,
                        kwarg_map,
                        subparser_frontier,
                        local_prog,
                        add_help=add_help,
                        console_outputs=console_outputs,
                        min_remaining_positional=self._min_positional_consumption(
                            positional_args
                        ),
                    )
                    args_to_pop.append(full_arg)

                    # If the flag is fixed (not user-settable) but the user
                    # supplied a value-like token right after it, attribute
                    # the error to the flag instead of reporting the value
                    # as an opaque "Unrecognized option".
                    if (
                        full_arg.lowered.is_fixed()
                        and equals_value is None
                        and len(args_deque) > 0
                    ):
                        next_token = args_deque[0]
                        looks_like_flag = next_token == "--" or (
                            next_token.startswith("-")
                            and len(next_token) > 1
                            and next_token.lstrip("-")[:1].isalpha()
                        )
                        looks_like_subcommand = any(
                            next_token in group.parser_from_name
                            for group in subparser_frontier.values()
                        )
                        if not looks_like_flag and not looks_like_subcommand:
                            unexpected = args_deque.popleft()
                            _errors._fire_and_exit(
                                _errors.BadValue(
                                    prog=local_prog,
                                    argument=full_arg,
                                    reason="fixed",
                                    offending_value=str(unexpected),
                                ),
                                console_outputs=console_outputs,
                                add_help=add_help,
                            )
                    continue

                # POSIX-style short flag clustering (-abc -> -a -b -c).
                # Tried only after exact-match lookups, so registered
                # multi-char shorts like -cail still win.
                #
                # We pass the original token (including any `=value`) rather
                # than the `=`-pre-split form: for a value-taking short, the
                # `=` is only a separator when it immediately follows the flag
                # character, and `expand_short_cluster` handles this. Splitting
                # eagerly here would corrupt values like `-na=foo` (which must
                # parse to `-n` with value `a=foo`).
                expanded = kwarg_map.expand_short_cluster(arg_value)
                if expanded is not None:
                    args_deque.extendleft(reversed(expanded))
                    continue

                # Implicitly select default subcommands. Triggered by:
                # - CascadeSubcommandArgs (existing behavior), or
                # - tyro.conf.subcommand(is_default=True) on a frontier
                #   subparser (no field default, so we recurse into the
                #   default subparser and let it consume the flag).
                # Note: maybe_flag_delimiter_swapped already has the "=value"
                # part stripped out if present, so we can use it directly.
                for intern_prefix, selectors in subparser_implicit_selectors.items():
                    if maybe_flag_delimiter_swapped not in selectors:
                        continue
                    # `intern_prefix` may have already been popped from the
                    # frontier when its subcommand was explicitly selected.
                    subparser = subparser_frontier.get(intern_prefix)
                    if subparser is None:
                        continue
                    assert subparser.default_name is not None
                    # Track which subcommand names can't be selected
                    # because of some implicit selection. This will
                    # be used to improve error messages.
                    for parser_name in subparser.parser_from_name.keys():
                        implicit_arg_from_subcommand_name[parser_name] = (
                            subparser.default_name,
                            arg_value,
                        )
                    args_deque.appendleft(arg_value)
                    evaluated = subparser.parser_from_name[
                        subparser.default_name
                    ].evaluate()
                    # Error should have been caught earlier.
                    assert not isinstance(evaluated, UnsupportedTypeAnnotationError), (
                        "Unexpected UnsupportedTypeAnnotationError in backend"
                    )
                    subparser_found = evaluated
                    subparser_found_name = subparser.default_name
                    output[_strings.make_subparser_dest(subparser.intern_prefix)] = (
                        subparser.default_name
                    )
                    subparser_frontier.pop(subparser.intern_prefix)
                    break

                if subparser_found is not None:
                    break

                # Handle positional arguments.
                if len(positional_args) > 0:
                    arg = positional_args.popleft()
                    args_deque.appendleft(arg_value)
                    assert arg.lowered.dest is None
                    self._consume_argument(
                        arg,
                        args_deque,
                        output,
                        kwarg_map,
                        subparser_frontier,
                        local_prog,
                        add_help=add_help,
                        console_outputs=console_outputs,
                        # A variable-nargs positional must leave enough trailing
                        # values for the remaining required positionals.
                        min_remaining_positional=self._min_positional_consumption(
                            positional_args
                        ),
                    )
                    continue

                # If we reach here, we have an unknown argument.
                # Check if this is a subcommand that was implicitly blocked.
                if arg_value in implicit_arg_from_subcommand_name:
                    selected_name, trigger_flag = implicit_arg_from_subcommand_name[
                        arg_value
                    ]
                    _errors._fire_and_exit(
                        _errors.SubcommandConflict(
                            prog=local_prog,
                            attempted=arg_value,
                            already_selected=selected_name,
                            trigger_flag=trigger_flag,
                            is_same_subcommand=arg_value == selected_name,
                        ),
                        console_outputs=console_outputs,
                        add_help=add_help,
                    )
                unknown_args_and_progs.append((arg_value, local_prog))

            # Pop parsed arguments. We de-duplicate using `dest`.
            for arg in {arg.lowered.dest: arg for arg in args_to_pop}.values():
                kwarg_map.pop(arg)

            # Process any missing arguments.
            missing_required_args: list[_tyro_help_formatting.ArgWithContext] = []
            for arg in tuple(positional_args) + tuple(kwarg_map.args()):
                if subparser_found and CascadeSubcommandArgs in arg.field.markers:
                    continue

                # Optional arguments.
                if (
                    not arg.lowered.required
                    or arg.lowered.nargs == "?"
                    or (
                        # For positional arguments, allow empty sequences.
                        arg.is_positional() and arg.lowered.nargs == "*"
                    )
                ) and arg.lowered.action != "count":
                    if arg.is_positional():
                        positional_args.remove(arg)
                    else:
                        kwarg_map.pop(arg)
                    output[arg.get_output_key()] = arg.lowered.default
                elif arg.get_output_key() not in output:
                    missing_required_args.append(
                        arg_ctx_from_dest[arg.get_output_key()]
                    )

            # Pop missing required args from kwarg_map before recursing
            # into subparsers, so the child _recurse won't see them as
            # its own missing args.
            for arg_ctx in missing_required_args:
                arg = arg_ctx.arg
                if not arg.is_positional():
                    kwarg_map.pop(arg)

            # Parse arguments for subparser.
            if subparser_found:
                _recurse(subparser_found, local_prog + " " + subparser_found_name)

            # Raise an error if there are mising arguments in this subcommand.
            # We parse subparsers before raising this error to make sure later
            # --help flags are handled before erroring!
            #
            # https://github.com/brentyi/tyro/issues/403
            if len(missing_required_args) > 0:
                _missing_args_error(prog, missing_required_args)

        _recurse(parser_spec, prog)

        # Handle any missing/remaining arguments.
        def _check_for_missing_args() -> None:
            missing_required_args: list[_tyro_help_formatting.ArgWithContext] = []
            missing_mutex_groups = set(required_mutex_args.keys()) - set(
                observed_mutex_groups.keys()
            )
            if len(missing_mutex_groups) > 0:
                # Surface mutex-group misses through the parse-error hook, then
                # render/exit. We report every member of each unsatisfied group,
                # since any one of them would have satisfied it. Every member is
                # guaranteed to be in arg_ctx_from_dest: both that map and
                # required_mutex_args are populated together in the same
                # registration loop above, after the same is_suppressed() filter.
                _errors._fire_and_exit(
                    _errors.MissingMutexGroup(
                        prog=prog,
                        groups=[
                            [
                                arg_ctx_from_dest[arg.get_output_key()]
                                for arg in required_mutex_args[missing_group]
                            ]
                            for missing_group in missing_mutex_groups
                        ],
                        partial_output=cast("dict[str, Any]", dict(output)),
                    ),
                    console_outputs=console_outputs,
                    add_help=add_help,
                )
            # Defensive fallback: in practice `_recurse` already collects and
            # raises every missing required argument (at its own
            # `_missing_args_error` call above) before returning, so no arg ever
            # reaches this post-recursion scan as still-missing. Kept as a safety
            # net against future divergence between the two collection passes.
            for arg in itertools.chain(positional_args, kwarg_map.args()):
                if arg.get_output_key() not in output:  # pragma: no cover
                    missing_required_args.append(
                        arg_ctx_from_dest[arg.get_output_key()]
                    )

            if len(missing_required_args) > 0:  # pragma: no cover
                _missing_args_error(prog, missing_required_args)

        _check_for_missing_args()

        # Catch unrecognized arguments.
        #
        # Unlike the other failure sites, this one does NOT route through
        # _errors._fire_and_exit: the message is the output of a computed
        # fuzzy-match ("did you mean") engine, not a static description, so it
        # keeps its dedicated renderer. We fire the hook directly (gated) here.
        if not return_unknown_args and len(unknown_args_and_progs) > 0:
            if _errors._has_hook():
                _errors._fire(
                    _errors.UnrecognizedArgs(
                        prog=prog,
                        tokens=[token for token, _prog in unknown_args_and_progs],
                    )
                )
            _tyro_help_formatting.unrecognized_args_error(
                prog=prog,
                unrecognized_args_and_progs=unknown_args_and_progs,
                subparser_frontier=subparser_frontier,
                args=list(args),
                parser_spec=parser_spec,
                console_outputs=console_outputs,
                add_help=add_help,
            )

        # Handle default subcommands for frontier groups.
        # This may take multiple passes, because each default subcommand may
        # introduce more default subcommands.
        while len(subparser_frontier) > 0:
            for intern_prefix in tuple(subparser_frontier.keys()):
                dest = _strings.make_subparser_dest(intern_prefix)
                assert dest not in output
                subparser_spec = subparser_frontier.pop(intern_prefix)

                # No subcommand was selected for this group.
                if subparser_spec.default_name is None:
                    # If the subparser is not required (e.g., field has EXCLUDE_FROM_CALL
                    # as default from TypedDict total=False), skip it entirely. The field
                    # will be excluded from the result via EXCLUDE_FROM_CALL handling in
                    # _calling.py.
                    if not subparser_spec.required:
                        continue
                    # No default available and required; this is an error.
                    _errors._fire_and_exit(
                        _errors.MissingSubcommand(
                            prog=prog,
                            subcommand_spec=subparser_spec,
                            found_token=args_deque[0] if len(args_deque) > 0 else None,
                            partial_output=cast("dict[str, Any]", dict(output)),
                        ),
                        console_outputs=console_outputs,
                        add_help=add_help,
                    )

                output[dest] = subparser_spec.default_name
                evaluated = subparser_spec.parser_from_name[
                    subparser_spec.default_name
                ].evaluate()
                # Error should have been caught earlier.
                assert not isinstance(evaluated, UnsupportedTypeAnnotationError), (
                    "Unexpected UnsupportedTypeAnnotationError in backend"
                )
                _recurse(
                    evaluated,
                    local_prog=prog
                    if subparser_spec.prog_suffix == ""
                    else f"{prog} {subparser_spec.prog_suffix}",
                )

        # Check second time for missing args; there are adversarial cases where
        # the default subcommand can have them via `tyro.MISSING`.
        _check_for_missing_args()

        return output, unknown_args_and_progs

    @staticmethod
    def _min_positional_consumption(
        positional_args: deque[_arguments.ArgumentDefinition],
    ) -> int:
        """Minimum number of values needed by remaining positional args.

        Positional nargs is always int, "?", or "*" after lowering.
        We also handle None (argparse default, means 1) defensively."""
        total = 0
        for arg in positional_args:
            nargs = arg.lowered.nargs
            if isinstance(nargs, int):
                total += nargs
            elif nargs is None:  # pragma: no cover
                # None means "consume one argument" in argparse.
                total += 1
            elif nargs in ("?", "*"):
                pass
            else:
                assert_never(nargs)
        return total

    @staticmethod
    def _consume_argument(
        arg: _arguments.ArgumentDefinition,
        args_deque: deque[str],
        output: dict[str | None, Any],
        kwarg_map: KwargMap,
        subparser_frontier: dict[str, _parsers.SubparsersSpecification],
        prog: str,
        add_help: bool,
        console_outputs: bool,
        seen_double_dash: bool = False,
        min_remaining_positional: int = 0,
    ):
        arg_values: list[str] = []

        # Consume arguments based on nargs.
        # https://docs.python.org/3/library/argparse.html#nargs
        if isinstance(arg.lowered.nargs, int):
            for _ in range(arg.lowered.nargs):
                # A flag-like token or the `--` end-of-options marker is not
                # swallowed as a value (unless explicitly attached via `=`, or we
                # are past a `--`). This is intentionally stricter than argparse
                # -- which would consume `--verbose` in `--x --verbose` -- so
                # that a mistyped flag surfaces as an error instead of a
                # silently-wrong value. `_is_value_token` carves out tokens
                # that are genuinely values for this argument (e.g. a
                # dash-prefixed `Literal`).
                if len(args_deque) == 0 or (
                    not seen_double_dash
                    and _blocks_value_consumption(args_deque[0], kwarg_map)
                    and not _is_value_token(arg, args_deque[0])
                ):
                    _errors._fire_and_exit(
                        _errors.BadValue(
                            prog=prog,
                            argument=arg,
                            reason="too_few_values",
                            offending_value=None,
                        ),
                        console_outputs=console_outputs,
                        add_help=add_help,
                    )
                arg_values.append(str(args_deque.popleft()))
        elif arg.lowered.nargs in ("*", "+", "?"):
            counter = 0
            while len(args_deque) > 0:
                # A value explicitly attached via `=` is consumed verbatim,
                # bypassing the `--` and flag-like termination checks below.
                is_literal = isinstance(args_deque[0], _LiteralValue)

                # Handle '--' end-of-options marker.
                if args_deque[0] == "--" and not seen_double_dash and not is_literal:
                    # For kwargs (non-positional), '--' terminates value
                    # consumption. Leave '--' in deque for the main loop.
                    if not arg.is_positional():
                        break
                    args_deque.popleft()
                    seen_double_dash = True
                    continue

                # After '--', skip all flag-related termination checks.
                if not seen_double_dash and not is_literal:
                    # Partition on '=' to handle --flag=value syntax.
                    token_key = args_deque[0].partition("=")[0]
                    if kwarg_map.contains_normalized(token_key):
                        break

                    # A flag-like string terminates variable nargs consumption.
                    # We check for a leading alpha character after stripping
                    # dashes to avoid treating negative numbers (like -2 or
                    # -3.14) and special floats (-inf/-nan) as flags. A token
                    # that is a registered value for this argument (e.g. a
                    # dash-prefixed Literal) is also a value, not a flag.
                    if (
                        token_key.startswith("-")
                        and len(token_key) > 1
                        and token_key.lstrip("-")[:1].isalpha()
                        and not _looks_like_negative_number(token_key)
                        and not _is_value_token(arg, args_deque[0])
                    ):
                        break
                    # Break if we reach a subparser. This diverges from
                    # argparse's behavior slightly, which has tradeoffs...
                    if any(
                        args_deque[0] in group.parser_from_name
                        for group in subparser_frontier.values()
                    ):
                        # Don't break for nargs="?" to allow one value.
                        # Don't break for nargs="+" if we haven't consumed anything yet.
                        if arg.lowered.nargs not in ("?",) and not (
                            arg.lowered.nargs == "+" and counter == 0
                        ):
                            break

                # Don't consume more than one value for nargs="?".
                if arg.lowered.nargs == "?" and counter > 0:
                    break

                # Reserve values for remaining required positional args. This
                # applies to a variable-nargs keyword arg (so it doesn't eat a
                # trailing required positional) and to a variable-nargs
                # positional (so it leaves values for later positionals).
                if (
                    min_remaining_positional > 0
                    and len(args_deque) <= min_remaining_positional
                ):
                    break

                arg_values.append(str(args_deque.popleft()))
                counter += 1
            # Currently unreachable: LoweredArgumentDefinition.nargs is only ever
            # int/"*"/"?" -- "+" is never produced -- so this "at least one
            # value" guard can never fire. Kept (with its already-pragma'd
            # renderer twin at _errors.py BadValue's variadic branch) as a
            # defensive net in case nargs="+" is reintroduced.
            if arg.lowered.nargs == "+" and counter == 0:  # pragma: no cover
                _errors._fire_and_exit(
                    _errors.BadValue(
                        prog=prog,
                        argument=arg,
                        reason="too_few_values",
                        offending_value=None,
                    ),
                    console_outputs=console_outputs,
                    add_help=add_help,
                )

        # If present: make sure arguments are in choices.
        if arg.lowered.choices is not None:
            for value in arg_values:
                if value not in arg.lowered.choices:
                    _errors._fire_and_exit(
                        _errors.InvalidChoice(
                            prog=prog,
                            argument=arg,
                            value=value,
                            choices=tuple(arg.lowered.choices),
                        ),
                        console_outputs=console_outputs,
                        add_help=add_help,
                    )

        # Populate output.
        dest = arg.get_output_key()
        if arg.lowered.action == "append":
            cast(list, output[dest]).append(arg_values)
        elif arg.lowered.nargs == "?" and len(arg_values) == 1:
            # Special case for nargs="?"; this is matched in _calling.py.
            output[dest] = arg_values[0]
        else:
            output[dest] = arg_values

    def get_parser_for_completion(
        self,
        parser_spec: _parsers.ParserSpecification,
        prog: str | None,
        add_help: bool,
    ) -> TyroArgumentParser:
        """Get an argparse parser for shell completion generation.

        This method delegates to ArgparseBackend for backward compatibility
        with code that expects an argparse parser object.
        """
        from ._argparse_backend import ArgparseBackend

        return ArgparseBackend().get_parser_for_completion(
            parser_spec, prog=prog, add_help=add_help
        )

    def generate_completion(
        self,
        parser_spec: _parsers.ParserSpecification,
        prog: str,
        shell: Literal["bash", "zsh", "tcsh", "fish"],
        root_prefix: str,
    ) -> str:
        """Generate shell completion script directly from parser specification.

        The TyroBackend provides native completion generation that supports
        tyro-specific features like CascadeSubcommandArgs and frontier-based
        subcommand parsing.

        Args:
            parser_spec: Specification for the parser structure.
            prog: Program name.
            shell: Shell type ('bash' or 'zsh').
            root_prefix: Prefix for completion function names.

        Returns:
            Shell completion script as a string.
        """
        from . import _completion

        if shell == "bash":
            generator = _completion.TyroBashCompletionGenerator()
        elif shell == "zsh":
            generator = _completion.TyroZshCompletionGenerator()
        elif shell == "fish":
            generator = _completion.TyroFishCompletionGenerator()
        else:
            # No native generator (e.g. tcsh); fall back to the shtab-based
            # implementation in the base class.
            return super().generate_completion(
                parser_spec, prog=prog, shell=shell, root_prefix=root_prefix
            )

        return generator.generate(parser_spec, prog, root_prefix)
