from __future__ import annotations

import json
import logging as py_logging
import os
import platform
import re
import shutil
import sys
import tomllib
from dataclasses import dataclass, fields, replace
from importlib import metadata
from collections.abc import Callable, Iterable
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast

import rich_click as click
from rich.highlighter import ReprHighlighter
from rich.logging import RichHandler
from rich.text import Text
from rich.theme import Theme

from .base_types import DEFAULT_ABXPKG_LIB_DIR
from .exceptions import ABXPkgError
from .logging import (
    RICH_INSTALLED,
    configure_logging,
    format_command,
    format_exception_with_output,
    format_loaded_binary_line,
    get_logger,
    summarize_value,
)
from .cli import merge_binary_overrides, normalize_binary_overrides

if TYPE_CHECKING:
    from .binary import Binary
    from .binprovider import BinProvider

logger = get_logger(__name__)
_INITIAL_ENV = dict(os.environ)


@dataclass(slots=True)
class CliOptions:
    lib_dir: Path
    provider_names: list[str]
    dry_run: bool
    debug: bool
    no_cache: bool
    # Binary-level fields forwarded verbatim to the Binary constructor.
    # Binary's own model validators propagate them to each provider via
    # the existing install/update kwarg path, which is
    # also where the ``supports_postinstall_disable`` /
    # ``supports_min_release_age`` warning emitters live.
    min_version: str | None = None
    postinstall_scripts: bool | None = None
    min_release_age: float | None = None
    overrides: dict[str, Any] | None = None
    # Provider-level fields forwarded to every provider constructor.
    install_root: Path | None = None
    bin_dir: Path | None = None
    euid: int | None = None
    install_timeout: int | None = None
    version_timeout: int | None = None


_NONE_STRINGS = frozenset({"", "none", "null"})
_ACTIVATE_SHELL_NAMES = frozenset({"bash", "zsh", "fish"})
_CLI_OPTION_FIELD_NAMES = frozenset(field.name for field in fields(CliOptions))
_SCRIPT_DEPENDENCY_HANDLER_KEYS = frozenset(
    {"abspath", "version", "install_args", "packages"},
)


def _none_or_stripped(raw: str | None) -> str | None:
    """Return ``raw.strip()`` unless the value is the ``None`` /
    ``'None'`` / ``'null'`` / empty-string sentinel.

    Called from every CLI parser below as a single short-circuit so
    pyright can narrow ``raw`` past the ``None`` branch and each parser
    stays focused on its one-value-type conversion logic.
    """

    if raw is None:
        return None
    stripped = raw.strip()
    return None if stripped.lower() in _NONE_STRINGS else stripped


def _parse_min_version(raw: str | None) -> str | None:
    return _none_or_stripped(raw)


def _parse_cli_bool(raw: str | None) -> bool | None:
    stripped = _none_or_stripped(raw)
    if stripped is None:
        return None
    lowered = stripped.lower()
    if lowered in {"1", "true", "yes", "on"}:
        return True
    if lowered in {"0", "false", "no", "off"}:
        return False
    raise click.BadParameter(f"expected a bool or 'None', got {raw!r}")


def _parse_cli_float(raw: str | None) -> float | None:
    stripped = _none_or_stripped(raw)
    if stripped is None:
        return None
    try:
        return float(stripped)
    except ValueError as err:
        raise click.BadParameter(f"expected a float or 'None', got {raw!r}") from err


def _parse_cli_int(raw: str | None) -> int | None:
    """Parse an integer from a CLI flag, accepting ``"10"`` and ``"10.0"``.

    Rejects ``"10.5"`` (non-integer float) so typos don't silently
    truncate. Returns None for the ``None``/``null``/empty sentinels.
    """

    stripped = _none_or_stripped(raw)
    if stripped is None:
        return None
    try:
        return int(stripped)
    except ValueError:
        pass
    try:
        as_float = float(stripped)
    except ValueError as err:
        raise click.BadParameter(
            f"expected an int or 'None', got {raw!r}",
        ) from err
    as_int = int(as_float)
    if as_float != as_int:
        raise click.BadParameter(f"expected an int or 'None', got {raw!r}")
    return as_int


def _parse_overrides(raw: str | None) -> dict[str, Any] | None:
    stripped = _none_or_stripped(raw)
    if stripped is None:
        return None
    try:
        data = json.loads(stripped)
    except json.JSONDecodeError as err:
        raise click.BadParameter(f"--overrides must be valid JSON: {err}") from err
    if not isinstance(data, dict):
        raise click.BadParameter("--overrides must be a JSON object")
    return data


def _parse_handler_override(raw: str | None) -> Any:
    stripped = _none_or_stripped(raw)
    if stripped is None:
        return None
    try:
        return json.loads(stripped)
    except json.JSONDecodeError:
        return stripped


def _parse_cli_path(raw: str | None) -> Path | None:
    stripped = _none_or_stripped(raw)
    if stripped is None:
        return None
    return Path(stripped).expanduser().resolve()


# Click ``callback=`` adapter: run the supplied parser over every raw
# click value so each option's final value is already typed (bool / int
# / float / Path / dict) by the time it reaches any command callback.
# build_cli_options, build_binary, and build_providers downstream only
# ever see typed values — no string parsing below this layer.
def _click_parse(parser: Callable[[str | None], Any]) -> Callable[..., Any]:
    return lambda _ctx, _param, value: parser(value)


def parse_script_metadata(
    script_path: Path,
    max_lines: int = 50,
) -> dict[str, Any] | None:
    """Extract inline ``/// script`` metadata from a script file.

    Scans the first *max_lines* lines for a ``/// script`` opening marker
    and a closing ``///``.  Content lines between the markers are stripped
    of their leading comment prefix (everything up to and including the
    first whitespace) and the resulting text is parsed as TOML.

    Works with any single-token comment prefix (``#``, ``//``, ``--``,
    ``;``, ``/*``, …) — the prefix is never hard-coded; we simply discard
    the first whitespace-delimited token on each content line.
    """

    try:
        text = script_path.read_text(encoding="utf-8", errors="replace")
    except OSError as err:
        raise click.ClickException(f"cannot read script {script_path}: {err}") from err

    lines = text.splitlines()
    scan_limit = min(len(lines), max_lines)

    block_start: int | None = None
    for i in range(scan_limit):
        if "/// script" in lines[i]:
            block_start = i + 1
            break

    if block_start is None:
        return None

    block_end: int | None = None
    for i in range(block_start, len(lines)):
        stripped = lines[i].strip()
        if stripped.endswith("///") and "/// script" not in stripped:
            block_end = i
            break

    if block_end is None:
        return None

    toml_lines: list[str] = []
    for i in range(block_start, block_end):
        stripped = lines[i].strip()
        parts = stripped.split(None, 1)
        if len(parts) < 2:
            toml_lines.append("")
        else:
            toml_lines.append(parts[1])

    try:
        return tomllib.loads("\n".join(toml_lines))
    except Exception as err:
        raise click.ClickException(
            f"invalid TOML in /// script block of {script_path}: {err}",
        ) from err


def get_package_version() -> str:
    try:
        return metadata.version("abxpkg")
    except metadata.PackageNotFoundError:
        pyproject_path = Path(__file__).resolve().parent.parent / "pyproject.toml"
        with pyproject_path.open("rb") as pyproject_file:
            project = tomllib.load(pyproject_file)
        return str(project["project"]["version"])


def resolve_lib_dir(raw_value: str | Path | None) -> Path:
    env_value = os.environ.get("ABXPKG_LIB_DIR")
    raw_str = raw_value if isinstance(raw_value, str) else None
    if raw_str is not None and _none_or_stripped(raw_str) is None:
        os.environ.pop("ABXPKG_LIB_DIR", None)
        return DEFAULT_ABXPKG_LIB_DIR.expanduser().resolve()
    if env_value is not None and _none_or_stripped(env_value) is None:
        os.environ.pop("ABXPKG_LIB_DIR", None)
        return DEFAULT_ABXPKG_LIB_DIR.expanduser().resolve()

    lib_dir = Path(raw_value or _none_or_stripped(env_value) or DEFAULT_ABXPKG_LIB_DIR)
    resolved = lib_dir.expanduser().resolve()
    os.environ["ABXPKG_LIB_DIR"] = str(resolved)
    return resolved


def parse_provider_names(raw_value: str | None) -> list[str]:
    from . import ALL_PROVIDER_NAMES, DEFAULT_PROVIDER_NAMES, PROVIDER_CLASS_BY_NAME

    if raw_value is None:
        env_value = os.environ.get("ABXPKG_BINPROVIDERS")
        if env_value is None:
            return list(DEFAULT_PROVIDER_NAMES)
        raw_value = env_value

    provider_names: list[str] = []
    seen: set[str] = set()
    for raw_name in raw_value.split(","):
        name = raw_name.strip()
        if not name or name in seen:
            continue
        provider_names.append(name)
        seen.add(name)

    if not provider_names:
        raise click.BadParameter("expected at least one provider name")

    invalid = [name for name in provider_names if name not in PROVIDER_CLASS_BY_NAME]
    if invalid:
        valid = ", ".join(ALL_PROVIDER_NAMES)
        invalid_names = ", ".join(invalid)
        raise click.BadParameter(
            f"unknown provider name(s): {invalid_names}. Valid providers: {valid}",
        )

    return provider_names


def resolve_dry_run(flag_value: bool | None) -> bool:
    if flag_value is not None:
        return flag_value
    return env_flag_is_true("ABXPKG_DRY_RUN") or env_flag_is_true("DRY_RUN")


def resolve_debug(flag_value: bool | None) -> bool:
    if flag_value is not None:
        return flag_value
    return env_flag_is_true("ABXPKG_DEBUG")


def resolve_no_cache(flag_value: bool | None) -> bool:
    if flag_value is not None:
        return flag_value
    return env_flag_is_true("ABXPKG_NO_CACHE")


def env_flag_is_true(name: str) -> bool:
    return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}


