"""Install and remove the Linux desktop entry (``<app_id>.desktop``).

The entry must be launch-context independent: ``Exec=`` is an absolute launcher that survives the
venv (no ``#!/usr/bin/env python3`` escapes, no checkout-internal argv[0]), and ``Icon=`` is the
themed name backed by a copy in the user's hicolor tree. Cache refresh is best-effort and
tool-gated (``update-desktop-database``, ``gtk-update-icon-cache``, ``kbuildsycoca6``/``5``); a
missing tool is not an error.
"""

from __future__ import annotations

import io
import os
import re
import shlex
import shutil
import struct
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Callable, Mapping, Optional

# Identity the packaged app claims for its window: electron-builder bakes product-identity.cjs's
# `appId` into extraMetadata.desktopName, and Electron hands that string to the compositor
# verbatim (Wayland app_id, CHROME_DESKTOP). GNOME links a window to a launcher by StartupWMClass
# or by a `<app_id>.desktop` file name, so the entry has to carry the same id — under the old
# "hermes.desktop" name a packaged launch matches neither rung and lands on the placeholder icon.
APP_ID = "com.nousresearch.hermes"
DESKTOP_ENTRY_NAME = f"{APP_ID}.desktop"

# Entry name written before the app-id rename; a successful install converts it into a hidden
# alias (NoDisplay=true) so pre-rename taskbar pins keep resolving (see _alias_legacy_desktop_entry).
LEGACY_DESKTOP_ENTRY_NAME = "hermes.desktop"

# XDG startup notification: set by an app-grid / menu launch, absent for terminal and detached
# (updater relaunch) launches. See launched_from_shell().
SHELL_LAUNCH_ENV_VAR = "DESKTOP_STARTUP_ID"
# Write end of the reveal pipe handed to Electron; one byte means "main window is on screen".
READY_FD_ENV_VAR = "HERMES_DESKTOP_READY_FD"
REVEAL_BYTE = b"r"  # what linux-launcher-ready.ts writes; anything else is finish()'s wake-up

_SHELL_NAMES = ("bash", "sh", "dash", "zsh", "ksh")


def is_supported() -> bool:
    """XDG desktop entries exist only on Linux and BSD."""
    return sys.platform.startswith(("linux", "freebsd", "openbsd", "netbsd"))


def _xdg_data_home() -> Path:
    raw = os.environ.get("XDG_DATA_HOME")
    return Path(raw).expanduser() if raw and raw.strip() else Path.home() / ".local" / "share"


def desktop_entry_path() -> Path:
    return _xdg_data_home() / "applications" / DESKTOP_ENTRY_NAME


def icon_path(project_root: Path) -> Path:
    return project_root / "apps" / "desktop" / "assets" / "icon.png"


def _running_interpreter() -> str:
    """Venv-semantic interpreter path for the persisted ``Exec=`` line.

    ``sys.executable`` inside a venv is commonly a SYMLINK into a shared base-interpreter tree
    (uv, pyenv, conda). ``resolve()`` follows it out of the venv, and CPython discovers
    ``pyvenv.cfg`` from the *lexical* argv[0] — so a dereferenced path boots without the venv's
    site-packages. Keep the lexical form when any ancestor holds a ``pyvenv.cfg``.

    See #80547, #90292.
    Idea credit: the lexical-preservation rule was independently proposed in #92516/#94115/#94544 and by
    nosliwhtes' review of this PR; the pyvenv.cfg-detection refinement here keeps both properties.
    """
    lexical = os.path.abspath(sys.executable)
    path = Path(lexical)
    if any((base / "pyvenv.cfg").is_file() for base in (path.parent, *path.parent.parents)):
        return lexical
    return str(path.resolve())


_probe_cache: "dict[str, bool]" = {}


def _can_import_hermes_cli(interpreter: Path) -> bool:
    """Whether *interpreter* can import ``hermes_cli.main`` unaided.

    Runs under ``-I`` (no user site, no PYTHONPATH, no cwd on ``sys.path``) from a neutral cwd, so
    the answer matches a cold desktop environment. Cached per process; an unprobeable interpreter
    (missing binary, spawn failure, timeout) is assumed capable and deliberately NOT cached, so one
    transient hiccup doesn't freeze the assumption for the session.

    Probe design per @nosliwhtes' isolated-mode capability check (#92122 lineage, commit 4150501f641).
    """
    key = str(interpreter)
    if key in _probe_cache:
        return _probe_cache[key]
    ok = _run_quiet(
        [key, "-I", "-c", "import hermes_cli.main"],
        cwd=os.path.abspath(os.sep), timeout=15, on_error=None,
    )
    if ok is None:
        return True
    _probe_cache[key] = ok
    return ok


def _running_interpreter_fallback() -> str:
    """The RUNNING interpreter — it has ``hermes_cli`` importable by definition."""
    return os.path.abspath(sys.executable)


