"""Thin wrapper around swaymsg and local process launching.

Identical in shape to hosts/thin-client/agent/thinclient_agent/sway_control.py and
hosts/touch-panel/agent/touchpanel_agent/sway_control.py — duplicated rather than
imported across hosts, same convention as the rest of this repo's agents.
"""

from __future__ import annotations

import glob
import logging
import os
import subprocess

log = logging.getLogger(__name__)

# Contract with configs/sway/config — these strings must match the `set $ws_*` lines,
# and with configs/session/media-session and configs/session/session-watcher, which
# hardcode the same names in shell.
WS_STEAM = "1:steam"
WS_GAMES = "2:games"
WS_WEB = "3:web"
WS_MEDIA = "4:media"
WS_MUSIC = "5:music"


def runtime_dir() -> str:
    return os.environ.get("XDG_RUNTIME_DIR") or f"/run/user/{os.getuid()}"


class SwayControl:
    def _socket_path(self) -> str | None:
        path = os.environ.get("SWAYSOCK")
        if path and os.path.exists(path):
            return path
        # sway names the socket sway-ipc.<uid>.<pid>.sock, so the path changes every
        # time sway restarts. steamtv-agent is a system service that outlives the
        # session, so the socket is re-resolved per call instead of cached at startup.
        matches = sorted(glob.glob(os.path.join(runtime_dir(), "sway-ipc.*.sock")))
        return matches[-1] if matches else None

    def session_env(self) -> dict[str, str]:
        env = dict(os.environ)
        env["XDG_RUNTIME_DIR"] = runtime_dir()
        env.setdefault("DBUS_SESSION_BUS_ADDRESS", f"unix:path={runtime_dir()}/bus")
        env.setdefault("WAYLAND_DISPLAY", "wayland-1")
        sock = self._socket_path()
        if sock:
            env["SWAYSOCK"] = sock
        return env

    def swaymsg(self, *args: str) -> str | None:
        if self._socket_path() is None:
            log.warning("no sway IPC socket found; dropping command %s", " ".join(args))
            return None
        try:
            result = subprocess.run(
                ["swaymsg", *args],
                env=self.session_env(),
                capture_output=True,
                text=True,
                timeout=10,
                check=False,
            )
        except (OSError, subprocess.TimeoutExpired) as exc:
            log.warning("swaymsg %s failed: %s", " ".join(args), exc)
            return None
        if result.returncode != 0:
            log.warning("swaymsg %s: %s", " ".join(args), result.stderr.strip())
            return None
        return result.stdout

    def switch_workspace(self, name: str) -> None:
        log.info("switching to workspace %s", name)
        self.swaymsg("workspace", name)

    def focus_window(self, criteria: str) -> None:
        self.swaymsg(f"[{criteria}] focus")

    def is_running(self, pattern: str) -> bool:
        """Match `pattern` against full command lines (pgrep -f)."""
        return self._pgrep("-f", pattern)

    def is_process(self, name: str) -> bool:
        """Match `name` against process names only (pgrep -x).

        Separate from is_running() because Steam is the one thing here that needs it:
        its command line is `/bin/sh /usr/games/steam -gamepadui …` and it spawns a
        dozen helpers (steamwebhelper, reaper, steamerrorreporter) whose command lines
        all contain the word "steam". A -f match would report Steam as running long
        after the client has gone, which would leave the session-mode sensor stuck on
        "gaming" for the rest of the evening.
        """
        return self._pgrep("-x", name)

    def _pgrep(self, flag: str, pattern: str) -> bool:
        try:
            result = subprocess.run(
                ["pgrep", "-u", str(os.getuid()), flag, pattern],
                capture_output=True,
                timeout=5,
                check=False,
            )
        except (OSError, subprocess.TimeoutExpired):
            return False
        return result.returncode == 0

    def launch_app(
        self,
        command: list[str],
        process_pattern: str | None = None,
        workspace: str | None = None,
        focus_criteria: str | None = None,
    ) -> None:
        if workspace:
            self.switch_workspace(workspace)

        if process_pattern and self.is_running(process_pattern):
            log.info("%s already running; focusing instead of launching", command[0])
            if focus_criteria:
                self.focus_window(focus_criteria)
            return

        log.info("launching %s", " ".join(command))
        try:
            subprocess.Popen(
                command,
                env=self.session_env(),
                stdin=subprocess.DEVNULL,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True,
            )
        except OSError as exc:
            log.error("could not launch %s: %s", " ".join(command), exc)