def build_providers(
    provider_names: list[str],
    *,
    dry_run: bool = False,
    install_root: Path | None = None,
    bin_dir: Path | None = None,
    euid: int | None = None,
    install_timeout: int | None = None,
    version_timeout: int | None = None,
) -> list[BinProvider]:
    from . import PROVIDER_CLASS_BY_NAME

    providers: list[BinProvider] = []
    for provider_name in provider_names:
        provider_class = PROVIDER_CLASS_BY_NAME[provider_name]
        provider_kwargs: dict[str, Any] = {"dry_run": dry_run}
        if euid is not None:
            provider_kwargs["euid"] = euid
        if install_timeout is not None:
            provider_kwargs["install_timeout"] = install_timeout
        if version_timeout is not None:
            provider_kwargs["version_timeout"] = version_timeout
        # User-supplied --install-root overrides the provider's default.
        # Otherwise each provider resolves its own install_root from
        # ABXPKG_LIB_DIR (set by resolve_lib_dir) via default_factory.
        if install_root is not None:
            provider_kwargs["install_root"] = install_root
        if bin_dir is not None:
            provider_kwargs["bin_dir"] = bin_dir
        providers.append(provider_class(**provider_kwargs))
    return providers


def build_binary(binary_name: str, options: CliOptions, *, dry_run: bool) -> Binary:
    from . import DEFAULT_PROVIDER_NAMES, PROVIDER_CLASS_BY_NAME, Binary, EnvProvider

    provider_names = options.provider_names
    if provider_names == list(DEFAULT_PROVIDER_NAMES):
        for provider_class in PROVIDER_CLASS_BY_NAME.values():
            if provider_class.model_fields["INSTALLER_BIN"].default != binary_name:
                continue
            preferred_provider_names = getattr(
                provider_class,
                "INSTALLER_BINPROVIDERS",
                None,
            )
            if preferred_provider_names:
                provider_names = [
                    provider_name
                    for provider_name in preferred_provider_names
                    if provider_name in options.provider_names
                ]
            break

    providers = build_providers(
        provider_names,
        dry_run=dry_run,
        install_root=options.install_root,
        bin_dir=options.bin_dir,
        euid=options.euid,
        install_timeout=options.install_timeout,
        version_timeout=options.version_timeout,
    )
    explicit_abspath = Path(binary_name).expanduser()
    inferred_overrides: dict[str, Any] | None = None
    if explicit_abspath.is_absolute():
        for provider in providers:
            if type(provider) is EnvProvider:
                if provider._is_managed_by_other_provider(explicit_abspath):
                    inferred_overrides = {
                        "env": {"abspath": str(explicit_abspath)},
                    }
                else:
                    provider.PATH = provider._merge_PATH(
                        explicit_abspath.parent,
                        PATH=provider.PATH,
                        prepend=True,
                    )

    binary_kwargs: dict[str, Any] = {
        "name": binary_name,
        "binproviders": providers,
    }
    # Binary's field validators coerce str → SemVer, dict → BinaryOverrides,
    # etc., so just forward the parsed values verbatim. Binary.install /
    # update then propagate postinstall_scripts /
    # min_release_age to each provider's install() kwarg, where the
    # existing ``supports_postinstall_disable`` / ``supports_min_release_age``
    # warn-and-ignore path fires for providers that can't enforce them.
    for key, value in (
        ("min_version", options.min_version),
        ("postinstall_scripts", options.postinstall_scripts),
        ("min_release_age", options.min_release_age),
        ("overrides", merge_binary_overrides(inferred_overrides, options.overrides)),
    ):
        if value is not None:
            binary_kwargs[key] = value
    return Binary(**binary_kwargs)


def build_cli_options(
    ctx: click.Context | None,
    *,
    lib_dir: str | None,
    global_mode: bool | None,
    binproviders: str | None,
    dry_run: bool | None,
    debug: bool | None,
    no_cache: bool | None,
    min_version: str | None,
    abspath_override: Any = None,
    version_override: Any = None,
    install_args_override: Any = None,
    packages_override: Any = None,
    postinstall_scripts: bool | None,
    min_release_age: float | None,
    overrides: dict[str, Any] | None,
    install_root: Path | None,
    bin_dir: Path | None,
    euid: int | None,
    install_timeout: int | None,
    version_timeout: int | None,
) -> CliOptions:
    """Single entry-point used by the group callback and every subcommand.

    All CLI flag values arrive here already typed — click's per-option
    ``callback=`` parsers run first, so there's no string-to-bool /
    string-to-int / JSON-decode work left at this layer. Every field is
    forwarded verbatim into the returned ``CliOptions``; Binary /
    BinProvider constructors downstream honor them via the existing
    kwarg paths, and the warn-and-ignore machinery in
    ``BinProvider.__init__`` / ``BinProvider.install`` handles providers
    that can't enforce a given option.

    Subcommand-level values override the group-level values on
    ``ctx.obj['group_options']`` field-by-field; if ``ctx`` is ``None``
    (the group callback itself), values are taken as-is.
    """

    group: CliOptions | None = (
        cast(CliOptions, ctx.obj["group_options"])
        if ctx is not None and ctx.obj and "group_options" in ctx.obj
        else None
    )

    def _override(value: Any, group_value: Any) -> Any:
        """Inherit from group unless the subcommand supplied a value."""
        return group_value if value is None else value

    if global_mode is True:
        lib_dir = "None"

    handler_overrides: dict[str, Any] | None = None
    for key, value in (
        ("abspath", abspath_override),
        ("version", version_override),
        ("install_args", install_args_override),
        ("packages", packages_override),
    ):
        if value is None:
            continue
        if handler_overrides is None:
            handler_overrides = {}
        handler_overrides[key] = value

    if group is None:
        explicit_provider_selection = (
            binproviders is not None
            or os.environ.get("ABXPKG_BINPROVIDERS") is not None
        )
        provider_names = parse_provider_names(binproviders)
        if explicit_provider_selection:
            os.environ["ABXPKG_BINPROVIDERS"] = ",".join(provider_names)
        else:
            os.environ.pop("ABXPKG_BINPROVIDERS", None)
        normalized_overrides = normalize_binary_overrides(
            provider_names,
            overrides=overrides,
            handler_overrides=handler_overrides,
        )
        return CliOptions(
            lib_dir=resolve_lib_dir(lib_dir),
            provider_names=provider_names,
            dry_run=resolve_dry_run(dry_run),
            debug=resolve_debug(debug),
            no_cache=resolve_no_cache(no_cache),
            min_version=min_version,
            postinstall_scripts=postinstall_scripts,
            min_release_age=min_release_age,
            overrides=normalized_overrides,
            install_root=install_root,
            bin_dir=bin_dir,
            euid=euid,
            install_timeout=install_timeout,
            version_timeout=version_timeout,
        )
    provider_names = (
        group.provider_names
        if binproviders is None
        else parse_provider_names(binproviders)
    )
    if binproviders is not None or os.environ.get("ABXPKG_BINPROVIDERS") is not None:
        os.environ["ABXPKG_BINPROVIDERS"] = ",".join(provider_names)
    else:
        os.environ.pop("ABXPKG_BINPROVIDERS", None)
    subcommand_overrides = normalize_binary_overrides(
        provider_names,
        overrides=overrides,
        handler_overrides=handler_overrides,
    )
    return CliOptions(
        lib_dir=(
            group.lib_dir
            if lib_dir is None
            else resolve_lib_dir("None" if global_mode is True else lib_dir)
        ),
        provider_names=provider_names,
        dry_run=_override(dry_run, group.dry_run),
        debug=_override(debug, group.debug),
        no_cache=_override(no_cache, group.no_cache),
        min_version=_override(min_version, group.min_version),
        postinstall_scripts=_override(postinstall_scripts, group.postinstall_scripts),
        min_release_age=_override(min_release_age, group.min_release_age),
        overrides=merge_binary_overrides(group.overrides, subcommand_overrides),
        install_root=_override(install_root, group.install_root),
        bin_dir=_override(bin_dir, group.bin_dir),
        euid=_override(euid, group.euid),
        install_timeout=_override(install_timeout, group.install_timeout),
        version_timeout=_override(version_timeout, group.version_timeout),
    )


def _stream_is_tty(stream: Any) -> bool:
    return bool(getattr(stream, "isatty", lambda: False)())


def is_interactive_tty() -> bool:
    return _stream_is_tty(sys.stdin) and _stream_is_tty(sys.stderr)


def _console_for_stream(*, err: bool):
    stream = sys.stderr if err else sys.stdout
    if not (RICH_INSTALLED and _stream_is_tty(stream)):
        return None
    from rich.console import Console
    from rich.highlighter import ReprHighlighter

    return Console(
        file=stream,
        highlighter=ReprHighlighter(),
        theme=Theme(
            {
                "repr.str": "#87af87",
                "repr.path": "#87af87",
                "repr.filename": "#87af87",
                "repr.url": "#87af87",
            },
        ),
    )


def _echo(message: Any, *, err: bool = False) -> None:
    console = _console_for_stream(err=err)
    if console is not None:
        if isinstance(message, str):
            console.print(ReprHighlighter()(Text.from_ansi(message)), highlight=False)
        else:
            console.print(message, highlight=False)
        return
    click.echo(str(message), err=err)