def resolve_exec_command(project_root: Optional[Path] = None) -> str:
    """Build the absolute ``Exec=`` command line for ``hermes desktop``.

    Prefer the real ``hermes`` launcher; fall back to ``<python> -m hermes_cli.main desktop``.
    """
    from hermes_cli.relaunch import resolve_hermes_bin

    bin_path = _resolve_hermes_bin_for_desktop_entry(resolve_hermes_bin, checkout_root=project_root)
    interpreter = _running_interpreter()
    if not _can_import_hermes_cli(Path(interpreter)):
        # Persisting an interpreter that can't import the CLI writes a dead entry (the DE spawns
        # Exec in a cold environment where exactly this import must succeed).
        # The candidate interpreter cannot actually import hermes_cli.main (checked in isolated mode from a
        # neutral cwd — so the probe can't be fooled by a checkout cwd or an inherited PYTHONPATH). Fall
        # back to the module form under the RUNNING interpreter, which by definition has the CLI importable.
        # Probe design follows the isolated-mode capability check proposed by @nosliwhtes (#92122 review
        # lineage, commit 4150501f641) — cached here per-process so a desktop launch pays the subprocess
        # cost at most once.
        interpreter = _running_interpreter_fallback()
    argv = [interpreter, "-m", "hermes_cli.main", *_desktop_argv_tail(project_root)]
    if bin_path:
        resolved = Path(bin_path).resolve()
        # A Python launcher whose shebang points OUTSIDE the venv (e.g. the repo's `hermes` script
        # with `#!/usr/bin/env python3`) would die silently on the first third-party import under
        # Terminal=false — run it under the venv interpreter explicitly.
        prefix = [interpreter] if _needs_interpreter(resolved) else []
        # See #90292.
        argv = [*prefix, str(resolved), *_desktop_argv_tail(project_root)]
    return " ".join(_quote_exec_arg(a) for a in argv)


def _desktop_argv_tail(project_root: Optional[Path]) -> list[str]:
    """The ``desktop`` subcommand arguments a persisted launcher should carry.

    A menu/taskbar click is a launch, not a build request: when a packaged
    Electron app already exists under the checkout it must start directly
    (``--skip-build``) instead of re-running the build-then-launch path, whose
    source-hash freshness check reports "stale" on any locally modified tree
    and makes every click pay a 60s+ rebuild that can fail outright (#126009).
    With no packaged app yet (first install), keep the build-then-launch
    default so the click still produces an app.
    """
    if project_root is None:
        return ["desktop"]
    release = project_root / "apps" / "desktop" / "release"
    packaged = any(
        (release / dist / name).exists()
        for dist in ("linux-unpacked", "linux-arm64-unpacked")
        for name in ("hermes", "Hermes")
    )
    return ["desktop", "--skip-build"] if packaged else ["desktop"]


def _is_interpreter(candidate: Path) -> bool:
    """A python interpreter binary (``bin/python*``), not a launcher: strict basename match
    (rejects ``python3-config``, ``pythonw``) inside a bin/Scripts dir (rejects a stray script
    named ``python`` elsewhere).

    Regex approach proposed independently in 94051; kept here with the parent-dir guard so a script named
    ``python`` outside a bin/Scripts tree is not misclassified. See #94051.
    """
    return bool(re.fullmatch(r"python[23]?(\d+)?(\.\d+)?", candidate.name.lower())) and (
        candidate.parent.name in {"bin", "scripts"}
    )


def _inside_checkout(candidate: str, checkout_root: Path, original_argv0: str) -> bool:
    """Whether a launcher candidate is a launch-context artifact rather than a durable launcher."""
    try:
        path = Path(candidate).resolve()
    except OSError:
        return False
    # Anything shipped in the tree (e.g. the repo `hermes` script) is checkout-internal. Compare
    # against BOTH the lexical and resolved roots: candidates resolve, so a symlinked home needs
    # the resolved comparison too.
    try:
        resolved_root = checkout_root.resolve()
    except OSError:
        resolved_root = None
    for root in {checkout_root, resolved_root}:
        if root is not None and (path == root or root in path.parents):
            return True
    # The `python -m hermes_cli.main` relaunch context surfaces the invoking interpreter as
    # argv[0]; an interpreter is never a launchable entry target (it would persist a bare
    # `<python> desktop`). Compare against argv[0]'s own file, not sys.executable — under test
    # harnesses they differ.
    try:
        return path.samefile(original_argv0) and _is_interpreter(path)
    except OSError:
        return False


def _is_this_checkout_managed_cli(candidate: str, checkout_root: Path) -> bool:
    """True when *candidate* is this checkout's PM-managed console script.

    That script lives under ``installs/<key>/environments`` and imports a
    generated workspace that does not ship ``apps/desktop``. It sits outside
    the checkout, so the external-primary rule would persist it and the
    desktop launcher would exit with the GUI source missing.
    """
    try:
        path = Path(candidate).resolve()
    except OSError:
        return False
    try:
        from pm.environments import install_state_dir

        generations = (install_state_dir(Path(checkout_root)) / "environments").resolve()
    except (OSError, RuntimeError, ValueError):
        return False
    return path == generations or generations in path.parents


