"""Home scoping for ``hermes update``'s fleet restart (#93349).

The restart phase enumerates ``hermes-gateway*``/``hermes-serve*`` units, ``ai.hermes.gateway*``
LaunchAgents and every ``gateway run`` process on the host. Those are HOST-wide namespaces: a
second Hermes install (another ``HERMES_HOME`` root under the same account, its own checkout and
venv) shares them, and the update used to restart that install's gateway too — including the
account's real ``hermes-gateway.service`` when a scratch home ran ``hermes update``.

The fleet an update owns is the set of homes its plan inventories: the updating root plus every
``<root>/profiles/<name>``. Ownership is judged from what a runtime actually runs on — the live
process environment (``HERMES_HOME``/``HOME``, via ``_hermes_home_for_pid``), the unit's declared
``Environment=`` or the plist's pinned ``HERMES_HOME`` — never from a unit label or an argv
substring. Unknown ownership is left alone: a restart we cannot prove is ours is somebody else's
outage.
"""

from __future__ import annotations

import shlex
from contextlib import suppress
from pathlib import Path


def _resolved(path) -> Path | None:
    try:
        return Path(str(path)).expanduser().resolve()
    except (OSError, RuntimeError, ValueError):
        return None


def update_scope_homes() -> set[Path]:
    """Resolved homes the running update owns: the invoking home, the install root and its profiles."""
    homes: set[Path] = set()
    with suppress(Exception):
        from hermes_constants import get_hermes_home
        if (home := _resolved(get_hermes_home())) is not None:
            homes.add(home)
    with suppress(Exception):
        from hermes_cli.update_receipt import _profile_homes
        for _profile, home in _profile_homes():
            if (resolved := _resolved(home)) is not None:
                homes.add(resolved)
    return homes


def home_in_update_scope(home, scope: set[Path] | None = None) -> bool:
    """True when *home* (a path or string) is one of the homes this update owns."""
    if not home:
        return False
    resolved = _resolved(home)
    if resolved is None:
        return False
    return resolved in (update_scope_homes() if scope is None else scope)


def _ledger_gateway_home(pid: int) -> str | None:
    """Home this install's spawn ledger records for gateway *pid* (``register_self`` at gateway boot),
    only for an exact live ``(pid, create_time)`` match. Proof for a gateway whose environment is
    unreadable (elevated, or another account's service) while its create time still is."""
    with suppress(Exception):
        from hermes_cli.process_identity import ledger_entries
        for entry in ledger_entries(verified_only=True):
            if entry.get("pid") == pid and entry.get("purpose") == "gateway":
                return entry.get("hermes_home") or None
    return None


def gateway_pid_in_update_scope(pid: int, scope: set[Path] | None = None) -> bool | None:
    """Does gateway *pid* run on a home this update owns? ``None`` when its home cannot be read."""
    from hermes_cli.dashboard_procs import _hermes_home_for_pid
    try:
        home = _hermes_home_for_pid(pid)
    except Exception:
        home = None
    home = home or _ledger_gateway_home(pid)
    if home is None:
        return None
    return home_in_update_scope(home, scope)


def partition_gateway_pids_by_scope(pids, scope: set[Path] | None = None) -> tuple[list[int], list[tuple[int, str | None]]]:
    """``(owned, foreign)`` split of *pids*; ``foreign`` pairs each PID with its home (None = unreadable)."""
    scope = update_scope_homes() if scope is None else scope
    owned: list[int] = []
    foreign: list[tuple[int, str | None]] = []
    for pid in pids:
        verdict = gateway_pid_in_update_scope(pid, scope)
        if verdict:
            owned.append(pid)
        else:
            home = None
            if verdict is False:
                with suppress(Exception):
                    from hermes_cli.dashboard_procs import _hermes_home_for_pid
                    home = _hermes_home_for_pid(pid)
            foreign.append((pid, home))
    return owned, foreign


def systemd_unit_hermes_home(scope_cmd: list, svc_name: str) -> str | None:
    """Home the systemd unit *svc_name* runs on: its live MainPID's environment first, then the
    unit's declared ``Environment=HERMES_HOME``; for a user-scope unit that declares none, the
    user's own default home. ``None`` when nothing readable names a home."""
    from hermes_cli.update_cmd_fleet import _systemctl, _unit_main_pid

    pid = _unit_main_pid(scope_cmd, svc_name)
    if pid > 0:
        with suppress(Exception):
            from hermes_cli.dashboard_procs import _hermes_home_for_pid
            if (home := _hermes_home_for_pid(pid)) is not None:
                return home
    try:
        shown = _systemctl(list(scope_cmd) + ["show", svc_name, "--property=Environment", "--value"], timeout=10)
    except Exception:
        return None
    if getattr(shown, "returncode", 1) != 0:
        return None
    env_line = (getattr(shown, "stdout", "") or "").strip()
    try:
        tokens = shlex.split(env_line)
    except ValueError:
        tokens = env_line.split()
    for token in tokens:
        key, sep, value = token.partition("=")
        if sep and key == "HERMES_HOME" and value.strip():
            return value.strip()
    if "--user" in scope_cmd:
        return str(Path.home() / ".hermes")
    return None


def systemd_unit_in_update_scope(scope_cmd: list, svc_name: str, scope: set[Path] | None = None) -> bool | None:
    """Does unit *svc_name* belong to this update? ``None`` = ownership unreadable (leave it alone)."""
    home = systemd_unit_hermes_home(scope_cmd, svc_name)
    if home is None:
        return None
    return home_in_update_scope(home, scope)


def launchd_label_foreign_home(label: str, scope: set[Path] | None = None) -> str | None:
    """The HERMES_HOME a derived launchd label's installed plist pins when that home is NOT one of
    this update's — labels are account-global, so root B's default profile derives the same bare
    ``ai.hermes.gateway`` root A installed. ``None`` = ours, or no/unreadable plist (the locate step
    decides whether a job exists; only a proven foreign home is refused)."""
    import plistlib
    with suppress(Exception):
        from hermes_cli.gateway import get_launchd_plist_path
        plist_path = get_launchd_plist_path().with_name(f"{label}.plist")
        if not plist_path.exists():
            return None
        data = plistlib.loads(plist_path.read_bytes())
        pinned = str(data["EnvironmentVariables"]["HERMES_HOME"])
        return None if home_in_update_scope(pinned, scope) else pinned
    return None


def describe_skipped_runtime(kind: str, name: str, home: str | None) -> str:
    """One notice line for a runtime the update leaves alone (foreign home or unreadable ownership)."""
    if home is None:
        return f"  ↷ {name}: {kind} whose Hermes home could not be read — left alone (not restarted)"
    return f"  ↷ {name}: {kind} of another Hermes home ({home}) — left alone (not restarted)"