class _CliRichHandler(RichHandler):
    _STDERR_PAYLOAD_PREFIX = "  \x1b[2;31m>\x1b[0m "
    _STDOUT_PAYLOAD_PREFIX = "  > "

    def render_message(
        self,
        record: py_logging.LogRecord,
        message: str,
    ) -> Text:
        contains_ansi = "\x1b[" in message
        lines = message.splitlines()
        highlighter = (
            getattr(record, "highlighter", None)
            or self.highlighter
            or getattr(self.console, "highlighter", None)
        )
        has_completed_process_payload = any(
            line.lstrip(" ").startswith("> ")
            or line.lstrip(" ").startswith(self._STDERR_PAYLOAD_PREFIX.strip())
            for line in lines
        )
        if contains_ansi or has_completed_process_payload:
            message_text = Text()
            for idx, line in enumerate(lines):
                indent = line[: len(line) - len(line.lstrip(" "))]
                stripped = line[len(indent) :]
                if stripped.startswith(self._STDERR_PAYLOAD_PREFIX.strip()):
                    message_text.append(indent)
                    message_text.append(">", style="red")
                    message_text.append(
                        " " + stripped[len(self._STDERR_PAYLOAD_PREFIX.strip()) :],
                        style="grey42",
                    )
                elif stripped.startswith(self._STDOUT_PAYLOAD_PREFIX.strip()):
                    message_text.append(indent + stripped, style="grey42")
                elif "\x1b[" in line:
                    line_text = Text.from_ansi(line)
                    if highlighter:
                        line_text = highlighter(line_text)
                    plain = line_text.plain
                    for match in re.finditer(
                        r"(?<![\w.])([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)(?=\()",
                        plain,
                    ):
                        method_name = match.group(2)
                        method_start = match.start(2)
                        method_end = match.end(2)
                        if "cache" in method_name:
                            line_text.stylize("#4e4e4e", method_start, method_end)
                        elif method_name in {
                            "install",
                            "update",
                            "uninstall",
                            "exec",
                        }:
                            line_text.stylize("#d78700", method_start, method_end)
                        elif method_name == "load" or method_name.startswith("get_"):
                            line_text.stylize("#2e8b57", method_start, method_end)
                    message_text.append_text(line_text)
                else:
                    line_text = Text(line)
                    if highlighter:
                        line_text = highlighter(line_text)
                    plain = line_text.plain
                    for match in re.finditer(
                        r"(?<![\w.])([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)(?=\()",
                        plain,
                    ):
                        method_name = match.group(2)
                        method_start = match.start(2)
                        method_end = match.end(2)
                        if "cache" in method_name:
                            line_text.stylize("#4e4e4e", method_start, method_end)
                        elif method_name in {
                            "install",
                            "update",
                            "uninstall",
                            "exec",
                        }:
                            line_text.stylize("#d78700", method_start, method_end)
                        elif method_name == "load" or method_name.startswith("get_"):
                            line_text.stylize("#2e8b57", method_start, method_end)
                    message_text.append_text(line_text)
                if idx < len(lines) - 1:
                    message_text.append("\n")
        else:
            message_text = Text.from_ansi(message)
            if highlighter:
                message_text = highlighter(message_text)
            plain = message_text.plain
            for match in re.finditer(
                r"(?<![\w.])([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)(?=\()",
                plain,
            ):
                method_name = match.group(2)
                method_start = match.start(2)
                method_end = match.end(2)
                if "cache" in method_name:
                    message_text.stylize("#4e4e4e", method_start, method_end)
                elif method_name in {"install", "update", "uninstall", "exec"}:
                    message_text.stylize("#d78700", method_start, method_end)
                elif method_name == "load" or method_name.startswith("get_"):
                    message_text.stylize("#2e8b57", method_start, method_end)
        if self.keywords is None:
            self.keywords = self.KEYWORDS
        if self.keywords:
            message_text.highlight_words(self.keywords, "logging.keyword")
        message_text.stylize("dim")
        return message_text


class _CliDuplicateStdoutFilter(py_logging.Filter):
    def filter(self, record: py_logging.LogRecord) -> bool:
        return not bool(getattr(record, "abx_cli_duplicate_stdout", False))


def configure_cli_logging(*, debug: bool) -> None:
    level = "DEBUG" if debug else "INFO"
    console = _console_for_stream(err=True)
    duplicate_stdout_filter = _CliDuplicateStdoutFilter()
    if console is not None:
        handler = _CliRichHandler(
            console=console,
            markup=False,
            show_time=False,
            show_level=False,
            show_path=False,
            highlighter=ReprHighlighter(),
        )
        handler.addFilter(duplicate_stdout_filter)
        configure_logging(
            level=level,
            handler=handler,
            fmt="%(message)s",
            replace_handlers=True,
        )
        return
    handler = py_logging.StreamHandler(sys.stderr)
    handler.addFilter(duplicate_stdout_filter)
    configure_logging(
        level=level,
        handler=handler,
        fmt="%(message)s",
        replace_handlers=True,
    )


def format_error(err: Exception) -> str:
    return format_exception_with_output(err)


def list_cached_binaries(
    options: CliOptions,
    names: tuple[str, ...] = (),
) -> list[str]:
    providers = tuple(
        build_providers(
            options.provider_names,
            dry_run=False,
            install_root=options.install_root,
            bin_dir=options.bin_dir,
            euid=options.euid,
            install_timeout=options.install_timeout,
            version_timeout=options.version_timeout,
        ),
    )
    installer_lines: list[str] = []
    binary_lines: list[str] = []
    seen_installer_lines: set[str] = set()
    seen_binary_lines: set[str] = set()
    selected_names = set(names)
    for provider in providers:
        provider_selected = provider.name in selected_names
        for binary in provider.depends_on_binaries():
            if names and not provider_selected:
                continue
            if binary.loaded_abspath is None or binary.loaded_version is None:
                continue
            resolved_provider = binary.loaded_binprovider or provider
            line = format_loaded_binary_line(
                binary.loaded_version,
                binary.loaded_abspath,
                resolved_provider.name,
                binary.name,
            )
            if line in seen_installer_lines:
                continue
            seen_installer_lines.add(line)
            installer_lines.append(line)
        for binary in provider.installed_binaries():
            if names and not (provider_selected or binary.name in selected_names):
                continue
            if binary.loaded_abspath is None or binary.loaded_version is None:
                continue
            resolved_provider = binary.loaded_binprovider or provider
            line = format_loaded_binary_line(
                binary.loaded_version,
                binary.loaded_abspath,
                resolved_provider.name,
                binary.name,
            )
            if line in seen_binary_lines or line in seen_installer_lines:
                continue
            seen_binary_lines.add(line)
            binary_lines.append(line)
    if installer_lines and binary_lines:
        return [*installer_lines, "", *binary_lines]
    return installer_lines or binary_lines


def version_report(options: CliOptions):
    from . import ALL_PROVIDER_NAMES, EnvProvider
    from .binprovider import DEFAULT_ENV_PATH, BinProvider

    highlighter = ReprHighlighter()
    all_providers = build_providers(
        list(ALL_PROVIDER_NAMES),
        dry_run=False,
        install_root=options.install_root,
        bin_dir=options.bin_dir,
        euid=options.euid,
        install_timeout=options.install_timeout,
        version_timeout=options.version_timeout,
    )
    install_timeout = all_providers[0].install_timeout if all_providers else 120
    version_timeout = all_providers[0].version_timeout if all_providers else 10
    env_provider = cast(
        EnvProvider,
        build_providers(
            ["env"],
            dry_run=False,
            install_root=options.install_root,
            bin_dir=options.bin_dir,
            euid=options.euid,
            install_timeout=options.install_timeout,
            version_timeout=options.version_timeout,
        )[0],
    )

    def render_env_value(value: Any) -> str:
        if isinstance(value, Path):
            return summarize_value(value, 10_000)
        if isinstance(value, str):
            return (
                summarize_value(value, 10_000)
                if os.sep in value or value.startswith(("~", ".", "/"))
                else value
            )
        return str(value)

    yield get_package_version()
    summary_line = Text()

    def append_part(
        text: str,
        *,
        style: str | None = None,
    ) -> None:
        if summary_line:
            summary_line.append(" ")
        summary_line.append(text, style=style)

    def append_env_var(name: str, value: Any, prefix: str = " ") -> None:
        if summary_line:
            summary_line.append(prefix)
        summary_line.append(
            f"{name}=",
            style="green" if name in _INITIAL_ENV else "grey42",
        )
        summary_line.append(render_env_value(value))

    append_part(f"ARCH={platform.machine()}")
    append_part(f"OS={platform.system()}")
    append_part(f"PLATFORM={platform.platform()}")
    append_part(
        f"PYTHON={sys.implementation.name.title()}-{'.'.join(map(str, sys.version_info[:3]))}",
    )
    append_env_var("ABXPKG_DRY_RUN", options.dry_run, prefix="\n")
    append_env_var("ABXPKG_DEBUG", options.debug)
    append_env_var("ABXPKG_NO_CACHE", options.no_cache)
    append_env_var("ABXPKG_INSTALL_TIMEOUT", install_timeout)
    append_env_var("ABXPKG_VERSION_TIMEOUT", version_timeout)
    append_env_var("ABXPKG_POSTINSTALL_SCRIPTS", options.postinstall_scripts)
    append_env_var("ABXPKG_MIN_RELEASE_AGE", options.min_release_age)
    append_env_var("ABXPKG_BINPROVIDERS", ",".join(options.provider_names))
    append_env_var("ABXPKG_LIB_DIR", options.lib_dir)
    yield summary_line
    for provider in build_providers(
        options.provider_names,
        dry_run=False,
        install_root=options.install_root,
        bin_dir=options.bin_dir,
        euid=options.euid,
        install_timeout=options.install_timeout,
        version_timeout=options.version_timeout,
    ):
        upstream_binaries = provider.depends_on_binaries()
        installed_binaries = provider.installed_binaries()
        emoji = (
            type(provider).__private_attributes__["_log_emoji"].default
            or BinProvider.__private_attributes__["_log_emoji"].default
        )
        try:
            installer_binary = env_provider.load(
                provider.INSTALLER_BIN,
                no_cache=options.no_cache,
            )
        except Exception:
            installer_binary = None
        installer_binary = installer_binary or next(
            (
                binary
                for binary in upstream_binaries
                if binary.name == provider.INSTALLER_BIN
            ),
            None,
        )
        if (
            installer_binary is None
            and provider._INSTALLER_BINARY is not None
            and provider._INSTALLER_BINARY.is_valid
        ):
            installer_binary = provider._INSTALLER_BINARY
        status = "✅" if installer_binary is not None or provider.is_valid else "❌"
        yield ""
        heading = Text()
        heading.append(f"{emoji} ", style="bold")
        heading.append(
            f"{provider.__class__.__name__} ({provider.name})",
            style="bold bright_white",
        )
        heading.append(f" {status}", style="green" if status == "✅" else "red")
        yield heading

        installer_line = Text("   ")
        installer_line.append("INSTALLER_BINARY=", style="bold cyan")
        if installer_binary and installer_binary.loaded_abspath:
            resolved_by = (
                installer_binary.loaded_binprovider.name
                if installer_binary.loaded_binprovider is not None
                else provider.name
            )
            installer_line.append(
                " ".join(
                    (
                        str(installer_binary.loaded_version or "unknown"),
                        summarize_value(installer_binary.loaded_abspath, 10_000),
                        f"({installer_binary.name})",
                        resolved_by,
                        f"euid={provider.EUID}",
                    ),
                ),
            )
        else:
            installer_line.append(f"None euid={provider.EUID}")
        yield installer_line

        path_line = Text("   ")
        path_line.append("PATH=", style="bold cyan")
        path_line.append(
            summarize_value(
                str(provider.PATH).replace(DEFAULT_ENV_PATH, "$PATH"),
                10_000,
            ),
        )
        yield path_line

        provider_env = provider.ENV
        env_line = Text("   ")
        env_line.append("ENV=", style="bold cyan")
        env_line.append(
            "{"
            + ", ".join(
                f"{key}: {summarize_value(str(value).replace(DEFAULT_ENV_PATH, '$PATH'), 10_000) if isinstance(value, str) else summarize_value(value, 10_000)}"
                for key, value in provider_env.items()
                if value is not None
            )
            + "}",
        )
        yield env_line
        install_root_line = Text("   ")
        install_root_line.append("install_root=", style="bold cyan")
        install_root_line.append(summarize_value(provider.install_root, 10_000))
        yield install_root_line
        bin_dir_line = Text("   ")
        bin_dir_line.append("bin_dir=", style="bold cyan")
        bin_dir_line.append(summarize_value(provider.bin_dir, 10_000))
        yield bin_dir_line

        upstream_binary_lines = [
            format_loaded_binary_line(
                binary.loaded_version,
                binary.loaded_abspath,
                (
                    binary.loaded_binprovider.name
                    if binary.loaded_binprovider is not None
                    else provider.name
                ),
                binary.name,
            )
            for binary in upstream_binaries
            if binary.loaded_abspath is not None and binary.loaded_version is not None
        ]
        installed_binary_lines = [
            format_loaded_binary_line(
                binary.loaded_version,
                binary.loaded_abspath,
                (
                    binary.loaded_binprovider.name
                    if binary.loaded_binprovider is not None
                    else provider.name
                ),
                binary.name,
            )
            for binary in installed_binaries
            if binary.loaded_abspath is not None and binary.loaded_version is not None
        ]

        if upstream_binary_lines:
            upstream_line = Text("   ")
            upstream_line.append("depends_on_binaries=", style="bold cyan")
            yield upstream_line
            for line in upstream_binary_lines:
                yield highlighter(Text("      " + line))

        if installed_binary_lines:
            installed_line = Text("   ")
            installed_line.append("installed_binaries=", style="bold cyan")
            yield installed_line
            for line in installed_binary_lines:
                yield highlighter(Text("      " + line))