def _resolve_hermes_bin_for_desktop_entry(
    resolve_fn=None,
    checkout_root: Optional[Path] = None,
) -> Optional[str]:
    """Resolve the launcher binary for the persisted ``.desktop`` entry.

    Wraps :func:`hermes_cli.relaunch.resolve_hermes_bin` with one rule: an ``argv[0]`` inside this
    checkout is a launch-context artifact, not a durable installed launcher — persisting it makes
    the entry depend on how the previous launch happened (a bootstrap loop). Skip such candidates
    and fall through to PATH, then to the installer's known wrapper locations. A candidate that
    provably cannot serve ``hermes desktop`` (a code tree carrying ``hermes_cli`` but no desktop
    app beside it — a managed runtime env's console script) is skipped the same way: its entry
    would only ever die with "Desktop GUI source not found". ``resolve_fn`` is injectable for tests.

    See #90492.
    """
    if resolve_fn is None:
        from hermes_cli.relaunch import resolve_hermes_bin as resolve_fn

    if checkout_root is None:
        checkout_root = _project_root()
    # Keep the LEXICAL root: the installer writes $INSTALL_DIR lexically into the shim text, so a
    # symlinked home would otherwise mismatch. Callers pass main.py's realpath'd PROJECT_ROOT; the
    # module-lexical root is tried alongside it.
    checkout_root = Path(os.path.abspath(checkout_root))
    module_lexical_root = _project_root()
    original_argv0 = sys.argv[0]

    # An external primary (another install's /opt/.../bin/hermes, a venv console script) wins
    # BEFORE any known-location probing, which could silently switch the entry to a different
    # installation. Only rerun the resolver with argv[0] hidden when the primary could actually
    # be checkout-internal (also shortens the window a concurrent reader sees mutated sys.argv).
    primary = resolve_fn()
    # A managed-environment console script is outside the checkout but is not a durable desktop
    # launcher: its workspace has no apps/desktop (#122438). The same holds for any launcher
    # that provably cannot serve `hermes desktop` (another install's managed env, #122485).
    # Fall through so the durable-wrapper probe below can still find a working one.
    if (
        primary
        and not _inside_checkout(primary, checkout_root, original_argv0)
        and not _is_this_checkout_managed_cli(primary, checkout_root)
        and not _is_this_checkout_managed_cli(primary, module_lexical_root)
        and _can_serve_desktop(primary) is not False
    ):
        return primary

    # A primary that is NOT checkout-internal and not the invoking interpreter is an external launcher (e.g.
    # /opt/.../bin/hermes from another install method, or a venv console script). It must be evaluated
    # BEFORE any known-location probing: probing first could silently switch the entry to a different
    # installation (#94443 review case 3).
    # Only reroute when argv[0] actually drove the resolution: re-run the resolver with argv[0] hidden and
    # compare. If PATH yields nothing, keep the resolver's original answer (its fallback chain stays
    # authoritative; #90492 semantics preserved).
    sys.argv[0] = ""
    try:
        rerouted = resolve_fn()
    finally:
        sys.argv[0] = original_argv0

    # A resolver miss (argv[0] is ``-c`` under ``python -m`` on a cold relaunch AND PATH has no
    # ``hermes``) must NOT return None here: that skipped the durable-wrapper probe below and persisted
    # the module form, so the entry's bytes flipped on every alternating launch context — and
    # gnome-shell 50.x crashes when the entry changes while its ShellApp is STARTING (#110885).
    # ``primary is None`` implies ``rerouted is None`` (the rerun only hides argv[0]), so only the
    # probe can still find anything.
    if (
        primary
        and rerouted is not None
        and not _inside_checkout(rerouted, checkout_root, original_argv0)
        and not _is_this_checkout_managed_cli(rerouted, checkout_root)
        and not _is_this_checkout_managed_cli(rerouted, module_lexical_root)
        and _can_serve_desktop(rerouted) is not False
    ):
        return rerouted
    # A PATH hit inside this checkout is the same launch-context artifact as argv[0]: the
    # desktop-update hand-off hands the updater <checkout>/venv/bin at the front of PATH, so
    # persisting a reroute to the venv console script pins the entry to WHO wrote it. The next
    # DE-launched context re-resolves to the durable wrapper and flips the bytes back — and
    # every flip rewrites the entry, which arms the gnome-shell 50.x crash this function's
    # callers guard against when the write lands inside a launch's STARTING window. Fall
    # through to the durable probe below, exactly as a PATH miss does.

    # argv[0] was checkout-internal AND PATH yielded no DURABLE launcher (miss, or a hit inside
    # this checkout) — common in stripped systemd user sessions, autostart relaunches, and the
    # update hand-off with <checkout>/venv/bin on PATH. Probe the installer's known wrapper
    # locations; each
    # candidate must be DE-safe and target THIS checkout (a foreign wrapper would make the entry
    # stable-but-wrong). No durable wrapper → None, so resolve_exec_command emits its runnable
    # module fallback instead of the self-regenerating checkout-internal form.
    for candidate in _known_wrapper_candidates():
        if (
            candidate.is_file()
            and os.access(candidate, os.X_OK)
            and _wrapper_shebang_safe(candidate)
            and (
                _wrapper_targets_checkout(candidate, checkout_root)
                or _wrapper_targets_checkout(candidate, module_lexical_root)
            )
        ):
            return str(candidate)
    return None


