"""Systemd helper — safe, async wrapper around systemctl.

All subprocess calls use ``asyncio.create_subprocess_exec`` (never ``shell=True``).
Unit names are validated against a strict regex rejecting shell metacharacters.
Commands are restricted to an explicit allowlist.
"""

from __future__ import annotations

import asyncio
import os
import re
from dataclasses import dataclass


def _get_systemd_env() -> dict[str, str]:
    """Return environment dict with D-Bus session variables set.

    When running in environments without D-Bus (cron, MCP servers, etc.),
    we need to explicitly set XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS.
    """
    env = os.environ.copy()

    # Set XDG_RUNTIME_DIR if not present
    if "XDG_RUNTIME_DIR" not in env:
        uid = os.getuid()
        runtime_dir = f"/run/user/{uid}"
        if os.path.isdir(runtime_dir):
            env["XDG_RUNTIME_DIR"] = runtime_dir

    # Set D-Bus address if not present
    if "DBUS_SESSION_BUS_ADDRESS" not in env:
        runtime_dir = env.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
        bus_path = f"{runtime_dir}/bus"
        if os.path.exists(bus_path):
            env["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path={bus_path}"

    return env


ALLOWED_COMMANDS: frozenset[str] = frozenset(
    {
        "start",
        "stop",
        "restart",
        "is-active",
        "status",
        "enable",
        "disable",
        "show",
    }
)

UNIT_NAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z0-9_@:.\-]+$")


@dataclass
class SystemctlResult:
    """Result of a ``systemctl`` invocation."""

    return_code: int
    stdout: str
    stderr: str

    @property
    def success(self) -> bool:
        """Return True when the command exited with code 0."""
        return self.return_code == 0


class SystemdError(Exception):
    """Raised when a ``systemctl`` command fails."""

    def __init__(self, command: str, result: SystemctlResult) -> None:
        self.command = command
        self.result = result
        super().__init__(f"systemctl {command} failed (rc={result.return_code}): {result.stderr}")


# ------------------------------------------------------------------
# Validation
# ------------------------------------------------------------------


def _validate_unit_name(unit: str) -> None:
    """Raise ``ValueError`` if *unit* contains shell metacharacters or is empty."""
    if not unit or not UNIT_NAME_PATTERN.match(unit):
        msg = f"Invalid unit name: {unit!r}"
        raise ValueError(msg)


def _validate_command(command: str) -> None:
    """Raise ``ValueError`` if *command* is not in the allowlist."""
    if command not in ALLOWED_COMMANDS:
        msg = f"Command {command!r} not allowed. Must be one of {sorted(ALLOWED_COMMANDS)}"
        raise ValueError(msg)


# ------------------------------------------------------------------
# Core async functions
# ------------------------------------------------------------------


async def systemctl(
    command: str,
    unit: str,
    *,
    timeout_s: float = 30.0,
) -> SystemctlResult:
    """Run ``systemctl --user <command> <unit>`` and return the result.

    All commands use ``systemctl --user`` to manage user-level systemd
    units.  No ``sudo`` is required — units live under
    ``~/.config/systemd/user/`` and the user session must have lingering
    enabled (``loginctl enable-linger $USER``).

    Raises
    ------
    SystemdError
        If the command exits with a non-zero return code.
    ValueError
        If *command* or *unit* fails validation.
    """
    _validate_command(command)
    _validate_unit_name(unit)

    argv: list[str] = ["systemctl", "--user", command, unit]

    proc = await asyncio.create_subprocess_exec(
        *argv,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        env=_get_systemd_env(),
    )

    stdout_bytes, stderr_bytes = await asyncio.wait_for(
        proc.communicate(),
        timeout=timeout_s,
    )

    result = SystemctlResult(
        return_code=proc.returncode if proc.returncode is not None else -1,
        stdout=stdout_bytes.decode() if stdout_bytes else "",
        stderr=stderr_bytes.decode() if stderr_bytes else "",
    )

    if not result.success:
        raise SystemdError(command, result)

    return result


async def is_active(unit: str) -> bool:
    """Return ``True`` if *unit* is ``active``. Never raises."""
    try:
        _validate_unit_name(unit)
        proc = await asyncio.create_subprocess_exec(
            "systemctl",
            "--user",
            "is-active",
            unit,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            env=_get_systemd_env(),
        )
        stdout_bytes, _ = await proc.communicate()
        return stdout_bytes.decode().strip() == "active"
    except Exception:
        return False


async def get_unit_state(unit: str) -> str:
    """Return the unit state string (e.g. ``active``, ``inactive``, ``failed``).

    Returns ``"unknown"`` on any error.
    """
    try:
        _validate_unit_name(unit)
        proc = await asyncio.create_subprocess_exec(
            "systemctl",
            "--user",
            "is-active",
            unit,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            env=_get_systemd_env(),
        )
        stdout_bytes, _ = await proc.communicate()
        return stdout_bytes.decode().strip()
    except Exception:
        return "unknown"


async def journal_logs(unit: str, lines: int = 20) -> list[str]:
    """Return recent journal entries for *unit*.  Never raises.

    Parameters
    ----------
    unit:
        Systemd unit name (validated against :data:`UNIT_NAME_PATTERN`).
    lines:
        Number of lines to retrieve (clamped to 1-200).

    Returns
    -------
    list[str]
        Journal lines, or ``[]`` on any error.
    """
    try:
        _validate_unit_name(unit)
    except ValueError:
        return []

    lines = max(1, min(lines, 200))

    try:
        proc = await asyncio.create_subprocess_exec(
            "journalctl",
            "--user",
            "-u",
            unit,
            "-n",
            str(lines),
            "--no-pager",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            env=_get_systemd_env(),
        )
        stdout_bytes, _ = await proc.communicate()
        if proc.returncode != 0:
            return []
        text = stdout_bytes.decode() if stdout_bytes else ""
        return [line for line in text.splitlines() if line]
    except Exception:
        return []


async def start(unit: str, *, timeout_s: float = 30.0) -> None:
    """Start a systemd unit."""
    await systemctl("start", unit, timeout_s=timeout_s)


async def stop(unit: str, *, timeout_s: float = 30.0) -> None:
    """Stop a systemd unit."""
    await systemctl("stop", unit, timeout_s=timeout_s)


async def restart(unit: str, *, timeout_s: float = 30.0) -> None:
    """Restart a systemd unit."""
    await systemctl("restart", unit, timeout_s=timeout_s)