def shared_options(command):
    # Options apply innermost-first; the --help listing order is the
    # reverse of the decoration order, so --lib / --binproviders /
    # --dry-run stay last here to preserve the pre-existing --help layout.
    # Every non-trivial option gets a ``callback=`` that runs its raw
    # string through a parser so the command receives a typed value
    # (bool / int / float / Path / dict) instead of a string.
    for decorator in (
        click.option(
            "--version-timeout",
            metavar="SECONDS",
            default=None,
            callback=_click_parse(_parse_cli_int),
            help="Seconds to wait for version/metadata probes. 'None' restores default.",
        ),
        click.option(
            "--install-timeout",
            metavar="SECONDS",
            default=None,
            callback=_click_parse(_parse_cli_int),
            help="Seconds to wait for install/update/uninstall subprocesses. 'None' restores default.",
        ),
        click.option(
            "--euid",
            metavar="UID",
            default=None,
            callback=_click_parse(_parse_cli_int),
            help="Pin the UID used when providers shell out. 'None' auto-detects.",
        ),
        click.option(
            "--bin-dir",
            metavar="PATH",
            default=None,
            callback=_click_parse(_parse_cli_path),
            help="Override the per-provider bin directory. Set 'None' to install globally.",
        ),
        click.option(
            "--install-root",
            metavar="PATH",
            default=None,
            callback=_click_parse(_parse_cli_path),
            help="Override the per-provider install directory. Set 'None' to install globally.",
        ),
        click.option(
            "--overrides",
            metavar="JSON",
            default=None,
            callback=_click_parse(_parse_overrides),
            help='JSON-encoded Binary.overrides dict, e.g. \'{"pip":{"install_args":["pkg"]}}\'. \'None\' restores defaults.',
        ),
        click.option(
            "--min-release-age",
            metavar="DAYS",
            default=None,
            callback=_click_parse(_parse_cli_float),
            help="Minimum days since publication. Providers that can't enforce it warn and ignore. 'None' restores defaults.",
        ),
        click.option(
            "--postinstall-scripts",
            metavar="BOOL",
            default=None,
            callback=_click_parse(_parse_cli_bool),
            help="Allow post-install scripts ('True'/'False'/'1'/'0'/'None' or bare `--postinstall-scripts` for implicit True). Providers that can't disable them warn and ignore.",
        ),
        click.option(
            "--min-version",
            metavar="SEMVER",
            default=None,
            callback=_click_parse(_parse_min_version),
            help="Minimum acceptable version floor for the binary. 'None' means any version is acceptable.",
        ),
        click.option(
            "--dry-run",
            metavar="BOOL",
            default=None,
            callback=_click_parse(_parse_cli_bool),
            help="Show installer commands without executing them ('True'/'False'/'None' or bare `--dry-run` for implicit True).",
        ),
        click.option(
            "--debug",
            metavar="BOOL",
            default=None,
            callback=_click_parse(_parse_cli_bool),
            help="Emit DEBUG logs to stderr ('True'/'False'/'None' or bare `--debug` for implicit True). Defaults to ABXPKG_DEBUG or False.",
        ),
        click.option(
            "--no-cache",
            metavar="BOOL",
            default=None,
            callback=_click_parse(_parse_cli_bool),
            help="Bypass cached/current-state checks and force install/update probes ('True'/'False'/'None' or bare `--no-cache` for implicit True). Defaults to ABXPKG_NO_CACHE or False.",
        ),
        click.option(
            "--binproviders",
            metavar="LIST",
            default=None,
            help="Comma-separated provider order. Defaults to ABXPKG_BINPROVIDERS or all providers.",
        ),
        click.option(
            "--global",
            "global_mode",
            default=None,
            flag_value=True,
            help="Thin alias for --lib=None. Bare --global = True.",
        ),
        click.option(
            "--lib",
            "lib_dir",
            metavar="PATH",
            default=None,
            help="Base library directory. Defaults to ABXPKG_LIB_DIR or the platformdirs user config abx/lib path.",
        ),
    ):
        command = decorator(command)
    return command


def binary_override_options(command=None, *, include_version: bool = True):
    def apply(target):
        decorators = [
            click.option(
                "--packages",
                "packages_override",
                metavar="JSON_OR_STR",
                default=None,
                callback=_click_parse(_parse_handler_override),
                help="Default packages override applied to all selected providers unless --overrides specifies a provider-specific value.",
            ),
            click.option(
                "--install-args",
                "install_args_override",
                metavar="JSON_OR_STR",
                default=None,
                callback=_click_parse(_parse_handler_override),
                help="Default install_args override applied to all selected providers unless --overrides specifies a provider-specific value.",
            ),
            click.option(
                "--abspath",
                "abspath_override",
                metavar="JSON_OR_STR",
                default=None,
                callback=_click_parse(_parse_handler_override),
                help="Default abspath override applied to all selected providers unless --overrides specifies a provider-specific value.",
            ),
        ]
        if include_version:
            decorators.append(
                click.option(
                    "--version",
                    "version_override",
                    metavar="JSON_OR_STR",
                    default=None,
                    callback=_click_parse(_parse_handler_override),
                    help="Default version override applied to all selected providers unless --overrides specifies a provider-specific value.",
                ),
            )
        for decorator in decorators:
            target = decorator(target)
        return target

    return apply(command) if command is not None else apply


# Single canonical list of kwargs carried by every CLI callback that
# uses @shared_options. Defined once so command callbacks don't have to
# enumerate all of them, and so ``get_command_options`` has a single
# source of truth for what gets forwarded to ``build_cli_options``.
_SHARED_OPTION_NAMES: tuple[str, ...] = (
    "lib_dir",
    "global_mode",
    "binproviders",
    "dry_run",
    "debug",
    "no_cache",
    "min_version",
    "abspath_override",
    "version_override",
    "install_args_override",
    "packages_override",
    "postinstall_scripts",
    "min_release_age",
    "overrides",
    "install_root",
    "bin_dir",
    "euid",
    "install_timeout",
    "version_timeout",
)


def get_command_options(
    ctx: click.Context,
    **shared_kwargs: Any,
) -> CliOptions:
    return build_cli_options(ctx, **shared_kwargs)


def apply_script_dependency_options(
    options: CliOptions,
    dep: dict[str, Any],
) -> CliOptions:
    """Apply one ``/// script`` dependency dict to a dependency's CliOptions.

    The script header is the dependency declaration, so each dependency must
    carry its own install/version/package/provider metadata into build_binary().
    ``name`` identifies the Binary and ``binproviders`` is normalized before this
    helper; every other key that is a real CliOptions field flows through here
    with the same CLI-wins precedence as the existing runtime dependency path.
    """

    dep_handler_overrides: dict[str, Any] = {}
    dep_overrides = (
        dep.get("overrides") if isinstance(dep.get("overrides"), dict) else None
    )
    replacement_kwargs: dict[str, Any] = {}

    for key, value in dep.items():
        if value is None:
            continue
        if key in _SCRIPT_DEPENDENCY_HANDLER_KEYS:
            dep_handler_overrides[key] = value
            continue
        if key == "overrides":
            continue
        if key in _CLI_OPTION_FIELD_NAMES and getattr(options, key) is None:
            if key in {"install_root", "bin_dir"}:
                value = Path(value).expanduser().resolve()
            replacement_kwargs[key] = value

    dep_binary_overrides = normalize_binary_overrides(
        options.provider_names,
        overrides=dep_overrides,
        handler_overrides=dep_handler_overrides or None,
    )
    # Dependency metadata is the closest statement of intent for that binary.
    # Merge caller-wide overrides first so per-dependency install/version/path
    # values always win and both CLI aliases and JSON overrides hit one cache key.
    replacement_kwargs["overrides"] = merge_binary_overrides(
        options.overrides,
        dep_binary_overrides,
    )

    return replace(options, **replacement_kwargs) if replacement_kwargs else options


def _expand_dependency_value(value: Any, values: dict[str, str]) -> Any:
    if isinstance(value, str):
        return re.sub(
            r"\{([A-Za-z_][A-Za-z0-9_]*)\}",
            lambda match: values.get(match.group(1), match.group(0)),
            value,
        )
    if isinstance(value, list):
        return [_expand_dependency_value(item, values) for item in value]
    if isinstance(value, dict):
        return {
            str(key): _expand_dependency_value(item, values)
            for key, item in value.items()
        }
    return value