def _shebang_tokens(shebang: str) -> "list[str]":
    return shebang[2:].strip().split()


def _is_native_binary(head: bytes) -> bool:
    return head[:4] == b"\x7fELF" or head.startswith(b"MZ")


def _read_head(path: Path, size: int = 4096) -> Optional[bytes]:
    try:
        with open(path, "rb") as fh:
            return fh.read(size)
    except OSError:
        return None


def _wrapper_shebang_safe(wrapper: Path) -> bool:
    """Whether an executable wrapper can actually run in the DE context.

    Native binaries and shell launchers are safe by construction (the shell script execs the right
    interpreter itself). A python-shebang wrapper is safe only when its interpreter stays inside
    the RUNNING venv; anything unknown fails safe toward the module fallback.
    """
    head = _read_head(wrapper)
    if head is None:
        return False
    if _is_native_binary(head):
        return True
    if not head.startswith(b"#!"):
        return False
    shebang = head.decode("utf-8", errors="replace").splitlines()[0]
    tokens = _shebang_tokens(shebang)
    if not tokens:
        return False
    interp = Path(tokens[0])
    if interp.name == "env":
        # `#!/usr/bin/env bash` is the installer's own launcher form; skip env's flags (-S, -u
        # VAR) and inspect the first real token. Only python-flavored `env` shebangs escape.
        target = next((Path(t) for t in tokens[1:] if not t.startswith("-")), Path(""))
        return target.name in _SHELL_NAMES or not _shebang_escapes_running_env(shebang)
    if interp.name in _SHELL_NAMES:
        return True
    if "python" not in interp.name.lower():
        return False
    return not _shebang_escapes_running_env(shebang)


_ROOT_TERMINATORS = ('"', "'", " ", "\n", "\t", "\r", "$", "\x00", "/")


def _wrapper_targets_checkout(wrapper: Path, checkout_root: Path) -> bool:
    """Whether a candidate launcher script actually launches THIS checkout.

    Boundary-aware: a bare substring test would also accept sibling paths that EXTEND this
    checkout's path (``<checkout>-old``, ``<checkout>.bak``), so the root must end at a quote /
    whitespace / EOL or continue INTO the tree (``<root>/``). Both lexical and resolved roots are
    tried: the installer writes $INSTALL_DIR lexically, so with a symlinked home the shim text
    carries the lexical path while the caller may pass a resolved one.
    """
    head = _read_head(wrapper)
    if head is None:
        return False
    if _is_native_binary(head):
        # Native binary: cannot verify, cannot be another checkout's bash shim either — accept.
        return True
    text = head.decode("utf-8", errors="replace")
    lexical_root = os.path.abspath(str(checkout_root))
    roots = {str(checkout_root), lexical_root}
    try:
        roots.add(str(Path(lexical_root).resolve()))
    except OSError:
        pass
    stripped = text.rstrip("\r\n")
    return any(
        stripped.endswith(root) or any(root + t in text for t in _ROOT_TERMINATORS)
        for root in roots
    )


_HERMES_CODE_MARKERS = ("hermes_cli", "workspace/hermes_cli")
_DESKTOP_APP_ENTRIES = ("apps/desktop/package.json", "workspace/apps/desktop/package.json")


def _launcher_tree(path: Path) -> Path:
    """Install tree a ``bin`` launcher belongs to: ``<tree>/venv/bin/x`` → ``<tree>``."""
    parent = path.parent
    if parent.name not in ("bin", "Scripts", "scripts"):
        return parent
    tree = parent.parent
    return tree.parent if tree.name in ("venv", ".venv", "env") else tree


def _tree_desktop_state(tree: Path) -> Optional[bool]:
    """``True`` when *tree* can serve ``hermes desktop``, ``False`` when it provably cannot, else ``None``.

    ``False`` is reserved for a tree that IS a hermes code tree (carries ``hermes_cli``) yet has no
    desktop app beside it — the managed runtime env layout, whose launcher runs but dies with
    "Desktop GUI source not found". Unfamiliar shapes stay ``None`` (accepted) so no install
    method is rejected for looking exotic.
    """
    if any((tree / entry).is_file() for entry in _DESKTOP_APP_ENTRIES):
        return True
    from hermes_cli.steward import is_bundled_payload

    if is_bundled_payload(tree):
        return True
    if any((tree / marker).is_dir() for marker in _HERMES_CODE_MARKERS):
        return False
    return None


def _embedded_launcher_target(wrapper: Path) -> Optional[Path]:
    """First absolute path to an existing ``bin`` launcher embedded in a shell wrapper script."""
    head = _read_head(wrapper)
    if head is None:
        return None
    for match in re.finditer(r"""["'\s](/[^\s"'$;)\\]+)""", head.decode("utf-8", errors="replace")):
        if "/bin/" in match.group(1) and Path(match.group(1)).is_file():
            return Path(match.group(1))
    return None


