"""
muxterm process supervision — start, monitor, restart on crash, stop.

Module state:
    _muxterm_process — the currently running muxterm subprocess (or None)
    _restart_task    — the background monitor/restart task (or None)

Public API:
    start_muxterm(secret, port, binary_path, auto_restart)
    stop_muxterm()

Internal:
    _find_muxterm_binary(binary_path)
    _monitor_and_restart(secret, port, binary_path)
"""

import asyncio
import logging
import shutil

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Module state
# ---------------------------------------------------------------------------

_muxterm_process: asyncio.subprocess.Process | None = None
_restart_task: asyncio.Task | None = None  # type: ignore[type-arg]

# ---------------------------------------------------------------------------
# Binary discovery
# ---------------------------------------------------------------------------


def _find_muxterm_binary(binary_path: str | None = None) -> str:
    """Locate the muxterm binary.

    Checks explicit *binary_path* first, then falls back to
    ``shutil.which('muxterm')``.  Raises :exc:`FileNotFoundError` if
    the binary cannot be found.
    """
    if binary_path is not None:
        return binary_path
    found = shutil.which("muxterm")
    if found is None:
        raise FileNotFoundError(
            "muxterm binary not found on PATH; pass binary_path explicitly"
        )
    return found


# ---------------------------------------------------------------------------
# Start
# ---------------------------------------------------------------------------


async def start_muxterm(
    secret: str,
    port: int = 7682,
    binary_path: str | None = None,
    auto_restart: bool = True,
) -> None:
    """Start the muxterm process.

    Finds the binary via :func:`_find_muxterm_binary`, spawns it with
    ``--addr 127.0.0.1:<port> --secret <secret>``, and optionally starts
    the background monitor/restart loop.
    """
    global _muxterm_process, _restart_task

    binary = _find_muxterm_binary(binary_path)

    _muxterm_process = await asyncio.create_subprocess_exec(
        binary,
        "--addr",
        f"127.0.0.1:{port}",
        "--secret",
        secret,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    logger.info("muxterm started (pid=%s, port=%s)", _muxterm_process.pid, port)

    if auto_restart:
        _restart_task = asyncio.create_task(
            _monitor_and_restart(secret, port, binary_path)
        )


# ---------------------------------------------------------------------------
# Monitor / restart
# ---------------------------------------------------------------------------


async def _monitor_and_restart(
    secret: str,
    port: int,
    binary_path: str | None,
) -> None:
    """Wait for process exit; restart on non-zero exit codes.

    Normal exit (rc=0) ends the loop.  On crash, waits 1 s before
    restarting, with 5 s backoff on repeated failures.
    """
    global _muxterm_process

    backoff = 1.0
    while True:
        if _muxterm_process is None:
            return

        rc = await _muxterm_process.wait()

        if rc == 0:
            logger.info("muxterm exited cleanly (rc=0)")
            return

        logger.warning("muxterm exited with rc=%s, restarting in %.0fs", rc, backoff)
        await asyncio.sleep(backoff)

        try:
            binary = _find_muxterm_binary(binary_path)
            _muxterm_process = await asyncio.create_subprocess_exec(
                binary,
                "--addr",
                f"127.0.0.1:{port}",
                "--secret",
                secret,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            logger.info(
                "muxterm restarted (pid=%s, port=%s)", _muxterm_process.pid, port
            )
            # Reset backoff on successful spawn, increase on repeated failures
            backoff = min(backoff + 1.0, 5.0)
        except Exception:
            logger.exception("failed to restart muxterm, retrying in 5s")
            backoff = 5.0
            await asyncio.sleep(backoff)


# ---------------------------------------------------------------------------
# Stop
# ---------------------------------------------------------------------------


async def stop_muxterm() -> None:
    """Stop the muxterm process and cancel the restart task.

    Terminates with a 5 s grace period, then kills if still alive.
    """
    global _muxterm_process, _restart_task

    if _restart_task is not None:
        _restart_task.cancel()
        _restart_task = None

    if _muxterm_process is not None:
        _muxterm_process.terminate()
        try:
            await asyncio.wait_for(_muxterm_process.wait(), timeout=5.0)
        except (asyncio.TimeoutError, ProcessLookupError):
            _muxterm_process.kill()
        _muxterm_process = None