def _deps_from_config_specs(
    raw_specs: Iterable[str],
    *,
    base_path: Path,
    options: CliOptions,
) -> list[Any]:
    deps: list[Any] = []
    values = {key: str(value) for key, value in os.environ.items()}
    values["ABXPKG_LIB_DIR"] = str(options.lib_dir)
    for raw_spec_group in raw_specs:
        for raw_spec in str(raw_spec_group or "").split(","):
            spec = raw_spec.strip()
            if not spec:
                continue
            raw_path, _, selector = spec.partition(":")
            deps_path = Path(raw_path)
            if not deps_path.is_absolute():
                deps_path = base_path / deps_path
            root = json.loads(deps_path.read_text())
            selected: Any = root
            for part in (selector or "dependencies").split("."):
                selected = selected[part]

            properties = root.get("properties") if isinstance(root, dict) else None
            if isinstance(properties, dict):
                for key, prop in properties.items():
                    if (
                        key not in values
                        and isinstance(prop, dict)
                        and "default" in prop
                    ):
                        values[str(key)] = str(prop["default"])

            selected_items = selected if isinstance(selected, list) else [selected]
            for selected_item in selected_items:
                expanded = _expand_dependency_value(selected_item, values)
                if isinstance(selected_item, dict) and isinstance(expanded, dict):
                    template_name = str(selected_item.get("name") or "").strip()
                    template_match = re.fullmatch(
                        r"\{([A-Za-z_][A-Za-z0-9_]*)\}",
                        template_name,
                    )
                    if template_match:
                        expanded = dict(expanded)
                        expanded["_abxpkg_env_key"] = template_match.group(1)
                deps.append(expanded)
    return deps


def build_deps_from_exec_env(
    deps: Iterable[Any],
    *,
    options: CliOptions,
    install_before_run: bool,
    update_before_run: bool,
    explicit_provider_selection: bool,
    base_env: dict[str, str],
) -> dict[str, str]:
    env = dict(base_env)
    for dep in deps:
        if isinstance(dep, str):
            dep_name = dep
            dep_options = options
        elif isinstance(dep, dict) and dep.get("name"):
            dep_name = str(dep["name"])
            dep_options = options
            if not explicit_provider_selection and "binproviders" in dep:
                raw_provider_names = dep["binproviders"]
                dep_options = replace(
                    dep_options,
                    provider_names=parse_provider_names(
                        ",".join(raw_provider_names)
                        if isinstance(raw_provider_names, list)
                        else str(raw_provider_names),
                    ),
                )
            dep_options = apply_script_dependency_options(dep_options, dep)
        else:
            continue

        binary, _ = resolve_runtime_binary(
            dep_name,
            options=dep_options,
            install_before_run=install_before_run,
            update_before_run=update_before_run,
        )
        env_key = dep.get("_abxpkg_env_key") if isinstance(dep, dict) else None
        if env_key and binary.loaded_abspath:
            env[str(env_key)] = str(binary.loaded_abspath)
        env = build_runtime_exec_env(
            binary,
            base_env=env,
        )
    return env


def run_binary_command(
    binary_name: str,
    *,
    action: str,
    options: CliOptions,
) -> None:
    binary = build_binary(binary_name, options, dry_run=options.dry_run)
    method = getattr(binary, action)
    configure_cli_logging(debug=options.debug)

    try:
        if action == "load":
            result = method(no_cache=options.no_cache)
        else:
            result = method(dry_run=options.dry_run, no_cache=options.no_cache)
    except ABXPkgError as err:
        raise click.ClickException(format_error(err)) from err

    if options.dry_run and action != "load":
        return

    if action == "uninstall":
        _echo(binary_name)
        return

    provider = result.loaded_binprovider
    provider_name = provider.name if provider is not None else "unknown"
    _echo(
        format_loaded_binary_line(
            result.loaded_version,
            result.loaded_abspath,
            provider_name,
            result.name,
        ),
    )


def resolve_runtime_binary(
    binary_name: str,
    *,
    options: CliOptions,
    install_before_run: bool = False,
    update_before_run: bool = False,
) -> tuple[Binary, list[BinProvider]]:
    if (
        install_before_run
        and not update_before_run
        and options.provider_names
        and options.provider_names[0] == "env"
    ):
        # Loading EnvProvider is intentionally a separate first phase. Building
        # every managed provider eagerly creates and inspects their environments,
        # which is both unnecessary and expensive when a compatible host binary
        # is already available. Only construct managed fallbacks after a real
        # host miss or version rejection.
        host_binary = build_binary(
            binary_name,
            replace(options, provider_names=["env"]),
            dry_run=options.dry_run,
        )
        try:
            host_binary = host_binary.load(no_cache=options.no_cache)
        except ABXPkgError as err:
            logger.debug("Host-first load missed %s: %s", binary_name, err)
        else:
            return host_binary, list(host_binary.binproviders)

    binary = build_binary(binary_name, options, dry_run=options.dry_run)
    update_provider_names = [
        provider_name
        for provider_name in options.provider_names
        if provider_name != "env"
    ]

    try:
        if update_before_run:
            loaded_for_update = None
            try:
                loaded_for_update = binary.load(no_cache=options.no_cache)
            except ABXPkgError:
                loaded_for_update = None
            if loaded_for_update is not None and update_provider_names:
                binary = loaded_for_update.update(
                    binproviders=update_provider_names,
                    dry_run=options.dry_run,
                    no_cache=options.no_cache,
                )
            else:
                binary = binary.update(
                    binproviders=update_provider_names or None,
                    dry_run=options.dry_run,
                    no_cache=options.no_cache,
                )
                if not binary.is_valid:
                    binary = binary.install(
                        dry_run=options.dry_run,
                        no_cache=options.no_cache,
                    )
        elif install_before_run:
            try:
                binary = binary.load(no_cache=options.no_cache)
            except ABXPkgError:
                binary = binary.install(
                    dry_run=options.dry_run,
                    no_cache=options.no_cache,
                )
        else:
            binary = binary.load(no_cache=options.no_cache)
    except ABXPkgError as err:
        raise click.ClickException(format_error(err)) from err

    return binary, build_providers(
        options.provider_names,
        dry_run=options.dry_run,
        install_root=options.install_root,
        bin_dir=options.bin_dir,
        euid=options.euid,
        install_timeout=options.install_timeout,
        version_timeout=options.version_timeout,
    )


def get_runtime_exec_providers(
    binary: Binary,
    runtime_binproviders: Iterable[BinProvider] = (),
) -> list[BinProvider]:
    if not binary.is_valid:
        raise click.ClickException(
            f"abxpkg: {binary.name}: binary could not be loaded",
        )

    assert binary.loaded_binprovider is not None
    return [
        provider
        for provider in runtime_binproviders
        if not same_runtime_provider(provider, binary.loaded_binprovider)
    ]


def same_runtime_provider(provider, loaded_provider) -> bool:
    # Dedupe by the provider fields that change runtime filesystem/env output.
    # Some tests and third-party callers provide provider-like objects that only
    # implement exec/load behavior; those should still execute instead of
    # failing during env assembly because optional layout fields are absent.
    if provider.name != loaded_provider.name:
        return False
    loaded_install_root = getattr(loaded_provider, "install_root", None)
    loaded_bin_dir = getattr(loaded_provider, "bin_dir", None)
    if loaded_install_root is None and loaded_bin_dir is None:
        return True
    return (
        getattr(provider, "install_root", None) == loaded_install_root
        and getattr(provider, "bin_dir", None) == loaded_bin_dir
    )


def build_runtime_exec_env(
    binary: Binary,
    runtime_binproviders: Iterable[BinProvider] = (),
    *,
    base_env: dict[str, str] | None = None,
) -> dict[str, str]:
    from .config import build_exec_env as build_provider_exec_env

    assert binary.loaded_binprovider is not None
    env = dict(os.environ if base_env is None else base_env)
    other_runtime_binproviders = get_runtime_exec_providers(
        binary,
        runtime_binproviders,
    )
    return build_provider_exec_env(
        # The loaded provider owns the binary being executed, so its runtime
        # env must have first precedence. Other providers still contribute
        # PATH/NODE_PATH/PYTHONPATH entries for sibling deps, but they cannot
        # replace single-value aliases like NODE_MODULES_DIR with generic roots.
        providers=[
            binary.loaded_binprovider,
            *binary.loaded_binprovider.exec_env_providers(),
            *other_runtime_binproviders,
        ],
        base_env=env,
    )


def _render_env_delta_value(
    before: str | None,
    after: str,
) -> str | None:
    if before == after:
        return None
    if before:
        if after.endswith(before):
            prefix = after[: -len(before)]
            if prefix:
                return prefix
        if after.startswith(before):
            suffix = after[len(before) :]
            if suffix:
                return f":{suffix}"
    return after


def render_env_delta_values(
    base_env: dict[str, str],
    final_env: dict[str, str],
) -> dict[str, str]:
    changed_values: dict[str, str] = {}
    for key, value in final_env.items():
        rendered = _render_env_delta_value(base_env.get(key), value)
        if rendered is not None:
            changed_values[key] = rendered

    ordered_keys = [
        *(["VIRTUAL_ENV"] if "VIRTUAL_ENV" in changed_values else []),
        *(["PATH"] if "PATH" in changed_values else []),
        *sorted(key for key in changed_values if key not in {"VIRTUAL_ENV", "PATH"}),
    ]
    return {key: changed_values[key] for key in ordered_keys}


def render_env_assignment_lines(
    base_env: dict[str, str],
    final_env: dict[str, str],
    *,
    prefix: str = "",
) -> list[str]:
    def render_assignment_value(value: str) -> str:
        if value and re.fullmatch(r"[A-Za-z0-9_./,:@%+=-]+", value):
            return value
        escaped = (
            value.replace("\\", "\\\\")
            .replace('"', '\\"')
            .replace("$", "\\$")
            .replace("`", "\\`")
            .replace("\n", "\\n")
            .replace("\r", "\\r")
            .replace("\t", "\\t")
        )
        return f'"{escaped}"'

    changed_values = render_env_delta_values(base_env, final_env)
    return [
        f"{prefix}{key}={render_assignment_value(changed_values[key])}"
        for key in changed_values
    ]