def _can_serve_desktop(candidate: str, _depth: int = 2) -> Optional[bool]:
    """Desktop-capability of a launcher path; shell wrappers followed to their target.

    Native binaries, unreadable files, interpreters, and shapes without markers stay ``None``;
    only a launcher whose tree provably lacks the desktop app is rejected (``False``).
    """
    try:
        path = Path(candidate)
        if not path.is_file():
            return None
        head = _read_head(path, 256)
    except OSError:
        return None
    if head is None or not head.startswith(b"#!"):
        return None
    tokens = _shebang_tokens(head.decode("utf-8", errors="replace").splitlines()[0])
    if _depth and tokens and (Path(tokens[0]).name in _SHELL_NAMES or Path(tokens[0]).name == "env"):
        # The installer's shim (any shell launcher) execs its real target; judge THAT tree.
        target = _embedded_launcher_target(path)
        if target is not None:
            verdict = _can_serve_desktop(str(target), _depth - 1)
            if verdict is not None:
                return verdict
    return _tree_desktop_state(_launcher_tree(path))


def _persisted_exec_serves_desktop(exec_command: str) -> Optional[bool]:
    """Desktop-capability of a rendered ``Exec`` line, ignoring an interpreter prefix.

    ``[<interpreter>, <launcher>, desktop]`` is judged by the launcher. The module fallback
    ``[<interpreter>, -m, hermes_cli.main, desktop]`` passes: it is only ever written by a
    process that already passed the desktop launch checks.
    """
    try:
        tokens = shlex.split(exec_command)
    except ValueError:
        return None
    if not tokens:
        return None
    candidate = tokens[0]
    if _is_interpreter(Path(candidate)):
        if len(tokens) > 1 and tokens[1] != "-m":
            candidate = tokens[1]  # `[<interpreter>, <launcher>, desktop]` prefix form
        else:
            # Module fallback `[<interpreter>, -m, hermes_cli.main, desktop]`: only a process
            # that already passed the desktop launch checks writes it, so it is not
            # second-guessed here.
            return None
    return _can_serve_desktop(candidate)


def _known_wrapper_candidates():
    """Durable installed-launcher locations, most likely first.

    Mirrors the installer's ``get_command_link_dir()`` layouts: Termux (``$PREFIX/bin``), root FHS
    (``/usr/local/bin``), and user (``~/.local/bin``). The wrapper is always named ``hermes``.
    """
    candidates = []
    prefix = os.environ.get("PREFIX")
    if prefix:
        candidates.append(Path(prefix) / "bin" / "hermes")
    if hasattr(os, "geteuid") and os.geteuid() == 0:
        candidates.append(Path("/usr/local/bin/hermes"))
    candidates.append(Path.home() / ".local" / "bin" / "hermes")
    return candidates


def _project_root() -> Path:
    """``<checkout>`` from this file's location — lexical (no ``.resolve()``) so shim-text matching
    against the installer's lexically-written $INSTALL_DIR works on symlinked homes."""
    return Path(os.path.abspath(__file__)).parent.parent


def _needs_interpreter(bin_path: Path) -> bool:
    """Whether ``bin_path`` is a Python script whose shebang escapes the running venv.

    Native binaries (uv shim, PyInstaller, distro package) and shell wrappers (the installer's
    bash launcher execs the venv python itself) never need one.
    """
    head = _read_head(bin_path, 256)
    if head is None or not head.startswith(b"#!"):
        return False
    shebang = head.decode("utf-8", errors="replace").splitlines()[0].strip()
    if "python" not in shebang.lower():
        return False
    return _shebang_escapes_running_env(shebang)


def _shebang_escapes_running_env(shebang: str) -> bool:
    """Whether a python shebang resolves OUTSIDE the running interpreter's directory.

    Compares PATH COMPONENTS, never substrings: ``<venv>/bin-extra/python`` is not inside
    ``<venv>/bin``. ``env`` shebangs ALWAYS escape — ``env`` resolves through the DE's cold PATH,
    not the shell that installed the venv — except the rare ``env -S <abs-interpreter>`` form,
    which is judged by its absolute target.

    Tokenizes the shebang (interpreter path plus any flags) and compares PATH COMPONENTS, never substrings:
    ``<venv>/bin-extra/python`` is not inside ``<venv>/bin`` even though it starts with it
    (sibling-directory confusion; independently surfaced in nosliwhtes' #92122 hardening ``b96427d0`` —
    reimplemented here with two extensions).
    The comparison uses the LEXICAL interpreter directory (abspath, not resolve()): on uv venvs the resolved
    parent is the base interpreter's dir, which makes a valid ``.venv/bin/python`` shebang look foreign
    (#94443 review case 1). Both sides use the SAME case operation (``.lower()``): interpreter paths
    legitimately carry uppercase (conda env names, usernames, uv's ephemeral build dirs) and an asymmetric
    compare would flag the venv's own console script as foreign.
    """
    tokens = _shebang_tokens(shebang)
    if not tokens:
        return True  # bare "#!python": resolves via PATH
    interp = Path(tokens[0])
    if interp.name in ("env", "env.exe"):
        rest = [t for t in tokens[1:] if not t.startswith("-")]
        if not (rest and Path(rest[0]).is_absolute()):
            return True
        interp = Path(rest[0])
    running_dir = os.path.dirname(os.path.abspath(sys.executable)).lower()
    return str(interp.parent).lower() != running_dir


