"""Generic process manager.

Handles desktop-app launching, process killing via ``pkill``, and
bare ``subprocess.Popen`` calls for non-tmux/non-systemd tools.
"""

from __future__ import annotations

import os
import shlex
import shutil
import subprocess
import threading
from collections.abc import Iterable

# Terminal emulator candidates, ordered by preference.
_TERMINALS = [
    "xterm", "konsole", "gnome-terminal", "xfce4-terminal",
    "lxterminal", "alacritty", "kitty", "wezterm",
]


def detect_terminal() -> str:
    """Return the first available terminal emulator on the system."""
    return next((t for t in _TERMINALS if shutil.which(t)), "xterm")


def _to_arg_list(command: str | list[str]) -> list[str]:
    """Coerce a registry command into a safe argv list.

    Accepts either a pre-split argv list (preferred) or a single shell-style
    string (which is split with :func:`shlex.split`, never passed to a shell).
    """
    if isinstance(command, (list, tuple)):
        return [str(c) for c in command]
    return shlex.split(command)


class ProcessManager:
    """Launch and terminate generic (desktop / CLI) processes.

    All launched processes are tracked in :attr:`_launched` so they can be
    polled / reaped periodically, preventing zombie accumulation in a
    long-lived GUI process.
    """

    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._launched: list[subprocess.Popen] = []

    def launch_desktop(self, command: str | list[str]) -> None:
        """Fire-and-forget launch of a desktop command."""
        argv = _to_arg_list(command)
        if not argv:
            raise ValueError("launch_desktop received an empty command")
        with self._lock:
            self._launched.append(subprocess.Popen(argv))

    def launch_terminal(self, command: str | list[str], env: dict[str, str] | None = None) -> None:
        """Open *command* inside a new terminal emulator window."""
        term = detect_terminal()
        # xterm, alacritty, kitty, wezterm use -e; others use --
        sep = "-e" if term in ("xterm", "alacritty", "kitty", "wezterm") else "--"
        cmd_str = shlex.join(_to_arg_list(command)) if isinstance(command, (list, tuple)) else command
        argv = [term, sep, "bash", "-c", cmd_str]
        with self._lock:
            self._launched.append(subprocess.Popen(argv, env=env))

    def kill_by_name(self, search_term: str) -> None:
        """Send SIGTERM to all processes matching *search_term*."""
        subprocess.run(
            ["pkill", "-f", search_term],
            timeout=5,
            stderr=subprocess.DEVNULL,
            check=False,
        )

    def docker_compose_down(self, compose_path: str, timeout: float = 10.0) -> None:
        """Run ``docker compose down`` on a given compose file."""
        subprocess.run(
            ["docker", "compose", "-f", compose_path, "down"],
            timeout=timeout,
            capture_output=True,
            check=False,
        )

    def reap(self) -> None:
        """Poll and drop finished children; call periodically from the GUI."""
        with self._lock:
            still_alive: list[subprocess.Popen] = []
            for proc in self._launched:
                if proc.poll() is None:
                    still_alive.append(proc)
            self._launched = still_alive

    def shutdown(self, timeout: float = 2.0) -> None:
        """Terminate every still-running child on application quit."""
        with self._lock:
            for proc in self._launched:
                if proc.poll() is None:
                    proc.terminate()
            for proc in self._launched:
                try:
                    proc.wait(timeout=timeout)
                except subprocess.TimeoutExpired:
                    proc.kill()
            self._launched.clear()

    def __del__(self) -> None:  # noqa: D401 - best-effort cleanup
        try:
            self.shutdown()
        except Exception:
            pass


def safe_env(base: dict[str, str] | None = None, **overrides: str) -> dict[str, str]:
    """Build a clean environment for child processes.

    Merges the current ``os.environ`` with any explicit *base* dict and
    keyword overrides.  Values are coerced to ``str``.
    """
    env = dict(os.environ)
    if base:
        env.update(base)
    env.update({k: str(v) for k, v in overrides.items()})
    return env


__all__ = [
    "ProcessManager",
    "detect_terminal",
    "safe_env",
    "_to_arg_list",
    "_TERMINALS",
]