def render_activate_lines(
    base_env: dict[str, str],
    final_env: dict[str, str],
    *,
    shell: str,
) -> list[str]:
    assert shell in _ACTIVATE_SHELL_NAMES

    if shell in {"bash", "zsh"}:
        return render_env_assignment_lines(base_env, final_env, prefix="export ")

    def render_fish_value(value: str) -> str:
        if value and re.fullmatch(r"[A-Za-z0-9_./,:@%+=-]+", value):
            return value
        escaped = (
            value.replace("\\", "\\\\")
            .replace('"', '\\"')
            .replace("$", "\\$")
            .replace("\n", "\\n")
            .replace("\r", "\\r")
            .replace("\t", "\\t")
        )
        return f'"{escaped}"'

    changed_values: dict[str, str] = {}
    for key, value in final_env.items():
        rendered = _render_env_delta_value(base_env.get(key), value)
        if rendered is not None:
            changed_values[key] = rendered

    ordered_keys = [
        *(["VIRTUAL_ENV"] if "VIRTUAL_ENV" in changed_values else []),
        *(["PATH"] if "PATH" in changed_values else []),
        *sorted(key for key in changed_values if key not in {"VIRTUAL_ENV", "PATH"}),
    ]
    return [
        f"set -x {key} {render_fish_value(changed_values[key])}" for key in ordered_keys
    ]


def parse_activate_shell(
    *,
    bash: bool,
    zsh: bool,
    fish: bool,
) -> str:
    selected = [
        shell_name
        for shell_name, enabled in (("bash", bash), ("zsh", zsh), ("fish", fish))
        if enabled
    ]
    if len(selected) > 1:
        raise click.BadParameter("choose only one of --bash, --zsh, or --fish")
    return selected[0] if selected else "bash"


def render_activate_comment(
    *,
    shell: str,
    binary_names: Iterable[str],
) -> str:
    command = " ".join(
        [
            "abxpkg",
            "activate",
            *([f"--{shell}"] if shell != "bash" else []),
            *(str(binary_name) for binary_name in binary_names),
        ],
    )
    if shell == "fish":
        return f"# {command} | source"
    return f'# eval "$({command})"'


def build_command_exec_env(
    binary_names: Iterable[str],
    *,
    options: CliOptions,
    install_before_run: bool = False,
    update_before_run: bool = False,
    base_env: dict[str, str] | None = None,
) -> dict[str, str]:
    from .config import build_exec_env as build_provider_exec_env

    env = dict(os.environ if base_env is None else base_env)
    names = tuple(binary_names)
    if not names:
        providers = build_providers(
            options.provider_names,
            dry_run=options.dry_run,
            install_root=options.install_root,
            bin_dir=options.bin_dir,
            euid=options.euid,
            install_timeout=options.install_timeout,
            version_timeout=options.version_timeout,
        )
        exec_env_providers: list[BinProvider] = []

        def append_provider(provider: BinProvider | None) -> None:
            if provider is None:
                return
            if all(existing != provider for existing in exec_env_providers):
                exec_env_providers.append(provider)

        for provider in providers:
            append_provider(provider)
            try:
                installer_binary = provider.INSTALLER_BINARY(no_cache=options.no_cache)
            except Exception:
                installer_binary = None
            append_provider(
                installer_binary.loaded_binprovider if installer_binary else None,
            )
            try:
                installed_binaries = provider.installed_binaries()
            except Exception:
                installed_binaries = []
            for binary in installed_binaries:
                append_provider(binary.loaded_binprovider or provider)

        return build_provider_exec_env(
            providers=exec_env_providers,
            base_env=env,
        )

    for binary_name in names:
        binary, _ = resolve_runtime_binary(
            binary_name,
            options=options,
            install_before_run=install_before_run,
            update_before_run=update_before_run,
        )
        env = build_runtime_exec_env(
            binary,
            base_env=env,
        )
    return env


def clear_lib_dir(lib_dir: Path) -> None:
    if lib_dir.is_symlink() or lib_dir.is_file():
        lib_dir.unlink(missing_ok=True)
        return
    if lib_dir.exists():
        logger.info("$ %s", format_command(["rm", "-rf", str(lib_dir)]))
    shutil.rmtree(lib_dir, ignore_errors=True)


@click.group(
    context_settings={"help_option_names": ["-h", "--help"]},
    invoke_without_command=True,
)
@click.pass_context
@binary_override_options(include_version=False)
@shared_options
@click.option(
    "--install",
    "install_before_run",
    is_flag=True,
    default=False,
    help="Used by `run`, `env`, and `activate`: load the binary first, and install it if missing.",
)
@click.option(
    "--update",
    "update_before_run",
    is_flag=True,
    default=False,
    help="Used by `run`, `env`, and `activate`: ensure the binary is available, then update it first.",
)
@click.option(
    "--version",
    "show_version",
    is_flag=True,
    default=False,
    help="Show the abxpkg version and available installer binaries.",
)
def cli(
    ctx: click.Context,
    install_before_run: bool,
    update_before_run: bool,
    show_version: bool,
    **shared_kwargs: Any,
) -> None:
    """Manage binaries via abxpkg binproviders."""

    ctx.ensure_object(dict)
    options = build_cli_options(None, **shared_kwargs)
    ctx.obj["group_options"] = options
    ctx.obj["install_before_run"] = install_before_run
    ctx.obj["update_before_run"] = update_before_run

    if show_version:
        for line in version_report(options):
            _echo(line)
        ctx.exit()

    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())


@cli.command("version")
@click.argument("binary_name", required=False)
@click.pass_context
@shared_options
def version_command(
    ctx: click.Context,
    binary_name: str | None,
    **shared_kwargs: Any,
) -> None:
    """Show the package version report, or load a named binary."""

    options = get_command_options(ctx, **shared_kwargs)
    if binary_name is not None:
        options = replace(options, dry_run=False)
        run_binary_command(binary_name, action="load", options=options)
        return
    for line in version_report(options):
        _echo(line)


@cli.command("list")
@click.argument("names", nargs=-1)
@click.pass_context
@shared_options
def list_command(
    ctx: click.Context,
    names: tuple[str, ...],
    **shared_kwargs: Any,
) -> None:
    """List binaries installed under the configured library directory."""

    options = get_command_options(ctx, **shared_kwargs)
    lines = list_cached_binaries(options, names)
    if lines:
        _echo("\n".join(lines))


@cli.command("install")
@click.argument("binary_name")
@click.pass_context
@binary_override_options
@shared_options
def install_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    """Install a binary via the selected providers in order."""

    options = get_command_options(ctx, **shared_kwargs)
    run_binary_command(binary_name, action="install", options=options)


@cli.command("add", hidden=True)
@click.argument("binary_name")
@click.pass_context
@shared_options
def add_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    options = get_command_options(ctx, **shared_kwargs)
    run_binary_command(binary_name, action="install", options=options)


@cli.command("help", hidden=True)
@click.pass_context
def help_command(ctx: click.Context) -> None:
    parent = ctx.parent
    assert parent is not None
    _echo(parent.get_help())


@cli.command("clear")
@click.pass_context
@shared_options
def clear_command(ctx: click.Context, **shared_kwargs: Any) -> None:
    """Delete the configured library directory immediately."""

    options = get_command_options(ctx, **shared_kwargs)
    clear_lib_dir(options.lib_dir)


@cli.command("update")
@click.argument("binary_name")
@click.pass_context
@binary_override_options
@shared_options
def update_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    """Update a binary via the selected providers in order."""

    options = get_command_options(ctx, **shared_kwargs)
    run_binary_command(binary_name, action="update", options=options)


@cli.command("upgrade", hidden=True)
@click.argument("binary_name")
@click.pass_context
@binary_override_options
@shared_options
def upgrade_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    options = get_command_options(ctx, **shared_kwargs)
    run_binary_command(binary_name, action="update", options=options)


@cli.command("uninstall")
@click.argument("binary_name")
@click.pass_context
@binary_override_options
@shared_options
def uninstall_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    """Uninstall a binary via the selected providers in order."""

    options = get_command_options(ctx, **shared_kwargs)
    run_binary_command(binary_name, action="uninstall", options=options)


@cli.command("remove", hidden=True)
@click.argument("binary_name")
@click.pass_context
@shared_options
def remove_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    options = get_command_options(ctx, **shared_kwargs)
    run_binary_command(binary_name, action="uninstall", options=options)


@cli.command("load")
@click.argument("binary_name")
@click.pass_context
@binary_override_options
@shared_options
def load_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    """Load an already-installed binary via the selected providers in order."""

    options = get_command_options(ctx, **shared_kwargs)
    # Load never installs, so force dry_run off regardless of what the
    # user passed; the other option fields are preserved so min_version
    # etc. still apply.
    options = replace(options, dry_run=False)
    run_binary_command(binary_name, action="load", options=options)


@cli.command("search")
@click.argument("binary_name")
@click.pass_context
@shared_options
def search_command(
    ctx: click.Context,
    binary_name: str,
    **shared_kwargs: Any,
) -> None:
    """Search every selected provider's package index in parallel for binary_name."""

    from concurrent.futures import ThreadPoolExecutor
    from .semver import SemVer

    options = get_command_options(ctx, **shared_kwargs)
    options = replace(options, dry_run=False)
    configure_cli_logging(debug=options.debug)
    providers = build_providers(
        options.provider_names,
        dry_run=False,
        install_root=options.install_root,
        bin_dir=options.bin_dir,
        euid=options.euid,
        install_timeout=options.install_timeout,
        version_timeout=options.version_timeout,
    )
    min_version_str = options.min_version
    min_version = SemVer.parse(min_version_str) if min_version_str else None

    def _search(provider: BinProvider) -> tuple[str, list[Binary]]:
        try:
            return provider.name, list(
                provider.search(
                    binary_name,
                    min_version=min_version,
                    min_release_age=options.min_release_age,
                    no_cache=options.no_cache,
                ),
            )
        except Exception as err:
            logger.debug("%s search for %s failed: %s", provider.name, binary_name, err)
            return provider.name, []

    with ThreadPoolExecutor(max_workers=max(1, len(providers))) as pool:
        results = list(pool.map(_search, providers))

    for provider_name, matches in results:
        if not matches:
            continue
        _echo(f"[{provider_name}]")
        for match in matches:
            _echo(f"  {match.name}    {match.description}".rstrip())