def _quote_exec_arg(arg: str) -> str:
    """Quote one ``Exec`` argument per the desktop entry spec."""
    if not any(c in arg for c in " \t\n\"'\\><~|&;$*?#()`"):
        return arg
    escaped = arg.replace("\\", "\\\\").replace('"', '\\"')
    return f'"{escaped}"'


def render_desktop_entry(exec_command: str, icon: str) -> str:
    """The app-id entry: identity lives in the file name and ``StartupWMClass``."""
    return (
        "[Desktop Entry]\n"
        "Type=Application\n"
        "Name=Hermes\n"
        "GenericName=Hermes Desktop\n"
        "Comment=Launch Hermes Desktop\n"
        f"Exec={exec_command}\n"
        f"Icon={icon}\n"
        "Terminal=false\n"
        "Categories=Utility;\n"
        "StartupNotify=true\n"
        f"StartupWMClass={APP_ID}\n"
    )


def _render_legacy_alias_entry(exec_command: str, icon: str) -> str:
    """The app-id entry, hidden from the app grid; ``StartupWMClass`` still groups old pins."""
    return render_desktop_entry(exec_command, icon) + "NoDisplay=true\n"


def refresh_desktop_databases(applications_dir: Path) -> "list[str]":
    """Reindex the menu caches. Run each tool only when it exists."""
    ran: list[str] = []

    update_db = shutil.which("update-desktop-database")
    if update_db and _run_quiet([update_db, str(applications_dir)]):
        ran.append("update-desktop-database")

    # Plasma 6 first, then Plasma 5. Only one of them is ever installed.
    for tool in ("kbuildsycoca6", "kbuildsycoca5"):
        resolved = shutil.which(tool)
        if resolved:
            if _run_quiet([resolved, "--noincremental"]):
                ran.append(tool)
            break

    return ran


def _run_quiet(cmd: "list[str]", *, timeout: int = 60, on_error: Optional[bool] = False, **kwargs) -> Optional[bool]:
    """Exit-status success of a silenced subprocess; ``on_error`` when it could not be run at all."""
    try:
        result = subprocess.run(
            cmd,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=False,
            timeout=timeout,
            **kwargs,
        )
    except (OSError, subprocess.SubprocessError):
        return on_error
    return result.returncode == 0


# Sizes a typical hicolor ``index.theme`` lists. ``scalable`` is SVG-only — a raster PNG there is
# what Cinnamon's panel draws as a mangled low-res blob. The shipped asset is 1024×1024 (not an
# indexed dir name), so a copy-only fallback lands in ``256x256``.
_HICOLOR_INDEXED_SIZES = (16, 22, 24, 32, 36, 48, 64, 72, 96, 128, 192, 256, 512)
# Cinnamon's panel is ~24–32px: write exact rasters so the theme needn't downscale a 1024px PNG.
_HICOLOR_INSTALL_SIZES = (24, 32, 48, 256)


def _png_dimensions(raw: bytes) -> Optional[tuple[int, int]]:
    """``(width, height)`` from a PNG IHDR, or ``None`` if unreadable."""
    if len(raw) >= 24 and raw[:8] == b"\x89PNG\r\n\x1a\n" and raw[12:16] == b"IHDR":
        return struct.unpack(">II", raw[16:24])
    return None


def _hicolor_subdir(dimensions: Optional[tuple[int, int]]) -> str:
    """Pick a fixed-size hicolor dir the theme indexes. Never ``scalable``."""
    if dimensions is None:
        return "256x256"
    width, height = dimensions
    if width in _HICOLOR_INDEXED_SIZES and width == height:
        return f"{width}x{width}"
    if width != height or width <= 0 or width > 256:
        return "256x256"
    nearest = min(_HICOLOR_INDEXED_SIZES, key=lambda size: abs(size - width))
    return f"{nearest}x{nearest}"


def _hicolor_icon_dest(subdir: str) -> Path:
    return _xdg_data_home() / "icons" / "hicolor" / subdir / "apps" / "hermes.png"


def _remove_stale_scalable_icon() -> bool:
    """Drop a leftover PNG from ``scalable/`` (the pre-fix install path); True if removed."""
    stale = _hicolor_icon_dest("scalable")
    try:
        removed = stale.is_file()
        if removed:
            stale.unlink()
        return removed
    except OSError:
        return False


def _refresh_hicolor_cache() -> None:
    """Best-effort reindex of the user hicolor tree. Missing tool is fine."""
    hicolor = _xdg_data_home() / "icons" / "hicolor"
    for tool in ("gtk-update-icon-cache", "gtk4-update-icon-cache"):
        resolved = shutil.which(tool)
        if resolved:
            _run_quiet([resolved, "-f", "-t", str(hicolor)])
            return


