# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Heuristics to detect all traits of the current environment.

This collection of heuristics is designed as a set of separate function with minimal
logic and dependencies. They're the building blocks to evaluate the current environment.

All these heuristics can be hard-cached as the underlying system is not changing
between code execution. They are still allowed to depends on each others, as long as
you're careful of not implementing circular dependencies.

```{warning}
Even if highly unlikely, it is possible to have multiple platforms detected for the
same environment.

Typical example is [Ubuntu WSL](https://documentation.ubuntu.com/wsl/), which
will make both the {func}`~extra_platforms.is_wsl2` and
{func}`~extra_platforms.is_ubuntu` functions return `True` at the same time.

That's because of the environment metadata, where:

.. code-block:: shell-session

    $ uname -a
    Linux 5.15.167.4-microsoft-standard-WSL2

    $ cat /etc/os-release
    PRETTY_NAME="Ubuntu 22.04.5 LTS"

That way we have the possibility elsewhere in `extra-platforms` to either decide
if we only allow one, and only one, heuristic to match the current system, or allow
for considering multiple systems at the same time.
```

Detection of Linux distributions relies on `/etc/os-release`, as specified by the
[`os-release` specification](https://www.freedesktop.org/software/systemd/man/latest/os-release.html).
Every modern Linux distribution (since 2012) ships this file.

For all other traits, we either rely on:

- [`sys.platform`](https://docs.python.org/3/library/sys.html#sys.platform)
- [`platform.platform`](https://docs.python.org/3/library/platform.html#platform.platform)
- [`platform.release`](https://docs.python.org/3/library/platform.html#platform.release)
- environment variables

```{todo}
`hostnamectl` could be used as a fallback detection source when
`/etc/os-release` is missing (e.g., stripped CloudLinux VMs). This approach was
[proposed upstream in python-distro](https://github.com/python-distro/distro/pull/369)
but rejected. The technique is sound and could be implemented here.
```

```{seealso}
Other source of inspiration for platform detection:

- [Rust's `sysinfo` crate](https://github.com/stanislav-tkach/os_info/tree/master/os_info/src).
```

```{currentmodule} extra_platforms
```
"""

from __future__ import annotations

import os
import platform
import re
import subprocess
import sys
from functools import cache
from os import environ
from pathlib import Path, PurePosixPath

from .platform_info import os_release_id

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Callable, Iterable

    from .trait import CI, Agent, Architecture, Platform, Shell, Terminal, Trait


_detection_registry: dict[str, Callable[[], bool]] = {}
"""Maps detection function IDs (like ``"is_bash"``) to their callables.

Populated automatically after all ``is_*()`` functions are defined. Group
detection functions generated in ``__init__.py`` are also registered here.

Used by :attr:`Trait.current <extra_platforms.Trait.current>` to look up
detection functions without a fragile string-based module attribute search.
"""


def _unrecognized_message(report: bool = True) -> str:
    """Generate a message for unrecognized environments.

    ```{important}
    This message must contain all the primitives used in the `detection` module so
    maintainers can debug heuristics from user reports.
    ```

    :param report: If `True`, append a request to report the issue on GitHub.
        Set to `False` for environments where the trait is legitimately absent
        (e.g., no terminal in CI, no CI locally).
    """
    msg = (
        "Environment:\n"
        f"  sys.platform:          {sys.platform!r}\n"
        "  platform.platform:     "
        f"{platform.platform(aliased=True, terse=True)!r}\n"
        f"  platform.release:      {platform.release()!r}\n"
        f"  platform.uname:        {platform.uname()!r}\n"
        f"  platform.machine:      {platform.machine()!r}\n"
        f"  platform.architecture: {platform.architecture()!r}\n"
        f"  os_release_id:         {os_release_id()!r}"
    )
    if report:
        msg += (
            "\n\nPlease report this at "
            "https://github.com/kdeldycke/extra-platforms/issues "
            "to improve detection heuristics."
        )
    return msg


def _report_unrecognized(
    trait_name: str,
    *,
    strict: bool,
    expected: bool = True,
) -> None:
    """Log or raise on unrecognized trait detection.

    :param trait_name: Human-readable name of the trait type (e.g., `"architecture"`).
    :param strict: If `True`, raise {exc}`SystemError` instead of logging.
    :param expected: If `True`, the trait is always expected to be detected
        (architecture, platform, shell), so an unrecognized result logs a `WARNING`
        and asks users to report the issue. If `False` (terminal, CI), the trait may
        legitimately be absent, so only `INFO` is logged without a report request.
    """
    msg = f"Unrecognized {trait_name}: {_unrecognized_message(report=expected)}"
    if strict:
        raise SystemError(msg)
    # Defer logging import to keep cold module load fast: stdlib logging pulls
    # in traceback (and in 3.14+, _colorize), which dominates import time on
    # slow architectures like i586. See issue #494.
    import logging

    logger = logging.getLogger(__name__)
    if expected:
        logger.warning(msg)
    else:
        logger.info(msg)


# =============================================================================
# Architecture detection heuristics
# =============================================================================


@cache
def is_aarch64() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.AARCH64`.

    ```{caution}
    {func}`platform.machine` returns different values depending on the OS:

    - Linux: `aarch64`
    - macOS: `arm64`
    - Windows: `ARM64`
    ```
    """
    return platform.machine().lower() in ("aarch64", "arm64")


@cache
def is_armv5tel() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.ARMV5TEL`.
    """
    return platform.machine() == "armv5tel"


@cache
def is_armv6l() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.ARMV6L`.
    """
    return platform.machine() == "armv6l"


@cache
def is_armv7l() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.ARMV7L`.
    """
    return platform.machine() == "armv7l"


@cache
def is_armv8l() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.ARMV8L`.
    """
    return platform.machine() == "armv8l"


@cache
def is_arm() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.ARM`.

    ```{hint}
    This is a fallback detection for generic ARM architecture. It will return
    `True` for any ARM architecture not specifically covered by the more precise
    variants: {func}`~extra_platforms.is_aarch64`,
    {func}`~extra_platforms.is_armv5tel`,
    {func}`~extra_platforms.is_armv6l`,
    {func}`~extra_platforms.is_armv7l` or
    {func}`~extra_platforms.is_armv8l`.
    ```
    """
    return bool(
        platform.machine().startswith("arm")
        and not any((
            is_aarch64(),
            is_armv5tel(),
            is_armv6l(),
            is_armv7l(),
            is_armv8l(),
        ))
    )


@cache
def is_i386() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.I386`."""
    return platform.machine() in ("i386", "i486")


@cache
def is_i586() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.I586`."""
    return platform.machine() == "i586"


@cache
def is_i686() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.I686`."""
    return platform.machine() == "i686"


@cache
def is_x86_64() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.X86_64`.

    ```{caution}
    Windows returns `AMD64` in uppercase, so we normalize to lowercase.
    ```
    """
    return platform.machine().lower() in ("x86_64", "amd64")


@cache
def is_mips() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.MIPS`."""
    return platform.machine() == "mips"


@cache
def is_mipsel() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.MIPSEL`.
    """
    return platform.machine() == "mipsel"


@cache
def is_mips64() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.MIPS64`.
    """
    return platform.machine() == "mips64"


@cache
def is_mips64el() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.MIPS64EL`.
    """
    return platform.machine() == "mips64el"


@cache
def is_ppc() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.PPC`."""
    return platform.machine() in ("ppc", "powerpc")


@cache
def is_ppc64() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.PPC64`."""
    return platform.machine() == "ppc64"


@cache
def is_ppc64le() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.PPC64LE`.
    """
    return platform.machine() == "ppc64le"


@cache
def is_riscv32() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.RISCV32`.
    """
    return platform.machine() == "riscv32"


@cache
def is_riscv64() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.RISCV64`.
    """
    return platform.machine() == "riscv64"


@cache
def is_sparc() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.SPARC`."""
    return platform.machine() == "sparc"


@cache
def is_sparc64() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.SPARC64`.
    """
    return platform.machine() in ("sparc64", "sun4u", "sun4v")


@cache
def is_s390x() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.S390X`."""
    return platform.machine() == "s390x"


@cache
def is_loongarch64() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.LOONGARCH64`.
    """
    return platform.machine() == "loongarch64"


@cache
def is_wasm32() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.WASM32`.

    ```{hint}
    WebAssembly detection is based on Emscripten's platform identifier.
    ```
    """
    return sys.platform == "emscripten" and platform.architecture()[0] == "32bit"


@cache
def is_wasm64() -> bool:
    """Return {data}`True` if current architecture is {data}`~extra_platforms.WASM64`.

    ```{hint}
    WebAssembly detection is based on Emscripten's platform identifier.
    ```
    """
    return sys.platform == "emscripten" and platform.architecture()[0] == "64bit"


@cache
def is_unknown_architecture() -> bool:
    """Return {data}`True` if current architecture is
    {data}`~extra_platforms.UNKNOWN_ARCHITECTURE`.
    """
    # Lazy import to avoid circular dependencies.
    from .architecture_data import UNKNOWN_ARCHITECTURE

    return current_architecture() is UNKNOWN_ARCHITECTURE


# =============================================================================
# Platform detection heuristics
# =============================================================================


@cache
def is_aix() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.AIX`."""
    return sys.platform.startswith("aix") or os_release_id() == "aix"


@cache
def is_alpine() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ALPINE`."""
    return os_release_id() == "alpine"


@cache
def is_altlinux() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ALTLINUX`."""
    return os_release_id() == "altlinux"


@cache
def is_amzn() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.AMZN`."""
    return os_release_id() == "amzn"


@cache
def is_android() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ANDROID`.

    ```{seealso}
    Source:
    <https://github.com/kivy/kivy/blob/master/kivy/utils.py>
    ```
    """
    return "ANDROID_ROOT" in environ or "P4A_BOOTSTRAP" in environ


@cache
def is_arch() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ARCH`."""
    return os_release_id() == "arch"


@cache
def is_buildroot() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.BUILDROOT`."""
    return os_release_id() == "buildroot"


@cache
def is_cachyos() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.CACHYOS`."""
    return os_release_id() == "cachyos"


@cache
def is_centos() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.CENTOS`."""
    return os_release_id() == "centos"


@cache
def is_cloudlinux() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.CLOUDLINUX`.
    """
    return os_release_id() == "cloudlinux"


@cache
def is_cygwin() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.CYGWIN`."""
    return sys.platform.startswith("cygwin")


@cache
def is_debian() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.DEBIAN`."""
    return os_release_id() == "debian"


@cache
def is_dragonfly_bsd() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.DRAGONFLY_BSD`.
    """
    return sys.platform.startswith("dragonfly")


@cache
def is_exherbo() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.EXHERBO`."""
    return os_release_id() == "exherbo"


@cache
def is_fedora() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.FEDORA`."""
    return os_release_id() == "fedora"


@cache
def is_freebsd() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.FREEBSD`."""
    return sys.platform.startswith("freebsd") or os_release_id() == "freebsd"


@cache
def is_generic_linux() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.GENERIC_LINUX`.

    Matches when running on a Linux kernel but `distro` cannot identify
    the specific distribution (like minimal containers or build chroots without
    `/etc/os-release`).
    """
    return sys.platform == "linux" and not os_release_id()


@cache
def is_gentoo() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.GENTOO`."""
    return os_release_id() == "gentoo"


@cache
def is_guix() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.GUIX`."""
    return os_release_id() == "guix"


@cache
def is_haiku() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.HAIKU`."""
    return sys.platform.startswith("haiku")


@cache
def is_hurd() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.HURD`.

    ```{caution}
    {data}`sys.platform` can returns `GNU` or `gnu0`, see:
    <https://github.com/kdeldycke/extra-platforms/issues/308>
    ```
    """
    return sys.platform.lower().startswith("gnu")


@cache
def is_ibm_powerkvm() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.IBM_POWERKVM`.
    """
    return os_release_id() == "ibm_powerkvm"


@cache
def is_illumos() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ILLUMOS`.

    ```{hint}
    Illumos is a Unix OS derived from OpenSolaris. It shares
    `sys.platform == 'sunos5'` with Solaris, but can be distinguished by checking
    `platform.uname().version` which contains "illumos" on Illumos-based systems
    (like OpenIndiana, SmartOS, OmniOS).
    ```

    .. note::
        Gates on {data}`sys.platform` before reading {func}`platform.uname` so
        the function returns immediately on non-SunOS hosts. This matters on
        Windows, where {func}`platform.uname` shells out via
        {func}`platform._syscmd_ver` to populate its ``version`` field.
    """
    return sys.platform == "sunos5" and "illumos" in platform.uname().version.lower()


@cache
def is_kali() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.KALI`."""
    return os_release_id() == "kali"


@cache
def is_kvmibm() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.KVMIBM`."""
    return os_release_id() == "kvmibm"


@cache
def is_linuxmint() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.LINUXMINT`."""
    return os_release_id() == "linuxmint"


@cache
def is_macos() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.MACOS`.

    .. note::
        Uses {data}`sys.platform` rather than {func}`platform.platform`. The
        former is a constant baked in by CPython at compile time (always
        ``"darwin"`` on macOS), while the latter performs runtime introspection
        and, on Windows, shells out via {func}`platform._syscmd_ver` to invoke
        ``cmd /c ver``. Calling {func}`platform.platform` here would therefore
        spawn a subprocess on every non-macOS host, and break test suites that
        globally patch {func}`subprocess.run`. Since ``sys.platform == "darwin"``
        is unambiguous for macOS (unlike Solaris vs. SunOS, which both report
        ``"sunos5"`` and genuinely need {func}`platform.platform` to be told
        apart), there is no reason to pay that cost here.
    """
    return sys.platform == "darwin"


@cache
def is_mageia() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.MAGEIA`."""
    return os_release_id() == "mageia"


@cache
def is_mandriva() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.MANDRIVA`."""
    return os_release_id() == "mandriva"


@cache
def is_manjaro() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.MANJARO`."""
    return os_release_id() == "manjaro"


@cache
def is_midnightbsd() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.MIDNIGHTBSD`.
    """
    return sys.platform.startswith("midnightbsd") or os_release_id() == "midnightbsd"


@cache
def is_netbsd() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.NETBSD`."""
    return sys.platform.startswith("netbsd") or os_release_id() == "netbsd"


@cache
def is_nixos() -> bool:
    """Return `True` if current platform is {data}`~extra_platforms.NIXOS`."""
    return os_release_id() == "nixos"


@cache
def is_nobara() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.NOBARA`."""
    return os_release_id() == "nobara"


@cache
def is_openbsd() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.OPENBSD`."""
    return sys.platform.startswith("openbsd") or os_release_id() == "openbsd"


@cache
def is_opensuse() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.OPENSUSE`."""
    return os_release_id() == "opensuse"


@cache
def is_openwrt() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.OPENWRT`."""
    return os_release_id() == "openwrt"


@cache
def is_oracle() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ORACLE`."""
    return os_release_id() == "oracle"


@cache
def is_os400() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.OS400`.

    ```{note}
    Detects [IBM i](https://en.wikipedia.org/wiki/IBM_i) (formerly OS/400),
    whose AIX-compatible PASE runtime hosts Python. Python 3.9+ on IBM i reports
    `sys.platform` as `os400` rather than `aix` (as older versions did), so PASE
    is distinguished from {data}`~extra_platforms.AIX` despite their binary
    compatibility.
    ```

    ```{note}
    {data}`~extra_platforms.OS400` is grouped under
    {data}`~extra_platforms.UNIX_LAYERS` (beside {data}`~extra_platforms.CYGWIN`),
    not {data}`~extra_platforms.SYSTEM_V` where its AIX kin lives. IBM i's native
    OS is not Unix: its only Unix surface is the PASE runtime that `os400` runs
    in. The [Wikipedia Unix taxonomy](https://en.wikipedia.org/wiki/Template:Unix)
    that these platform families track classes PASE as a Unix *compatibility
    layer* (a peer of Cygwin on Windows), so the trait sits there rather than
    with its System V binary kin.
    ```
    """
    return sys.platform.startswith("os400")


@cache
def is_parallels() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.PARALLELS`."""
    return os_release_id() == "parallels"


@cache
def is_pidora() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.PIDORA`."""
    return os_release_id() == "pidora"


@cache
def is_raspbian() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.RASPBIAN`."""
    return os_release_id() == "raspbian"


@cache
def is_rhel() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.RHEL`."""
    return os_release_id() == "rhel"


@cache
def is_rocky() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.ROCKY`."""
    return os_release_id() == "rocky"


@cache
def is_scientific() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.SCIENTIFIC`.
    """
    return os_release_id() == "scientific"


@cache
def is_slackware() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.SLACKWARE`."""
    return os_release_id() == "slackware"


@cache
def is_sles() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.SLES`."""
    return os_release_id() == "sles"


@cache
def is_solaris() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.SOLARIS`.

    .. note::
        Gates on {data}`sys.platform == "sunos5"` before invoking
        {func}`platform.platform`, which on Windows would shell out via
        {func}`platform._syscmd_ver`. Solaris and SunOS share the same
        {data}`sys.platform` value, so {func}`platform.platform` is still needed
        to tell them apart, but only when we already know we're on a SunOS-based
        host.
    """
    return sys.platform == "sunos5" and platform.platform(
        aliased=True, terse=True
    ).startswith("Solaris")


@cache
def is_sunos() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.SUNOS`.

    .. note::
        See {func}`is_solaris` for the rationale behind the {data}`sys.platform`
        guard.
    """
    return sys.platform == "sunos5" and platform.platform(
        aliased=True, terse=True
    ).startswith("SunOS")


@cache
def is_tumbleweed() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.TUMBLEWEED`.
    """
    return os_release_id() == "opensuse-tumbleweed"


@cache
def is_tuxedo() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.TUXEDO`."""
    return os_release_id() == "tuxedo"


@cache
def is_ubuntu() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.UBUNTU`."""
    return os_release_id() == "ubuntu"


@cache
def is_ultramarine() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.ULTRAMARINE`.
    """
    return os_release_id() == "ultramarine"


@cache
def is_void() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.VOID`."""
    return os_release_id() == "void"


@cache
def is_windows() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.WINDOWS`."""
    return sys.platform.startswith("win32")


@cache
def is_wsl1() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.WSL1`.

    ```{caution}
    The only difference between WSL1 and WSL2 is
    [the case of the kernel release version](https://github.com/andweeb/presence.nvim/pull/64#issue-1174430662):

    - WSL 1:

      .. code-block:: shell-session

            $ uname -r
            4.4.0-22572-Microsoft

    - WSL 2:

      .. code-block:: shell-session

            $ uname -r
            5.10.102.1-microsoft-standard-WSL2
    ```

    .. note::
        Gates on {data}`sys.platform == "linux"` before invoking
        {func}`platform.release`. WSL is by definition a Linux subsystem, so on
        any other host the answer is trivially {data}`False`. The guard also
        avoids a {func}`platform._syscmd_ver` subprocess on Windows, where
        {func}`platform.release` is implemented as a ``cmd /c ver`` shell-out.
    """
    return sys.platform == "linux" and "Microsoft" in platform.release()


@cache
def is_wsl2() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.WSL2`.

    .. note::
        See {func}`is_wsl1` for the rationale behind the {data}`sys.platform`
        guard.
    """
    return sys.platform == "linux" and "microsoft" in platform.release()


@cache
def is_xenserver() -> bool:
    """Return {data}`True` if current platform is {data}`~extra_platforms.XENSERVER`."""
    return os_release_id() == "xenserver"


@cache
def is_unknown_platform() -> bool:
    """Return {data}`True` if current platform is
    {data}`~extra_platforms.UNKNOWN_PLATFORM`.
    """
    # Lazy import to avoid circular dependencies.
    from .platform_data import UNKNOWN_PLATFORM

    return current_platform() is UNKNOWN_PLATFORM


# =============================================================================
# Shell detection heuristics
# =============================================================================


@cache
def _resolved_shell_id() -> str | None:
    """Resolve the ``SHELL`` environment variable through symlinks.

    Returns the stem of the resolved path (like ``"bash"`` when
    ``/bin/sh`` symlinks to ``/bin/bash``), or ``None`` when ``SHELL`` is
    not set.
    """
    shell_path = environ.get("SHELL", "")
    if not shell_path:
        return None
    try:
        return Path(shell_path).resolve(strict=True).stem.lower()
    except OSError:
        return PurePosixPath(shell_path).stem.lower()


@cache
def _active_env_var_shell_ids() -> frozenset[str]:
    """Return shell IDs whose startup environment variable is currently set.

    Reads ``version_env_var`` from each {class}`~extra_platforms.Shell`
    instance. PowerShell is naturally excluded because
    {data}`~extra_platforms.POWERSHELL` has ``version_env_var=None``
    (``PSModulePath`` is a presence signal, not a version variable).

    This identifies shells that are *actively running*, not merely configured
    as the login shell.
    """
    # Lazy import to avoid circular dependencies.
    from .group_data import ALL_SHELLS

    return frozenset(
        shell.id
        for shell in ALL_SHELLS
        if (env_var := getattr(shell, "version_env_var", None)) and env_var in environ
    )


def _shell_name(command: str) -> str:
    """Normalize a process command into a comparable shell name.

    Strips a leading ``-`` (which marks a login shell, like ``-bash``), then
    returns the lowercased filename stem (``/usr/bin/bash`` becomes ``bash``).
    Returns an empty string when no name can be extracted.
    """
    return PurePosixPath(command.lstrip("-")).stem.lower()


def _parse_proc_ppid(record: str) -> int:
    """Extract the parent PID from a ``/proc/<pid>`` process record.

    Two on-disk layouts are recognized, told apart by the presence of the
    parenthesized ``comm`` field:

    - Linux ``stat`` (and NetBSD's Linux-compatible ``stat``):
      ``"pid (comm) state ppid ..."``. The ``comm`` field may itself contain
      spaces and parentheses, so the PPID is taken as the second field after
      the last ``)``.
    - BSD ``status`` (FreeBSD, DragonFly): ``"comm pid ppid pgid ..."``, where
      the PPID is the third whitespace-separated field.

    Raises {class}`ValueError` or {class}`IndexError` on an unparsable record.
    """
    marker = record.rfind(")")
    if marker != -1:
        return int(record[marker + 2 :].split()[1])
    return int(record.split()[2])


def _ppid_from_proc(pid: int) -> int | None:
    """Read the parent PID of ``pid`` from ``/proc``.

    Tries the Linux-style ``stat`` file first, then the BSD-style ``status``
    file, so the same walk works across Linux, NetBSD, and FreeBSD. Returns
    {data}`None` when neither file can be read or parsed.

    ```{note}
    System V ``/proc`` (illumos, Solaris, AIX) exposes ``status`` as a binary
    ``pstatus_t`` rather than text. Bytes are decoded with ``errors="replace"``
    so a binary record degrades to an unparsable string (and {data}`None`)
    instead of raising {exc}`UnicodeDecodeError`. {func}`_parent_process_tree`
    then falls back to ``ps`` on those systems.
    ```
    """
    for proc_file in ("stat", "status"):
        try:
            record = Path(f"/proc/{pid}/{proc_file}").read_bytes()
        except OSError:
            continue
        try:
            return _parse_proc_ppid(record.decode(errors="replace"))
        except (ValueError, IndexError):
            return None
    return None


@cache
def _interpreter_shell_specs() -> tuple[tuple[re.Pattern[str], str], ...]:
    """Compiled ``(interpreter pattern, launcher name)`` pairs from the registry.

    Built from {class}`~extra_platforms.Shell` instances that declare an
    {attr}`~extra_platforms.Shell.interpreter`. The interpreter base name
    becomes a version-tolerant pattern (so ``python`` matches ``python3`` and
    ``python3.11``); the launcher name to look for is the shell's ``id``.
    """
    # Lazy import to avoid circular dependencies.
    from .group_data import ALL_SHELLS

    return tuple(
        (re.compile(rf"^{re.escape(interp)}(\d+(\.\d+)?)?$"), shell.id)
        for shell in ALL_SHELLS
        if (interp := getattr(shell, "interpreter", None))
    )


def _interpreter_shell(argv: list[str]) -> tuple[str, str] | None:
    """Detect a shell hosted by an interpreter from a process command line.

    When ``argv[0]`` is a known interpreter (like ``python``) and a later
    argument is an existing file whose name is a hosted shell's launcher (like
    ``xonsh``), returns ``(shell_id, launcher_path)``, else {data}`None`.

    The three guards (interpreter-name pattern, exact launcher basename, and an
    {meth}`~pathlib.Path.is_file` check) keep unrelated invocations like
    ``python -m pytest test_xonsh.py`` from matching. Mirrors the approach in
    [shellingham](https://github.com/sarugaku/shellingham).
    """
    if not argv:
        return None
    proc_name = Path(argv[0]).name.lower()
    for pattern, launcher in _interpreter_shell_specs():
        if not pattern.fullmatch(proc_name):
            continue
        for arg in argv[1:]:
            if Path(arg).name.lower() == launcher and Path(arg).is_file():
                return launcher, arg
    return None


_EMULATOR_NAMES = re.compile(r"rosetta|qemu-[a-z0-9_]+(?:-static)?")
"""User-mode CPU emulators that wrap the real command in their arguments."""


def _unwrap_emulator(argv: list[str]) -> list[str]:
    """Strip a leading user-mode emulator (``qemu-<arch>``, ``rosetta``) from argv.

    Under foreign-architecture emulation (like ``docker run --platform``), a
    process shows up as ``qemu-aarch64 /bin/bash …``; the real command is the
    remaining arguments. Returns ``argv`` with the emulator prefix removed, or
    ``argv`` unchanged when ``argv[0]`` is not an emulator (or nothing follows
    it). Composes with {func}`_interpreter_shell`: an emulated ``python`` running
    xonsh unwraps to ``python …`` first, then resolves to xonsh.

    ```{seealso}
    Mirrors the emulator handling in
    [shellingham](https://github.com/sarugaku/shellingham).
    ```
    """
    if len(argv) >= 2 and _EMULATOR_NAMES.fullmatch(_shell_name(argv[0])):
        return argv[1:]
    return argv


def _pairs_from_argv(argv: list[str]) -> list[tuple[str, str]]:
    """Derive ``(name, path)`` pairs a single process contributes from its argv.

    Unwraps a user-mode emulator prefix (so the emulated shell is seen), then
    yields the ``argv[0]`` shell name (with its path only when absolute, since a
    login dash carries none) plus any interpreter-hosted shell found in the
    arguments (like xonsh run under python). Shared by the ``/proc`` and ``ps``
    walks, which differ only in how they obtain ``argv``.
    """
    pairs: list[tuple[str, str]] = []
    argv = _unwrap_emulator(argv)
    if argv:
        # argv[0] recovers login shells and survives an unreadable exe; keep it
        # as a path only when absolute (a login dash carries none).
        if name := _shell_name(argv[0]):
            pairs.append((name, argv[0] if argv[0].startswith("/") else ""))
        # A shell hosted by an interpreter (like xonsh run under python).
        if hosted := _interpreter_shell(argv):
            pairs.append(hosted)
    return pairs


def _tree_from_proc() -> tuple[tuple[str, str], ...]:
    """Walk the parent process tree through ``/proc`` (Linux and BSD procfs).

    Returns ordered ``(name, path)`` pairs, nearest ancestor first. For each
    process it reads the resolved executable (``/proc/<pid>/exe``, an absolute
    path that follows a ``/bin/sh`` -> Bash symlink) and the full ``argv``
    (``/proc/<pid>/cmdline``). Reading both means a login shell is still
    recognized when the ``exe`` symlink is unreadable (hardened ``/proc``
    mounted with ``hidepid``), and vice versa. ``argv`` also lets an
    interpreter-hosted shell (xonsh under python) or an emulated shell (under
    qemu/rosetta) be recognized. ``path`` is empty when only a non-absolute
    ``argv[0]`` is available.
    """
    pairs: list[tuple[str, str]] = []
    pid = os.getpid()
    visited: set[int] = set()
    while pid > 1 and pid not in visited:
        visited.add(pid)
        # Resolved executable: an absolute path that follows symlinks.
        try:
            target = os.readlink(f"/proc/{pid}/exe")
            if name := _shell_name(target):
                pairs.append((name, target))
        except OSError:
            pass
        # Full argv from the raw, null-separated command line.
        try:
            raw = Path(f"/proc/{pid}/cmdline").read_bytes()
            argv = [a for a in raw.decode(errors="replace").split("\0") if a]
        except OSError:
            argv = []
        pairs.extend(_pairs_from_argv(argv))
        ppid = _ppid_from_proc(pid)
        if ppid is None:
            break
        pid = ppid
    return tuple(pairs)


def _tree_from_ps() -> tuple[tuple[str, str], ...]:
    """Walk the parent process tree through ``ps``.

    Used on POSIX systems without a usable text ``/proc`` (macOS and BSDs
    without procfs, plus the System V ``/proc`` of illumos, Solaris, and
    AIX/IBM i). Parses a single ``ps`` snapshot into a ``{pid: (ppid, args)}``
    table, then walks from the current process up to the root, returning ordered
    ``(name, path)`` pairs (nearest first).

    The full argument list is requested (rather than just the executable) so
    that interpreter-hosted shells (like xonsh under python) can be recognized
    from their arguments. ``path`` is taken from ``argv[0]`` when absolute; a
    login shell (``-zsh``) or a bare name carries no path, so callers fall back
    to ``SHELL``.

    The invocation is the portable POSIX form (``-A``, ``-o field=``, and the
    ``args`` specifier) with no ``-ww``, so it works across macOS, the BSDs,
    Linux, and the System V ``ps`` of illumos, Solaris, and AIX/IBM i. Those
    System V variants lack the ``command`` specifier (only ``args``) and reject
    ``-ww`` (AIX accepts ``-w`` only in Berkeley mode, which has no ``-o``);
    ``args`` may be truncated there, but ``argv[0]`` stays intact. Parsing reads
    positional columns from empty (``field=``) headers, sidestepping the
    header-name differences (``COMMAND`` vs ``CMD``) that complicate name-based
    parsing. Mirrors [shellingham](https://github.com/sarugaku/shellingham).

    ```{important}
    ``-A`` (select every process) is essential, not merely convenient. Without
    it, `ps` defaults to processes sharing the caller's controlling terminal,
    and macOS returns *nothing* when run outside a tty (as in CI or any
    non-interactive context, unlike Linux which lists all processes regardless).
    ``-A`` makes the snapshot tty-independent.
    ```
    """
    try:
        result = subprocess.run(
            ("ps", "-A", "-o", "pid=,ppid=,args="),
            capture_output=True,
            text=True,
            check=True,
            timeout=2,
        )
    except (OSError, subprocess.SubprocessError):
        return ()

    # str() coerces an unexpected stdout (like a globally mocked subprocess.run
    # returning a Mock) to text, so parsing degrades to an empty result instead
    # of raising.
    output = str(result.stdout)

    # Parse "<pid> <ppid> <args...>" rows into {pid: (ppid, args)}. The
    # argument list is the last field and is kept whole.
    table: dict[int, tuple[int, str]] = {}
    for line in output.splitlines():
        fields = line.split(maxsplit=2)
        if len(fields) < 3:
            continue
        try:
            child, parent = int(fields[0]), int(fields[1])
        except ValueError:
            continue
        table[child] = (parent, fields[2])

    pairs: list[tuple[str, str]] = []
    pid = os.getpid()
    visited: set[int] = set()
    while pid > 1 and pid in table and pid not in visited:
        visited.add(pid)
        ppid, command = table[pid]
        pairs.extend(_pairs_from_argv(command.split()))
        pid = ppid
    return tuple(pairs)


def _walk_process_map(
    process_map: dict[int, tuple[int, str]],
    start_pid: int,
    path_getter: Callable[[int], str],
) -> tuple[tuple[str, str], ...]:
    """Walk a ``{pid: (ppid, executable)}`` map into ordered ``(name, path)`` pairs.

    Climbs from ``start_pid`` to the root, nearest ancestor first. Each
    ``executable`` is normalized into a shell name via {func}`_shell_name`, and
    ``path_getter`` resolves a matched pid's full executable path. This is the
    platform-agnostic core of the Windows walk: the snapshot acquisition is the
    only Win32-specific part.
    """
    pairs: list[tuple[str, str]] = []
    pid = start_pid
    visited: set[int] = set()
    while pid in process_map and pid not in visited:
        visited.add(pid)
        ppid, executable = process_map[pid]
        if name := _shell_name(executable):
            pairs.append((name, path_getter(pid)))
        pid = ppid
    return tuple(pairs)


def _tree_from_windows() -> tuple[tuple[str, str], ...]:
    """Walk the parent process tree through the Win32 Tool Help API.

    Used on Windows, which has neither ``/proc`` nor ``ps``. Snapshots every
    process into a ``{pid: (ppid, executable)}`` map (see
    {mod}`extra_platforms._windows`), then walks from the current process up to
    the root, resolving each ancestor's full path via
    ``QueryFullProcessImageNameW``.

    Returns an empty tuple off Windows or on any Win32 failure, so detection
    degrades to the environment-variable heuristics.

    ```{caution}
    Windows parent-PID links can be stale: a parent may exit and its PID be
    reused. The ``visited`` set bounds the walk, but a reused PID could in
    theory divert it. This matches the limitation in
    [shellingham](https://github.com/sarugaku/shellingham).
    ```
    """
    tree: tuple[tuple[str, str], ...] = ()
    if sys.platform == "win32":
        try:
            from . import _windows

            tree = _walk_process_map(
                _windows.process_map(), os.getpid(), _windows.process_path
            )
        except (OSError, ImportError):
            tree = ()
    return tree


@cache
def _parent_process_tree() -> tuple[tuple[str, str], ...]:
    """Ordered ``(name, path)`` pairs for the process tree, nearest first.

    ``name`` is the normalized shell name (see {func}`_shell_name`); ``path`` is
    the best-effort executable path, absolute when the source provides it and
    empty otherwise. Dispatches on what the platform exposes:

    - ``/proc`` when present (Linux always, BSDs that mount procfs): no
      subprocess is spawned.
    - ``ps`` otherwise (macOS, BSDs without procfs).
    - The Win32 Tool Help API on Windows.
    - An empty tuple on platforms exposing none of these.
    """
    # Linux (and procfs BSDs) expose a text /proc; this needs no subprocess.
    if Path("/proc").is_dir():
        tree = _tree_from_proc()
        if tree:
            return tree
        # /proc exists but yielded nothing: System V procfs (illumos, Solaris,
        # AIX) uses a binary layout the Linux reader cannot parse. Use `ps`.
    # macOS, procfs-less BSDs, and System V procfs systems: walk via `ps`.
    if os.name == "posix":
        return _tree_from_ps()
    # Windows: snapshot via the Win32 Tool Help API. _tree_from_windows()
    # returns an empty tuple on any other platform.
    return _tree_from_windows()


@cache
def _parent_process_exe_names() -> frozenset[str]:
    """Collect executable names from the parent process tree.

    Returns a {class}`frozenset` of lowercased executable stems (like
    ``"bash"`` or ``"python3"``), derived from {func}`_parent_process_tree`.
    """
    return frozenset(name for name, _ in _parent_process_tree())


def _running_shell_path(shell_id: str) -> str | None:
    """Return the executable path of the nearest ancestor matching ``shell_id``.

    Walks {func}`_parent_process_tree` and returns the first (nearest) absolute
    path whose normalized name equals ``shell_id``. Non-absolute sources (a
    login dash, a bare name, or a truncated BSD ``ps`` ``comm``) are skipped so
    callers can fall back to ``SHELL``. A path is considered absolute when it
    starts with ``/`` (POSIX) or satisfies ``os.path.isabs`` (Windows drive
    paths like ``C:\\...``). Returns {data}`None` when no running path is
    found.
    """
    for name, path in _parent_process_tree():
        if name == shell_id and (path.startswith("/") or os.path.isabs(path)):
            return path
    return None


def _parent_process_shells(shell_ids: str | tuple[str, ...]) -> bool:
    """Check if any parent process in the tree matches the given shell IDs.

    :param shell_ids: Shell executable name(s) to match. Can be a single string
        (like ``"bash"``) or a tuple of strings (like ``("powershell", "pwsh")``).
    :returns: ``True`` if a matching shell is found in the parent process tree,
        ``False`` otherwise or on non-Linux platforms where ``/proc`` is
        unavailable.
    """
    id_set = (
        frozenset({shell_ids}) if isinstance(shell_ids, str) else frozenset(shell_ids)
    )
    return bool(id_set & _parent_process_exe_names())


def _detect_shell(
    version_env_var: str | None = None,
    shell_ids: str | Iterable[str] | None = None,
) -> bool:
    """Detect a specific shell from the environment.

    ```{caution}
    This function is designed primarily for POSIX/Unix systems. The `SHELL`
    environment variable and `/proc` filesystem are Unix-specific conventions.
    For Windows shells like {data}`~extra_platforms.CMD`, use platform-specific
    detection instead.
    ```

    Uses a tiered detection strategy:

    1. Checks for shell-specific version environment variable (most reliable).
    2. Resolves symlinks in the ``SHELL`` environment variable path, then
       matches the resolved executable name against known shell IDs. This
       reports the actual shell implementation rather than the interface name:
       when ``/bin/sh`` symlinks to ``/bin/bash``, ``bash`` is detected, not
       ``sh``.
    3. Falls back to walking the parent process tree (via `/proc` on Linux,
       `ps` on macOS and the BSDs) to find the active shell, for stripped
       environments without shell env vars.

    :param version_env_var: Shell-specific environment variable name
        (like ``"BASH_VERSION"``).
    :param shell_ids: Shell executable name(s) to match. Can be a single string
        (like ``"bash"``) or a tuple of strings (like ``("powershell", "pwsh")``).
    :returns: ``True`` if the shell is detected, ``False`` otherwise.
    """
    # Check shell-specific version environment variable.
    if version_env_var and version_env_var in environ:
        return True

    # Normalize shell_ids for consistent handling.
    if shell_ids is None:
        return False

    ids = (
        frozenset((shell_ids,)) if isinstance(shell_ids, str) else frozenset(shell_ids)
    )

    # Check resolved SHELL environment variable against known shell IDs.
    if _resolved_shell_id() in ids:
        return True

    # Fallback: walk the parent process tree to find the active shell. This
    # covers stripped containers (like ubuntu-slim) where SHELL is not set.
    normalized_ids = (shell_ids,) if isinstance(shell_ids, str) else tuple(shell_ids)
    return _parent_process_shells(normalized_ids)


@cache
def is_ash() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.ASH`.

    ```{hint}
    Detected via the `SHELL` environment variable path, as Almquist
    Shell does not set its own version variable.
    ```

    ```{note}
    [BusyBox](https://busybox.net)'s built-in shell is an {data}`~extra_platforms.ASH`
    derivative. On BusyBox-based systems ({data}`~extra_platforms.ALPINE`,
    {data}`~extra_platforms.OPENWRT`), `$SHELL` typically resolves to `/bin/ash`,
    so BusyBox environments are detected as {data}`~extra_platforms.ASH`.
    ```
    """
    return _detect_shell(shell_ids="ash")


@cache
def is_bash() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.BASH`.

    ```{hint}
    Detected via the `BASH_VERSION` environment variable (set by Bash
    on startup), or via the `SHELL` path as a fallback.
    ```

    ```{attention}
    GitHub's `ubuntu-slim` runner is a
    [stripped-down environments, running as a WSL2 container](https://docs.github.com/en/actions/reference/runners/github-hosted-runners#single-cpu-runners)
    on top of Windows. It
    [uses Bash as the default shell](https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md),
    but does not set neither `BASH_VERSION` nor `SHELL`.
    In that case we fall back to walking the parent process tree via `/proc`
    to find it.
    ```
    """
    return _detect_shell(version_env_var="BASH_VERSION", shell_ids="bash")


@cache
def is_cmd() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.CMD`.

    ```{hint}
    Detected on Windows via `cmd.exe` in the parent process tree, or when the
    `PROMPT` environment variable is set and `PSModulePath` is not (to exclude
    PowerShell).
    ```
    """
    # cmd.exe in the parent process tree is the strong signal; the PROMPT/
    # PSModulePath heuristic covers the case where the tree is unavailable.
    return sys.platform == "win32" and (
        _detect_shell(shell_ids="cmd")
        or ("PROMPT" in environ and "PSModulePath" not in environ)
    )


@cache
def is_csh() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.CSH`.

    ```{hint}
    Detected via the `SHELL` environment variable path.
    ```
    """
    return _detect_shell(shell_ids="csh")


@cache
def is_dash() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.DASH`.

    ```{hint}
    Detected via the `SHELL` environment variable path, as Dash does
    not set its own version variable.
    ```
    """
    return _detect_shell(shell_ids="dash")


@cache
def is_fish() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.FISH`.

    ```{hint}
    Detected via the `FISH_VERSION` environment variable (set by Fish
    on startup), or via the `SHELL` path as a fallback.
    ```
    """
    return _detect_shell(version_env_var="FISH_VERSION", shell_ids="fish")


@cache
def is_ksh() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.KSH`.

    ```{hint}
    Detected via the `KSH_VERSION` environment variable (set by Korn
    shell on startup), or via the `SHELL` path as a fallback.
    ```
    """
    return _detect_shell(version_env_var="KSH_VERSION", shell_ids="ksh")


@cache
def is_nushell() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.NUSHELL`.

    ```{hint}
    Detected via the `NU_VERSION` environment variable (set by Nushell
    on startup), or via the `SHELL` path as a fallback.
    ```
    """
    return _detect_shell(version_env_var="NU_VERSION", shell_ids="nu")


@cache
def is_powershell() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.POWERSHELL`.

    ```{note}
    PowerShell is cross-platform and
    [available on Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell-on-linux)
    and macOS. Detection covers all platforms via `PSModulePath`,
    `SHELL` path, and parent process tree.
    ```

    ```{attention}
    `PSModulePath` can leak into non-PowerShell child processes via two
    vectors:

    1. **Process-level inheritance** (all platforms): PowerShell
       modifies `PSModulePath` at startup, and
       [all non-PowerShell children inherit it](https://github.com/PowerShell/PowerShell/issues/9957).
    2. **System-wide registry variable** (Windows only):
       `PSModulePath` is a [persistent machine-level
       environment variable](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_psmodulepath)
       visible to all processes.

    This is the case for all GitHub Ubuntu runners, where
    `PSModulePath` leaks from Azure infrastructure. This leads to multiple
    shell detections, which is arbitraged by `current_shell()`,
    which deprioritizes PowerShell when other shells are detected.
    ```
    """
    # PSModulePath is a presence signal, not a version variable. Check it
    # inline instead of routing through _detect_shell(version_env_var=...).
    if "PSModulePath" in environ:
        return True
    return _detect_shell(shell_ids=("powershell", "powershell_ise", "pwsh"))


@cache
def is_sh() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.SH`.

    ```{hint}
    Detected via the ``SHELL`` environment variable path, after symlink
    resolution. Only matches when the resolved shell binary is literally
    ``sh``, not when ``/bin/sh`` is a symlink to another shell (like bash or
    dash).
    ```

    ```{note}
    On most modern systems, ``/bin/sh`` is a symlink to a concrete shell
    (``bash``, ``dash``, etc.). In that case, ``is_sh()`` returns ``False``
    and the concrete shell's detection function returns ``True`` instead.
    To test whether the environment provides a Bourne-compatible *interface*
    regardless of the underlying implementation, use
    {func}`~extra_platforms.is_bourne_shells` instead.
    ```

    ```{important}
    {data}`~extra_platforms.SH` is treated as a low-specificity fallback by
    {func}`~extra_platforms.current_shell` (like
    {data}`~extra_platforms.GENERIC_LINUX` for platforms): when both
    {data}`~extra_platforms.SH` and a more specific shell are detected,
    {func}`~extra_platforms.current_shell` returns the specific shell.
    ```
    """
    return _detect_shell(shell_ids="sh")


@cache
def is_tcsh() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.TCSH`.

    ```{hint}
    Detected via the `SHELL` environment variable path.
    ```
    """
    return _detect_shell(shell_ids="tcsh")


@cache
def is_xonsh() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.XONSH`.

    ```{hint}
    Detected via the `XONSH_VERSION` environment variable (set by Xonsh
    on startup), or via the `SHELL` path as a fallback.
    ```

    ```{note}
    Xonsh runs as a Python script rather than a standalone binary, so the
    parent process tree shows `python` rather than `xonsh`. When neither
    `XONSH_VERSION` nor `SHELL` identifies it, the tree walk inspects
    interpreter arguments for the `xonsh` launcher (see
    {attr}`~extra_platforms.Shell.interpreter`).
    ```
    """
    return _detect_shell(version_env_var="XONSH_VERSION", shell_ids="xonsh")


@cache
def is_zsh() -> bool:
    """Return {data}`True` if current shell is {data}`~extra_platforms.ZSH`.

    ```{hint}
    Detected via the `ZSH_VERSION` environment variable (set by Zsh on
    startup), or via the `SHELL` path as a fallback.
    ```
    """
    return _detect_shell(version_env_var="ZSH_VERSION", shell_ids="zsh")


@cache
def is_unknown_shell() -> bool:
    """Return {data}`True` if current shell is
    {data}`~extra_platforms.UNKNOWN_SHELL`.
    """
    # Lazy import to avoid circular dependencies.
    from .shell_data import UNKNOWN_SHELL

    return current_shell() is UNKNOWN_SHELL


# =============================================================================
# Terminal detection heuristics
# =============================================================================


@cache
def is_alacritty() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.ALACRITTY`."""
    return "ALACRITTY_SOCKET" in environ or "ALACRITTY_WINDOW_ID" in environ


@cache
def is_apple_terminal() -> bool:
    """Return {data}`True` if current terminal is
    {data}`~extra_platforms.APPLE_TERMINAL`.
    """
    return environ.get("TERM_PROGRAM") == "Apple_Terminal"


@cache
def is_contour() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.CONTOUR`."""
    return environ.get("TERMINAL_NAME") == "contour"


@cache
def is_foot() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.FOOT`."""
    return environ.get("TERM", "").startswith("foot")


@cache
def is_ghostty() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.GHOSTTY`."""
    return "GHOSTTY_RESOURCES_DIR" in environ


@cache
def is_gnome_terminal() -> bool:
    """Return {data}`True` if current terminal is
    {data}`~extra_platforms.GNOME_TERMINAL`.
    """
    return "GNOME_TERMINAL_SCREEN" in environ


@cache
def is_gnu_screen() -> bool:
    """Return {data}`True` if current terminal is
    {data}`~extra_platforms.GNU_SCREEN`.
    """
    return "STY" in environ


@cache
def is_hyper() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.HYPER`."""
    return environ.get("TERM_PROGRAM") == "Hyper"


@cache
def is_iterm2() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.ITERM2`."""
    return "ITERM_SESSION_ID" in environ or environ.get("TERM_PROGRAM") == "iTerm.app"


@cache
def is_kitty() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.KITTY`."""
    return "KITTY_WINDOW_ID" in environ


@cache
def is_konsole() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.KONSOLE`."""
    return "KONSOLE_VERSION" in environ


@cache
def is_rio() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.RIO`."""
    return "RIO_WINDOW_ID" in environ


@cache
def is_tabby() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.TABBY`."""
    return "TABBY" in environ or environ.get("TERM_PROGRAM") == "Tabby"


@cache
def is_tilix() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.TILIX`."""
    return "TILIX_ID" in environ


@cache
def is_tmux() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.TMUX`."""
    return "TMUX" in environ


@cache
def is_unknown_terminal() -> bool:
    """Return {data}`True` if current terminal is
    {data}`~extra_platforms.UNKNOWN_TERMINAL`.
    """
    # Lazy import to avoid circular dependencies.
    from .terminal_data import UNKNOWN_TERMINAL

    return current_terminal() is UNKNOWN_TERMINAL


@cache
def is_vscode_terminal() -> bool:
    """Return {data}`True` if current terminal is
    {data}`~extra_platforms.VSCODE_TERMINAL`.
    """
    return environ.get("TERM_PROGRAM") == "vscode"


@cache
def is_wezterm() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.WEZTERM`."""
    return "WEZTERM_EXECUTABLE" in environ


@cache
def is_windows_terminal() -> bool:
    """Return {data}`True` if current terminal is
    {data}`~extra_platforms.WINDOWS_TERMINAL`.
    """
    return "WT_SESSION" in environ


@cache
def is_xterm() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.XTERM`.

    ```{note}
    We check for `XTERM_VERSION` rather than `TERM=xterm` because many
    headless environments (e.g., GitHub Actions `ubuntu-slim` runners) set
    `TERM=xterm` for termcap/terminfo compatibility without actually running
    xterm.
    ```
    """
    return "XTERM_VERSION" in environ


@cache
def is_zellij() -> bool:
    """Return {data}`True` if current terminal is {data}`~extra_platforms.ZELLIJ`."""
    return "ZELLIJ" in environ


# =============================================================================
# CI/CD detection heuristics
# =============================================================================


@cache
def is_azure_pipelines() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.AZURE_PIPELINES`.

    ```{seealso}
    Environment variables reference:
    <https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&viewFallbackFrom=vsts&tabs=yaml#system-variables>.
    ```
    """
    return "TF_BUILD" in environ


@cache
def is_bamboo() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.BAMBOO`.

    ```{seealso}
    Environment variables reference:
    <https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables>.
    ```
    """
    return "bamboo.buildKey" in environ


@cache
def is_buildkite() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.BUILDKITE`.

    ```{seealso}
    Environment variables reference:
    <https://buildkite.com/docs/pipelines/environment-variables>.
    ```
    """
    return "BUILDKITE" in environ


@cache
def is_circle_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.CIRCLE_CI`.

    ```{seealso}
    Environment variables reference:
    <https://circleci.com/docs/reference/variables/#built-in-environment-variables>.
    ```
    """
    return "CIRCLECI" in environ


@cache
def is_cirrus_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.CIRRUS_CI`.

    ```{seealso}
    Environment variables reference:
    <https://cirrus-ci.org/guide/writing-tasks/#environment-variables>.
    ```
    """
    return "CIRRUS_CI" in environ


@cache
def is_codebuild() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.CODEBUILD`.

    ```{seealso}
    Environment variables reference:
    <https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html>.
    ```
    """
    return "CODEBUILD_BUILD_ID" in environ


@cache
def is_github_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.GITHUB_CI`.

    ```{seealso}
    Environment variables reference:
    <https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables>.
    ```
    """
    return "GITHUB_ACTIONS" in environ or "GITHUB_RUN_ID" in environ


@cache
def is_gitlab_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.GITLAB_CI`.

    ```{seealso}
    Environment variables reference:
    <https://docs.gitlab.com/ci/variables/predefined_variables/#predefined-variables>.
    ```
    """
    return "GITLAB_CI" in environ


@cache
def is_guix_build() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.GUIX_BUILD`.

    ```{note}
    The Guix build daemon runs packages in an isolated sandbox with
    ``HOME`` set to ``/homeless-shelter`` (a non-existent directory). This
    prevents builds from reading or writing to a real home directory.
    ```

    ```{seealso}
    Build environment reference:
    <https://guix.gnu.org/manual/en/html_node/Build-Environment-Setup.html>.
    ```
    """
    return environ.get("HOME") == "/homeless-shelter"


@cache
def is_heroku_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.HEROKU_CI`.

    ```{seealso}
    Environment variables reference:
    <https://devcenter.heroku.com/articles/heroku-ci#immutable-environment-variables>.
    ```
    """
    return "HEROKU_TEST_RUN_ID" in environ


@cache
def is_teamcity() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.TEAMCITY`.

    ```{seealso}
    Environment variables reference:
    <https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters>.
    ```
    """
    return "TEAMCITY_VERSION" in environ


@cache
def is_travis_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.TRAVIS_CI`.

    ```{seealso}
    Environment variables reference:
    <https://docs.travis-ci.com/user/environment-variables/#default-environment-variables>.
    ```
    """
    return "TRAVIS" in environ


@cache
def is_unknown_ci() -> bool:
    """Return {data}`True` if current CI is {data}`~extra_platforms.UNKNOWN_CI`."""
    # Lazy import to avoid circular dependencies.
    from .ci_data import UNKNOWN_CI

    return current_ci() is UNKNOWN_CI


# =============================================================================
# Agent detection heuristics
# =============================================================================


@cache
def is_claude_code() -> bool:
    """Return {data}`True` if current agent is {data}`~extra_platforms.CLAUDE_CODE`.

    ```{seealso}
    Claude Code sets the `CLAUDECODE` environment variable when running.
    ```
    """
    return "CLAUDECODE" in environ


@cache
def is_cline() -> bool:
    """Return {data}`True` if current agent is {data}`~extra_platforms.CLINE`.

    ```{seealso}
    Cline sets the `CLINE_ACTIVE` environment variable when running.
    ```
    """
    return "CLINE_ACTIVE" in environ


@cache
def is_cursor() -> bool:
    """Return {data}`True` if current agent is {data}`~extra_platforms.CURSOR`.

    ```{seealso}
    Cursor sets the `CURSOR_AGENT` environment variable when running.
    ```
    """
    return "CURSOR_AGENT" in environ


@cache
def is_unknown_agent() -> bool:
    """Return {data}`True` if current agent is
    {data}`~extra_platforms.UNKNOWN_AGENT`.
    """
    # Lazy import to avoid circular dependencies.
    from .agent_data import UNKNOWN_AGENT

    return current_agent() is UNKNOWN_AGENT


# Populate the detection registry with all is_*() functions defined above.
_detection_registry.update({
    _name: _func
    for _name in dir()
    if _name.startswith("is_") and callable(_func := globals()[_name])
})


# =============================================================================
# Current environment detection
# =============================================================================


@cache
def current_architecture(strict: bool = False) -> Architecture:
    """Returns the {class}`~extra_platforms.Architecture` matching the
    current environment.

    Returns {data}`~extra_platforms.UNKNOWN_ARCHITECTURE` if not running inside a
    recognized architecture. To raise an error instead, set `strict` to `True`.

    ```{important}
    Always raises an error if multiple architectures match.
    ```

    ```{warning}
    An architecture is always expected to be detected. An unrecognized result
    logs a `WARNING` and likely indicates a missing detection heuristic that
    should be [reported](https://github.com/kdeldycke/extra-platforms/issues).
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .architecture_data import UNKNOWN_ARCHITECTURE
    from .group_data import ALL_ARCHITECTURES

    # Collect all matching architectures.
    matching: set[Architecture] = {
        arch  # type: ignore[misc]
        for arch in ALL_ARCHITECTURES
        if arch.current
    }

    # Return the only matching architecture.
    if len(matching) == 1:
        return matching.pop()

    if len(matching) > 1:
        raise RuntimeError(
            f"Multiple architectures matches: {matching!r}. {_unrecognized_message()}"
        )

    _report_unrecognized("architecture", strict=strict)
    return UNKNOWN_ARCHITECTURE


@cache
def current_platform(strict: bool = False) -> Platform:
    """Always returns the best matching {class}`~extra_platforms.Platform`
    for the current environment.

    Returns {data}`~extra_platforms.UNKNOWN_PLATFORM` if not running inside a recognized
    platform. To raise an error instead, set `strict` to `True`.

    ```{important}
    If multiple platforms match the current environment, this function will try to
    select the best, informative one. Raises an error if we can't decide on a single,
    appropriate platform.
    ```

    ```{warning}
    A platform is always expected to be detected. An unrecognized result logs a
    `WARNING` and likely indicates a missing detection heuristic that should be
    [reported](https://github.com/kdeldycke/extra-platforms/issues).
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .group_data import ALL_PLATFORMS
    from .platform_data import GENERIC_LINUX, UNKNOWN_PLATFORM, WSL1, WSL2

    # Collect all matching platforms.
    matching: set[Platform] = {
        plat  # type: ignore[misc]
        for plat in ALL_PLATFORMS
        if plat.current
    }

    # Return the only matching platform.
    if len(matching) == 1:
        (result,) = matching
        if result is GENERIC_LINUX:
            _report_unrecognized("Linux distribution", strict=False)
        return result

    # Removes some generic platforms from the matching, until we have a single match.
    # Starts by removing the least specific WSL1, then WSL2: WSL is a generic platform,
    # so we should prefer the remaining, more specific platform matches
    # like Ubuntu. See:
    # - https://github.com/kdeldycke/extra-platforms/issues/158
    # - https://github.com/kdeldycke/meta-package-manager/issues/944
    for wsl in (WSL1, WSL2):
        if wsl in matching:
            matching.remove(wsl)
            if len(matching) == 1:
                return matching.pop()

    # Remove GENERIC_LINUX if a more specific platform was also detected.
    if GENERIC_LINUX in matching and len(matching) > 1:
        matching.remove(GENERIC_LINUX)
        if len(matching) == 1:
            return matching.pop()

    if len(matching) > 1:
        raise RuntimeError(
            f"Multiple platforms matches: {matching!r}. {_unrecognized_message()}"
        )

    _report_unrecognized("platform", strict=strict)
    return UNKNOWN_PLATFORM


@cache
def current_shell(strict: bool = False) -> Shell:
    """Returns the {class}`~extra_platforms.Shell` matching the current environment.

    Uses a tiered disambiguation strategy with cached signals shared with
    {func}`_detect_shell`:

    1. Shell-specific environment variables (strongest: the Python process
       *is* the shell).
    2. Parent process tree, read from ``/proc`` on Linux or ``ps`` on macOS
       and the BSDs (strong: the shell is an ancestor process actively
       running).
    3. ``SHELL`` environment variable resolved through symlinks (weak:
       configured login shell, may differ from the active shell).

    {data}`~extra_platforms.SH` is treated as a low-specificity fallback (like
    {data}`~extra_platforms.GENERIC_LINUX` for platforms): on modern systems
    ``/bin/sh`` is almost always a symlink to a concrete shell, so
    {data}`~extra_platforms.SH` is stripped when a more specific shell is also
    detected.

    Returns {data}`~extra_platforms.UNKNOWN_SHELL` if not running inside a
    recognized shell. To raise an error instead, set `strict` to `True`.

    ```{important}
    If both {data}`~extra_platforms.POWERSHELL` and another shell are
    detected (because `PSModulePath`
    [leaks into child processes](https://github.com/PowerShell/PowerShell/issues/9957)),
    the other shell is preferred.
    ```

    ```{note}
    This returns the **single primary shell**, after disambiguation.
    {func}`~extra_platforms.current_traits` may include additional shells
    that are detectable but not primary (like
    {data}`~extra_platforms.POWERSHELL` on GitHub Ubuntu runners).
    ```

    ```{warning}
    A shell is always expected to be detected. An unrecognized result logs a
    `WARNING` and likely indicates a missing detection heuristic that should be
    [reported](https://github.com/kdeldycke/extra-platforms/issues).
    ```

    ```{seealso}
    Inspired by [UV's cross-platform shell detection](https://github.com/astral-sh/uv/blob/0.10.2/crates/uv-shell/src/lib.rs).
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .group_data import ALL_SHELLS
    from .shell_data import POWERSHELL, SH, UNKNOWN_SHELL

    # Collect all matching shells via the full scan (env vars, SHELL=,
    # parent process tree, Windows defaults).
    matching: set[Shell] = {
        shell  # type: ignore[misc]
        for shell in ALL_SHELLS
        if shell.current
    }

    if len(matching) == 1:
        return matching.pop()

    # Tier 1: prefer shells whose startup env var is set (strongest signal
    # for the *active* shell). These env vars (like BASH_VERSION) are
    # shell-internal and typically not exported to child processes, so this
    # tier only fires when the Python process IS the shell itself.
    active = _active_env_var_shell_ids()
    if active:
        active_matches = {s for s in matching if s.id in active}
        if len(active_matches) == 1:
            return active_matches.pop()
        if active_matches:
            matching = active_matches

    # Tier 2: prefer shells found in the parent process tree (strong signal:
    # the shell is actively running as an ancestor process). This resolves
    # the common CI conflict where SHELL=/bin/sh resolves to /bin/dash
    # (configured login shell) while bash is the actual parent process
    # running the step.
    proc_names = _parent_process_exe_names()
    if proc_names:
        proc_matches = {s for s in matching if s.id in proc_names}
        if len(proc_matches) == 1:
            return proc_matches.pop()
        if proc_matches:
            matching = proc_matches

    # Tier 3: prefer the shell resolved from SHELL= over remaining matches.
    resolved = _resolved_shell_id()
    if resolved:
        resolved_matches = {s for s in matching if s.id == resolved}
        if len(resolved_matches) == 1:
            return resolved_matches.pop()

    # Remove SH if a more specific shell was also detected. On modern
    # systems /bin/sh is almost always a symlink to a concrete shell, so
    # SH is a low-specificity fallback (like GENERIC_LINUX for platforms).
    if SH in matching and len(matching) > 1:
        matching.discard(SH)
        if len(matching) == 1:
            return matching.pop()

    # If PowerShell is detected alongside another shell, prefer the other.
    if POWERSHELL in matching and len(matching) > 1:
        matching.discard(POWERSHELL)
        if len(matching) == 1:
            return matching.pop()

    if len(matching) > 1:
        raise RuntimeError(
            f"Multiple shells matches: {matching!r}. {_unrecognized_message()}"
        )

    _report_unrecognized("shell", strict=strict)
    return UNKNOWN_SHELL


@cache
def current_shell_path() -> str | None:
    """Returns the executable path of the current shell, or {data}`None`.

    Resolution order:

    1. The actual running shell binary, taken from the nearest ancestor in the
       parent process tree that matches {func}`current_shell` (read from
       ``/proc`` on Linux, ``ps`` on macOS and the BSDs). This is the true
       interpreter, even when ``SHELL`` is unset or points elsewhere.
    2. The ``SHELL`` environment variable (the configured login shell), as a
       fallback.

    Returns {data}`None` when neither is available: no recognized shell, or a
    stripped environment without ``SHELL``.

    ```{note}
    On some BSDs, ``ps`` reports only a truncated process name rather than a
    full path. The non-absolute name is discarded, so this falls back to
    ``SHELL`` there.
    ```

    ```{seealso}
    Comparable to [shellingham](https://github.com/sarugaku/shellingham)'s
    `detect_shell()`, which returns the shell name paired with its path. Here
    the name is {func}`current_shell` and the path is this function.
    ```
    """
    path = _running_shell_path(current_shell().id)
    if path:
        return path
    return environ.get("SHELL") or None


@cache
def current_terminal(strict: bool = False) -> Terminal:
    """Returns the {class}`~extra_platforms.Terminal` matching the current environment.

    Returns {data}`~extra_platforms.UNKNOWN_TERMINAL` if not running inside a
    recognized terminal. To raise an error instead, set `strict` to `True`.

    ```{important}
    If multiple terminals match (e.g., {data}`~extra_platforms.TMUX` inside
    {data}`~extra_platforms.KITTY`), multiplexers are filtered out first to
    identify the innermost terminal. If multiple non-multiplexer terminals still
    match, a {class}`RuntimeError` is raised.
    ```

    ```{note}
    Unlike architectures, platforms, and shells, a terminal is not always present.
    Headless environments (CI runners, cron jobs, Docker containers, SSH
    non-interactive commands) have no terminal emulator attached.

    If the `TERM` environment variable is set, an unrecognized terminal logs at
    `WARNING` level, as it suggests a terminal emulator is present but not
    recognized. Otherwise, it logs at `INFO` level.
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .group_data import ALL_TERMINALS, MULTIPLEXERS
    from .terminal_data import UNKNOWN_TERMINAL

    # Collect all matching terminals.
    matching: set[Terminal] = {
        term  # type: ignore[misc]
        for term in ALL_TERMINALS
        if term.current
    }

    # Return the only matching terminal.
    if len(matching) == 1:
        return matching.pop()

    # If multiple terminals match, filter out multiplexers to find the innermost.
    if len(matching) > 1:
        non_mux = {t for t in matching if t not in MULTIPLEXERS}
        if len(non_mux) == 1:
            return non_mux.pop()
        raise RuntimeError(
            f"Multiple terminals matches: {matching!r}. {_unrecognized_message()}"
        )

    # TERM env var signals a terminal emulator is present.
    if "TERM" in environ:
        _report_unrecognized("terminal", strict=strict)
    else:
        _report_unrecognized("terminal", strict=strict, expected=False)
    return UNKNOWN_TERMINAL


@cache
def current_ci(strict: bool = False) -> CI:
    """Returns the {class}`~extra_platforms.CI` system matching the current environment.

    Returns {data}`~extra_platforms.UNKNOWN_CI` if not running inside a recognized CI
    system. To raise an error instead, set `strict` to `True`.

    ```{important}
    Always raises an error if multiple CI systems match.
    ```

    ```{note}
    Unlike architectures, platforms, and shells, a CI system is not always present.
    Local development environments have no CI system running.

    If the `CI` environment variable is set, an unrecognized CI system logs at
    `WARNING` level, as it suggests a CI system is present but not recognized.
    Otherwise, it logs at `INFO` level.
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .ci_data import UNKNOWN_CI
    from .group_data import ALL_CI

    # Collect all matching CI systems.
    matching: set[CI] = {ci for ci in ALL_CI if ci.current}  # type: ignore[misc]

    # Return the only matching CI system.
    if len(matching) == 1:
        return matching.pop()

    if len(matching) > 1:
        raise RuntimeError(
            f"Multiple CI matches: {matching!r}. {_unrecognized_message()}"
        )

    # CI env var signals a CI system is present.
    if "CI" in environ:
        _report_unrecognized("CI", strict=strict)
    else:
        _report_unrecognized("CI", strict=strict, expected=False)
    return UNKNOWN_CI


@cache
def current_agent(strict: bool = False) -> Agent:
    """Returns the {class}`~extra_platforms.Agent` matching the current environment.

    Returns {data}`~extra_platforms.UNKNOWN_AGENT` if not running inside a recognized
    agent. To raise an error instead, set `strict` to `True`.

    ```{important}
    Always raises an error if multiple agents match.
    ```

    ```{note}
    Unlike architectures, platforms, and shells, an agent is not always present.
    Local development without AI agents has no agent running.

    If the `LLM` environment variable is set, an unrecognized agent logs at
    `WARNING` level, as it suggests an AI agent is present but not recognized.
    Otherwise, it logs at `INFO` level.
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .agent_data import UNKNOWN_AGENT
    from .group_data import ALL_AGENTS

    # Collect all matching agents.
    matching: set[Agent] = {
        agent  # type: ignore[misc]
        for agent in ALL_AGENTS
        if agent.current
    }

    # Return the only matching agent.
    if len(matching) == 1:
        return matching.pop()

    if len(matching) > 1:
        raise RuntimeError(
            f"Multiple agent matches: {matching!r}. {_unrecognized_message()}"
        )

    # LLM env var signals an AI agent is present.
    if "LLM" in environ:
        _report_unrecognized("agent", strict=strict)
    else:
        _report_unrecognized("agent", strict=strict, expected=False)
    return UNKNOWN_AGENT


@cache
def current_traits() -> set[Trait]:
    """Returns all traits matching the current environment.

    This includes {class}`~extra_platforms.Architecture`,
    {class}`~extra_platforms.Platform`, {class}`~extra_platforms.Shell`,
    {class}`~extra_platforms.Terminal`, {class}`~extra_platforms.CI` systems,
    and {class}`~extra_platforms.Agent` environments.

    ```{caution}
    Never returns {data}`~extra_platforms.UNKNOWN` traits.
    ```

    Raises {exc}`SystemError` if the current environment is not recognized at all.

    ```{important}
    This function returns **all detectable traits**, not the disambiguated
    primary trait per category. Multiple shells, platforms, or other traits
    may appear in the result (for example, both
    {data}`~extra_platforms.BASH` and {data}`~extra_platforms.POWERSHELL`
    on GitHub Ubuntu runners where ``PSModulePath`` leaks from Azure).

    Use the individual ``current_*()`` functions
    ({func}`~extra_platforms.current_shell`,
    {func}`~extra_platforms.current_platform`, etc.) to get the single
    best match per trait type.
    ```

    ```{attention}
    At this point it is too late to worry about caching. This function has no
    choice but to evaluate all detection heuristics.
    ```
    """
    # Lazy imports to avoid circular dependencies.
    from .group_data import ALL_TRAITS, UNKNOWN

    # Collect all matching traits.
    matching = {trait for trait in ALL_TRAITS - UNKNOWN if trait.current}

    if not matching:
        raise SystemError(f"Unrecognized environment: {_unrecognized_message()}")

    return matching