def _run_command_impl(
    ctx: click.Context,
    **shared_kwargs: Any,
) -> None:
    command_install_before_run = bool(shared_kwargs.pop("command_install_before_run"))
    command_update_before_run = bool(shared_kwargs.pop("command_update_before_run"))
    script_mode = bool(shared_kwargs.pop("script_mode"))
    binary_name = cast(str, shared_kwargs.pop("binary_name"))
    binary_args = cast(tuple[str, ...], shared_kwargs.pop("binary_args"))

    if binary_name in {"--help", "-h"} and not binary_args:
        _echo(ctx.get_help())
        ctx.exit(0)
        return

    run_options = get_command_options(ctx, **shared_kwargs)
    install_before_run = bool(ctx.obj.get("install_before_run", False)) or bool(
        command_install_before_run,
    )
    update_before_run = bool(ctx.obj.get("update_before_run", False)) or bool(
        command_update_before_run,
    )

    configure_cli_logging(debug=run_options.debug)

    runtime_binproviders: list[BinProvider] = []
    binary_options = run_options

    # --script: the OS appends the script path as binary_args[0] via the
    # shebang.  Parse its /// metadata and resolve deps before normal run.
    if script_mode:
        if not binary_args:
            _echo("abxpkg: --script requires a script path", err=True)
            ctx.exit(1)
            return

        script_path = Path(binary_args[0])
        if not script_path.is_file():
            _echo(f"abxpkg: script not found: {script_path}", err=True)
            ctx.exit(1)
            return

        meta = parse_script_metadata(script_path)
        if meta is None:
            _echo(
                f"abxpkg: no /// script metadata found in {script_path}",
                err=True,
            )
            ctx.exit(1)
            return

        # Apply [tool.abxpkg] as env vars before resolving deps.
        tool_section = meta.get("tool")
        tool_config = (
            tool_section.get("abxpkg", {}) if isinstance(tool_section, dict) else {}
        )
        for key, value in tool_config.items():
            os.environ.setdefault(key, str(value))

        # Resolve all declared dependencies and collect their runtime ENV for
        # the final script execution. Provider resolution remains hermetic:
        # only the subprocess env gets the merged dependency PATH / ENV.
        explicit_provider_selection = shared_kwargs.get(
            "binproviders",
        ) is not None or os.environ.get("ABXPKG_BINPROVIDERS") not in (None, "")
        for dep in meta.get("dependencies", []):
            if isinstance(dep, str):
                dep_name = dep
                dep_options = run_options
            elif isinstance(dep, dict):
                if "name" not in dep:
                    continue
                dep_name = dep["name"]
                dep_options = run_options
                if "binproviders" in dep:
                    dep_options = replace(
                        dep_options,
                        provider_names=dep["binproviders"],
                    )
                dep_options = apply_script_dependency_options(dep_options, dep)
            else:
                continue

            if dep_name == binary_name:
                if not isinstance(dep, dict):
                    continue
                if (
                    not explicit_provider_selection
                    and "binproviders" in dep
                    and binary_options.provider_names == run_options.provider_names
                ):
                    binary_options = replace(
                        binary_options,
                        provider_names=dep["binproviders"],
                    )
                binary_options = apply_script_dependency_options(binary_options, dep)
                continue

            try:
                dep_binary = build_binary(
                    dep_name,
                    dep_options,
                    dry_run=run_options.dry_run,
                )
                dep_binary = dep_binary.install(
                    dry_run=run_options.dry_run,
                    no_cache=run_options.no_cache,
                )
                if dep_binary.loaded_binprovider:
                    runtime_binproviders.append(dep_binary.loaded_binprovider)
            except ABXPkgError as err:
                _echo(
                    f"abxpkg: failed to resolve dependency {dep_name}: "
                    f"{format_error(err)}",
                    err=True,
                )
                ctx.exit(1)
                return

        # --script implies --install
        install_before_run = True

    try:
        binary, _ = resolve_runtime_binary(
            binary_name,
            options=binary_options,
            install_before_run=install_before_run,
            update_before_run=update_before_run,
        )
    except click.ClickException as err:
        _echo(str(err), err=True)
        ctx.exit(1)
        return
    if not script_mode:
        runtime_binproviders = []
    else:
        runtime_binproviders = [*runtime_binproviders]

    if run_options.dry_run:
        # Provider exec honors dry_run and returns a no-op CompletedProcess;
        # keep the behavior consistent here so nothing is actually run.
        ctx.exit(0)
        return

    if not binary.is_valid:
        _echo(
            f"abxpkg: {binary_name}: binary could not be loaded",
            err=True,
        )
        ctx.exit(1)
        return

    # binary.is_valid guarantees both fields are set; narrow for pyright.
    assert binary.loaded_binprovider is not None
    assert binary.loaded_abspath is not None
    exec_kwargs: dict[str, Any] = {"capture_output": False}
    other_runtime_binproviders = get_runtime_exec_providers(
        binary,
        runtime_binproviders,
    )
    if other_runtime_binproviders:
        from .config import build_exec_env as build_provider_exec_env

        exec_kwargs["env"] = build_provider_exec_env(
            providers=other_runtime_binproviders,
            base_env=os.environ.copy(),
        )
    proc = binary.loaded_binprovider.exec(
        bin_name=binary.loaded_abspath,
        cmd=list(binary_args),
        **exec_kwargs,
    )
    ctx.exit(proc.returncode)


@cli.command(
    "run",
    context_settings={
        "ignore_unknown_options": True,
        "allow_extra_args": True,
        "allow_interspersed_args": False,
        "help_option_names": [],
    },
)
@binary_override_options
@shared_options
@click.option(
    "--install",
    "command_install_before_run",
    is_flag=True,
    default=False,
    help="Load the binary first, and install it if missing.",
)
@click.option(
    "--update",
    "command_update_before_run",
    is_flag=True,
    default=False,
    help="Ensure the binary is available, then update it before executing it.",
)
@click.option(
    "--script",
    "script_mode",
    is_flag=True,
    default=False,
    help="Parse inline /// script metadata from the script file and resolve dependencies before running.",
)
@click.argument("binary_name")
@click.argument("binary_args", nargs=-1, type=click.UNPROCESSED)
@click.pass_context
def run_command(
    ctx: click.Context,
    **shared_kwargs: Any,
) -> None:
    """Run an installed binary, passing all remaining arguments through to it.

    The full shared option surface is accepted on ``run`` itself before the
    binary name. Everything after the binary name is forwarded verbatim to
    the underlying binary's argv.
    """

    _run_command_impl(ctx, **shared_kwargs)


@cli.command(
    "exec",
    hidden=True,
    context_settings={
        "ignore_unknown_options": True,
        "allow_extra_args": True,
        "allow_interspersed_args": False,
        "help_option_names": [],
    },
)
@binary_override_options
@shared_options
@click.option(
    "--install",
    "command_install_before_run",
    is_flag=True,
    default=False,
)
@click.option(
    "--update",
    "command_update_before_run",
    is_flag=True,
    default=False,
)
@click.option(
    "--script",
    "script_mode",
    is_flag=True,
    default=False,
)
@click.argument("binary_name")
@click.argument("binary_args", nargs=-1, type=click.UNPROCESSED)
@click.pass_context
def exec_command(
    ctx: click.Context,
    **shared_kwargs: Any,
) -> None:
    _run_command_impl(ctx, **shared_kwargs)


@cli.command("env")
@shared_options
@click.option(
    "--install",
    "command_install_before_run",
    is_flag=True,
    default=False,
    help="Load each binary first, and install it if missing.",
)
@click.option(
    "--update",
    "command_update_before_run",
    is_flag=True,
    default=False,
    help="Ensure each binary is available, then update it before emitting the env.",
)
@click.option(
    "--json",
    "json_output",
    is_flag=True,
    default=False,
    help="Emit the environment delta as a JSON object instead of dotenv lines.",
)
@click.option(
    "--deps-from",
    "deps_from",
    multiple=True,
    default=(),
    help="Read dependency records from JSON config paths like path:required_binaries before emitting env.",
)
@click.argument("binary_names", nargs=-1)
@click.pass_context
def env_command(
    ctx: click.Context,
    binary_names: tuple[str, ...],
    **shared_kwargs: Any,
) -> None:
    """Emit env vars for the selected binaries or providers."""

    command_install_before_run = bool(shared_kwargs.pop("command_install_before_run"))
    command_update_before_run = bool(shared_kwargs.pop("command_update_before_run"))
    json_output = bool(shared_kwargs.pop("json_output"))
    deps_from = tuple(shared_kwargs.pop("deps_from") or ())
    explicit_provider_selection = shared_kwargs.get(
        "binproviders",
    ) is not None or _INITIAL_ENV.get("ABXPKG_BINPROVIDERS") not in (None, "")
    options = get_command_options(ctx, **shared_kwargs)
    install_before_run = bool(ctx.obj.get("install_before_run", False)) or bool(
        command_install_before_run,
    )
    update_before_run = bool(ctx.obj.get("update_before_run", False)) or bool(
        command_update_before_run,
    )
    configure_cli_logging(debug=options.debug)

    base_env = os.environ.copy()
    render_base_env = base_env.copy()
    if deps_from:
        base_env = build_deps_from_exec_env(
            _deps_from_config_specs(
                deps_from,
                base_path=Path.cwd(),
                options=options,
            ),
            options=options,
            install_before_run=install_before_run,
            update_before_run=update_before_run,
            explicit_provider_selection=explicit_provider_selection,
            base_env=base_env,
        )
    final_env = (
        build_command_exec_env(
            binary_names,
            options=options,
            install_before_run=install_before_run,
            update_before_run=update_before_run,
            base_env=base_env,
        )
        if binary_names or not deps_from
        else base_env
    )
    if json_output:
        _echo(json.dumps(render_env_delta_values(render_base_env, final_env), indent=2))
        return

    lines = render_env_assignment_lines(render_base_env, final_env)
    if lines:
        _echo("\n".join(lines))