def _resized_hicolor_pngs(raw: bytes) -> Optional[dict[str, bytes]]:
    """Lanczos-resize *raw* to each panel size; ``None`` when it will not decode (truncated/fake
    PNG) so the caller falls back to a copy. Pillow is imported lazily to keep the uninstaller
    import-light."""
    try:
        from PIL import Image
    except ImportError:
        return None
    try:
        with Image.open(io.BytesIO(raw)) as im:
            rgba = im.convert("RGBA")
            out: dict[str, bytes] = {}
            for size in _HICOLOR_INSTALL_SIZES:
                buf = io.BytesIO()
                rgba.resize((size, size), Image.Resampling.LANCZOS).save(buf, format="PNG")
                out[f"{size}x{size}"] = buf.getvalue()
            return out
    except (OSError, ValueError):
        return None


def _write_hicolor_pngs(files: dict[str, bytes]) -> bool:
    """Write *files* keyed by hicolor size dir. Return True if any file changed."""
    wrote = False
    for subdir, data in files.items():
        dest = _hicolor_icon_dest(subdir)
        if dest.is_file() and dest.read_bytes() == data:
            continue
        dest.parent.mkdir(parents=True, exist_ok=True)
        dest.write_bytes(data)
        wrote = True
    return wrote


def _install_icon_to_hicolor(icon: Path) -> bool:
    """Install the app icon into the user's hicolor tree so ``Icon=hermes`` resolves without an
    absolute checkout path. Raster PNGs go to indexed fixed-size dirs, never ``scalable``."""
    try:
        raw = icon.read_bytes()
        resized = _resized_hicolor_pngs(raw)
        if resized is not None:
            wrote = _write_hicolor_pngs(resized)
        else:
            dest = _hicolor_icon_dest(_hicolor_subdir(_png_dimensions(raw)))
            wrote = not (dest.is_file() and dest.read_bytes() == raw)
            if wrote:
                dest.parent.mkdir(parents=True, exist_ok=True)
                shutil.copyfile(icon, dest)
        if _remove_stale_scalable_icon() or wrote:
            _refresh_hicolor_cache()
        return True
    except OSError:
        return False


def _launcher_entry_management_enabled() -> bool:
    """Whether config.yaml allows touching an EXISTING launcher entry.

    ``desktop.manage_launcher_entry: false`` opts out of the every-launch
    rewrite: a hand-edited entry is then left alone instead
    of silently reverting (#101097's clobber complaint), and the pre-rename
    retirement is skipped with it — deletion is management too. A MISSING
    entry is still created regardless — the opt-out protects user edits,
    not first-run presence. Any config error reads as enabled (default).
    """
    try:
        from hermes_cli.config import load_config_readonly

        desktop_cfg = (load_config_readonly() or {}).get("desktop") or {}
        raw = desktop_cfg.get("manage_launcher_entry", True)
        if isinstance(raw, bool):
            return raw
        if isinstance(raw, str):
            return raw.strip().lower() not in ("false", "0", "no", "off")
        return True
    except Exception:
        return True


def _alias_legacy_desktop_entry(applications_dir: Path, exec_command: str, icon: str) -> bool:
    """Keep the pre-rename ``hermes.desktop`` as a hidden alias of the app-id entry.

    Shells resolve a taskbar pin by the entry file name it was pinned against: deleting the
    file makes GNOME drop the favourite and Plasma leave an inert item, and nothing can
    re-pin for the user. The alias stays launchable for old pins without listing Hermes
    twice. Only a file that still names this app is converted; anything else at that path
    is left alone. True when the legacy file was (re)written.
    """
    legacy = applications_dir / LEGACY_DESKTOP_ENTRY_NAME
    try:
        text = legacy.read_text(encoding="utf-8-sig")
    except OSError:
        return False
    if not any(line.strip() == "Name=Hermes" for line in text.splitlines()):
        return False
    alias_contents = _render_legacy_alias_entry(exec_command, icon)
    if text == alias_contents:
        return False
    try:
        from utils import atomic_write_text

        atomic_write_text(legacy, alias_contents, create_mode=0o755)
        legacy.chmod(0o755)
    except OSError:
        return False
    return True