@cli.command("activate")
@shared_options
@click.option(
    "--install",
    "command_install_before_run",
    is_flag=True,
    default=False,
    help="Load each binary first, and install it if missing.",
)
@click.option(
    "--update",
    "command_update_before_run",
    is_flag=True,
    default=False,
    help="Ensure each binary is available, then update it before emitting the activation script.",
)
@click.option(
    "--bash",
    "activate_bash",
    is_flag=True,
    default=False,
    help="Emit bash-compatible export lines (default).",
)
@click.option(
    "--zsh",
    "activate_zsh",
    is_flag=True,
    default=False,
    help="Emit zsh-compatible export lines.",
)
@click.option(
    "--fish",
    "activate_fish",
    is_flag=True,
    default=False,
    help="Emit fish-compatible set -x lines.",
)
@click.argument("binary_names", nargs=-1)
@click.pass_context
def activate_command(
    ctx: click.Context,
    binary_names: tuple[str, ...],
    **shared_kwargs: Any,
) -> None:
    """Emit shell commands that activate the selected abxpkg runtime env."""

    command_install_before_run = bool(shared_kwargs.pop("command_install_before_run"))
    command_update_before_run = bool(shared_kwargs.pop("command_update_before_run"))
    activate_bash = bool(shared_kwargs.pop("activate_bash"))
    activate_zsh = bool(shared_kwargs.pop("activate_zsh"))
    activate_fish = bool(shared_kwargs.pop("activate_fish"))
    options = get_command_options(ctx, **shared_kwargs)
    install_before_run = bool(ctx.obj.get("install_before_run", False)) or bool(
        command_install_before_run,
    )
    update_before_run = bool(ctx.obj.get("update_before_run", False)) or bool(
        command_update_before_run,
    )
    configure_cli_logging(debug=options.debug)
    shell = parse_activate_shell(
        bash=activate_bash,
        zsh=activate_zsh,
        fish=activate_fish,
    )

    base_env = os.environ.copy()
    final_env = build_command_exec_env(
        binary_names,
        options=options,
        install_before_run=install_before_run,
        update_before_run=update_before_run,
        base_env=base_env,
    )
    lines = [
        render_activate_comment(shell=shell, binary_names=binary_names),
        *render_activate_lines(base_env, final_env, shell=shell),
    ]
    _echo("\n".join(lines))


# Bool flags that should auto-set to True when passed bare (e.g. `--dry-run`
# with no ``=VALUE``). Pre-processing in main() / abx_main() rewrites bare
# occurrences to ``--flag=True`` so a single click string option can handle
# both the bare and the value form. Callers pass ``--dry-run=False`` or
# ``--dry-run=None`` to override the auto-True semantics.
_BARE_TRUE_BOOL_FLAGS = frozenset(
    {"--dry-run", "--debug", "--postinstall-scripts", "--no-cache"},
)

_RUN_LIKE_COMMANDS = frozenset({"run", "exec"})

_BINARY_OVERRIDE_FLAGS = frozenset(
    # The group accepts the non-conflicting binary override flags directly so
    # before-subcommand placement preserves the same precedence as JSON
    # --overrides. Only binary --version still needs hoisting because group
    # --version is reserved for the abxpkg version report.
    {"--version"},
)
_BINARY_OVERRIDE_COMMANDS = frozenset(
    {"install", "update", "upgrade", "uninstall", "load", *tuple(_RUN_LIKE_COMMANDS)},
)


def _normalize_binary_override_option_order(argv: list[str]) -> list[str]:
    """Allow binary override flags before or after the subcommand.

    Click only knows the binary-specific override flags on concrete binary
    subcommands, so a call like ``abxpkg --install-args '["black"]' upgrade
    black`` would normally fail at the group level before ``upgrade`` gets a
    chance to parse it. Hoist those flags across the first binary-managing
    subcommand so both placements normalize to the same argv.
    """

    group_opts_with_values = frozenset(
        opt
        for param in cli.params
        if isinstance(param, click.Option) and not param.is_flag
        for opt in param.opts
        if opt.startswith("--") and opt not in _BARE_TRUE_BOOL_FLAGS
    )

    prefix: list[str] = []
    hoisted: list[str] = []
    idx = 0
    while idx < len(argv):
        tok = argv[idx]
        if tok == "--":
            return argv
        if tok.startswith("--") and "=" in tok:
            opt_name = tok.split("=", 1)[0]
            if opt_name in _BINARY_OVERRIDE_FLAGS:
                hoisted.append(tok)
            else:
                prefix.append(tok)
            idx += 1
            continue
        if tok in _BINARY_OVERRIDE_FLAGS:
            hoisted.append(tok)
            if idx + 1 < len(argv):
                hoisted.append(argv[idx + 1])
                idx += 2
            else:
                idx += 1
            continue
        if tok in group_opts_with_values:
            prefix.append(tok)
            if idx + 1 < len(argv):
                prefix.append(argv[idx + 1])
                idx += 2
            else:
                idx += 1
            continue
        if tok.startswith("-") and tok != "-":
            prefix.append(tok)
            idx += 1
            continue
        if tok not in _BINARY_OVERRIDE_COMMANDS or not hoisted:
            return argv
        return [*prefix, tok, *hoisted, *argv[idx + 1 :]]
    return argv


def _expand_bare_bool_flags(argv: list[str]) -> list[str]:
    """Translate bare bool flags (``--dry-run``) into their value form
    (``--dry-run=True``) so a single click string option can handle both.

    Crucially, the rewrite stops at the run-like subcommands: every token
    after ``run`` / ``exec`` is a child binary arg that must be forwarded verbatim
    (a bare ``rsync --dry-run /src /dst`` must stay ``--dry-run``, not
    ``--dry-run=True``, because many tools reject the value form).
    """

    out: list[str] = []
    past_run = False
    skip_next = False
    for tok in argv:
        if past_run:
            out.append(tok)
        elif skip_next:
            # This token is the value of the preceding option (e.g.
            # `--lib run`), not the `run` subcommand itself.
            skip_next = False
            out.append(tok)
        elif tok in _ABXPKG_GROUP_OPTS_WITH_VALUES:
            skip_next = True
            out.append(tok)
        elif tok in _RUN_LIKE_COMMANDS:
            past_run = True
            out.append(tok)
        elif tok in _BARE_TRUE_BOOL_FLAGS:
            out.append(f"{tok}=True")
        else:
            out.append(tok)
    return out


def main() -> None:
    cli(_expand_bare_bool_flags(_normalize_binary_override_option_order(sys.argv[1:])))


# ---------------------------------------------------------------------------
# `abx` — thin alias for `abxpkg --install run ...`
# ---------------------------------------------------------------------------

# Group-level options that consume a following value (e.g. `--lib PATH`,
# `--binproviders LIST`). Derived at import time by introspecting the
# click group's own option definitions — no hardcoding — so any option
# added later via @shared_options automatically joins this set. Used by
# _split_abx_argv to know when to pull an extra token into the
# "pre-package-name" prefix; options written as `--name=value` never hit
# this code path (they're handled by the `"=" in tok` branch).
# Bare-bool-flags are click string options under the hood, but they're
# used as bare flags (``--dry-run`` = True). Exclude them so the splitter
# doesn't try to consume the next token as a value.
_ABXPKG_GROUP_OPTS_WITH_VALUES = (
    frozenset(
        opt
        for param in cli.params
        if isinstance(param, click.Option) and not param.is_flag
        for opt in param.opts
        if opt.startswith("--") and opt not in _BARE_TRUE_BOOL_FLAGS
    )
    | _BINARY_OVERRIDE_FLAGS
)

_ABX_USAGE = (
    "Usage: abx [OPTIONS] BINARY_NAME [BINARY_ARGS]...\n"
    "\n"
    "Install (if needed) and run a package-installed binary.\n"
    "Equivalent to `abxpkg run --install [OPTIONS] BINARY_NAME [BINARY_ARGS]`.\n"
    "\n"
    "Options (forwarded to abxpkg): --lib, --binproviders, --dry-run,\n"
    "--debug, "
    "--no-cache, "
    "--update, --version, --help, --abspath, --install-args, --packages.\n"
)


def _split_abx_argv(argv: list[str]) -> tuple[list[str], list[str]]:
    """Split ``argv`` around the first positional (binary name) token.

    Everything up to (and not including) the first non-option token is
    treated as `abxpkg` group options and returned as ``pre``. The binary
    name and all following tokens are returned verbatim as ``rest``.

    Options that take a separate value (`--lib PATH`, `--binproviders LIST`)
    are handled so the value token is kept with its option instead of being
    mistaken for the binary name.
    """
    pre: list[str] = []
    i = 0
    while i < len(argv):
        tok = argv[i]
        if tok == "--":
            # POSIX option terminator: everything after `--` is the binary
            # name and its arguments, never abxpkg options. Consume the
            # `--` so we don't end up injecting `--install run` *after* a
            # stray `--` that would otherwise force click to treat every
            # following token as a positional group argument.
            return pre, argv[i + 1 :]
        if tok.startswith("--") and "=" in tok:
            pre.append(tok)
            i += 1
            continue
        if tok in _ABXPKG_GROUP_OPTS_WITH_VALUES:
            pre.append(tok)
            if i + 1 < len(argv):
                pre.append(argv[i + 1])
                i += 2
            else:
                i += 1
            continue
        if tok.startswith("-") and tok != "-":
            pre.append(tok)
            i += 1
            continue
        # First non-option token: this is the binary name.
        return pre, argv[i:]
    return pre, []


def abx_main() -> None:
    """Console-script entrypoint for the thin ``abx`` alias.

    Rewrites ``abx [OPTS] BINARY [ARGS]`` into
    ``abxpkg run --install [OPTS] BINARY [ARGS]`` and hands it off to the
    existing click group. Keeps us from redefining any of the rich-click
    surface area — every option is still documented and parsed exactly
    once, by ``abxpkg`` itself.
    """
    argv = list(sys.argv[1:])
    pre, rest = _split_abx_argv(argv)
    # Expand bare bool flags only in the pre-binary-name slice; rest is
    # the child binary's argv and must be forwarded verbatim.
    pre = _expand_bare_bool_flags(pre)
    pre = ["--update" if tok == "--upgrade" else tok for tok in pre]

    if not rest:
        # No binary name given. Forward info-only flags so `abx --version`
        # and `abx --help` still do something useful; otherwise print our
        # own usage to stderr and exit 2 like click would.
        if "--version" in pre:
            _echo(get_package_version())
            return
        if any(flag in pre for flag in ("--help", "-h")):
            cli(pre)
            return
        _echo(_ABX_USAGE, err=True)
        sys.exit(2)

    # --update already implies "install if missing", so adding --install alongside
    # it is a no-op; always injecting --install keeps this wrapper stateless.
    cli(["run", "--install", *pre, *rest])


__all__ = [
    "CliOptions",
    "abx_main",
    "build_binary",
    "build_providers",
    "cli",
    "get_package_version",
    "is_interactive_tty",
    "main",
    "parse_provider_names",
    "parse_script_metadata",
    "resolve_debug",
    "resolve_dry_run",
    "resolve_no_cache",
    "resolve_lib_dir",
    "version_report",
]