def install_desktop_entry(project_root: Path) -> Optional[Path]:
    """Create or refresh the app-id entry, respecting the opt-out for existing entries.

    Only the app-id entry is written; a pre-rename ``hermes.desktop`` beside it is converted
    into a hidden alias once the new entry exists, and only while launcher management is
    enabled — deleting it instead would silently kill existing taskbar pins (#124492).
    ``None`` on non-Linux platforms, when the write fails, or when the resolved ``Exec``
    provably cannot serve ``hermes desktop`` — a convenience, never a reason to fail a launch.
    """
    if not is_supported():
        return None

    entry_path = desktop_entry_path()

    # Opt-out honored only for an entry that already exists: the flag
    # stops the every-launch clobber, not first-run creation.
    manage_enabled = _launcher_entry_management_enabled()
    if entry_path.is_file() and not manage_enabled:
        return entry_path

    exec_command = resolve_exec_command(project_root)
    # Never persist an Exec the entry provably cannot launch from: a dead entry looks broken
    # (a click that does nothing) while skipping leaves whatever is already on disk instead of
    # churning it. Unknown shapes still write — only a proven mismatch skips.
    if _persisted_exec_serves_desktop(exec_command) is False:
        return None

    icon = icon_path(project_root)
    # Prefer the themed name: the icon is COPIED into the hicolor tree, so the entry outlives the
    # checkout (an absolute Icon= path breaks when the checkout moves). Absolute path only when
    # the copy is impossible (read-only tree); themed name when the checkout has no icon at all.
    icon_value = str(icon) if icon.is_file() else "hermes"
    if icon.is_file() and _install_icon_to_hicolor(icon):
        icon_value = "hermes"
    contents = render_desktop_entry(exec_command, icon_value)

    try:
        entry_path.parent.mkdir(parents=True, exist_ok=True)
        # When nothing changed, skip the rewrite. Then a launch does not
        # churn the menu caches.
        unchanged = entry_path.is_file() and entry_path.read_text(encoding="utf-8-sig") == contents
        if not unchanged:
            # Atomic replace: an interrupted plain write leaves a zero-byte entry, which permanently
            # breaks the taskbar pin (nothing later rewrites a file that exists at the right path).
            # The temp+rename dance in utils.atomic_write_text is the codebase's shared implementation — ported
            # from #80547, which closed unmerged with this piece unlanded.
            from utils import atomic_write_text

            atomic_write_text(entry_path, contents, create_mode=0o755)
            # Some launchers (and older Plasma) offer the entry only when it is executable.
            entry_path.chmod(0o755)
    except OSError:
        return None

    # Converting the old entry is management too: with the opt-out set, an existing
    # launcher stays untouched even here, in the missing-entry path where the new
    # entry is still created.
    aliased = manage_enabled and _alias_legacy_desktop_entry(entry_path.parent, exec_command, icon_value)
    if aliased or not unchanged:
        refresh_desktop_databases(entry_path.parent)
    return entry_path


def launched_from_shell(environ: Optional[Mapping[str, str]] = None) -> bool:
    """True when this process was started from the app grid / menu (XDG startup notification).

    A grid launch has a gnome-shell ShellApp in STARTING until our window maps; unpatched
    shells (before GNOME MR !4428) drop that app's last reference when its own ``.desktop``
    entry changes, and the next idle GC kills the whole Wayland session (#111906). A false
    negative degrades to the pre-#111906 behaviour; a false positive only delays a heal.
    """
    env = os.environ if environ is None else environ
    return bool(env.get(SHELL_LAUNCH_ENV_VAR))


class DeferredDesktopEntryInstall:
    """Install the entry once the desktop window is on screen — never while the shell's
    ShellApp is STARTING.

    The launcher hands Electron the write end of a pipe (``HERMES_DESKTOP_READY_FD``); Electron
    writes one byte when the main window is revealed and a worker thread then installs the entry.
    An exit without a reveal (boot crash, early quit) does NOT heal: gnome-shell keeps the ShellApp
    in STARTING until the startup-notification sequence completes or times out (mutter, ~15 s),
    not until the process dies, so a write right after such an exit still lands in the arming
    window. The next terminal/updater launch or revealed grid launch installs the entry instead.
    Terminal and detached launches never build one of these: they install immediately, as before.
    """

    def __init__(
        self,
        project_root: Path,
        install: Optional[Callable[[Path], Optional[Path]]] = None,
        settle_seconds: float = 2.0,
    ) -> None:
        self._project_root = project_root
        self._install = install or install_desktop_entry
        # Electron reports the reveal before the compositor has necessarily mapped the surface;
        # a short settle after the signal keeps the write on the RUNNING side. It is a margin
        # after the condition, not a substitute for it.
        self._settle_seconds = settle_seconds
        self._read_fd, self.write_fd = os.pipe()
        self._thread = threading.Thread(target=self._wait_for_reveal, name="desktop-entry-install", daemon=True)

    def child_env(self, env: dict) -> dict:
        env[READY_FD_ENV_VAR] = str(self.write_fd)
        return env

    @property
    def pass_fds(self) -> tuple[int, ...]:
        return (self.write_fd,)

    def start(self) -> None:
        self._thread.start()

    def _wait_for_reveal(self) -> None:
        try:
            data = os.read(self._read_fd, 1)
        except OSError:
            return
        if data != REVEAL_BYTE:  # woken by finish(): the app exited without a reveal, skip the heal
            return
        if self._settle_seconds:
            time.sleep(self._settle_seconds)
        try:
            entry = self._install(self._project_root)
            if entry:
                print(f"✓ Desktop launcher entry installed: {entry}")
        except Exception as exc:  # never fail a launch on launcher plumbing
            print(f"⚠ Could not install the desktop launcher entry: {exc}")

    def finish(self) -> None:
        """Electron exited: let a heal already triggered by the reveal complete, never start one."""
        try:
            os.write(self.write_fd, b"x")  # wake a still-waiting reader so join() returns promptly
        except OSError:
            pass
        self._thread.join(timeout=15)
        for fd in (self._read_fd, self.write_fd):
            try:
                os.close(fd)
            except OSError:
                pass
