"""ProcessManager — spawn, watch, and restart managed processes."""

from __future__ import annotations

import subprocess
import threading
import time
from collections import deque
from pathlib import Path

from konstant.config import ServiceConfig
from konstant.forwarder import create_forwarder
from konstant.logger import LogCapture
from konstant.state import StateManager
from konstant.utils import kill_process_tree, load_env_file, pid_alive


_BACKOFF_WINDOW = 60  # seconds


class ProcessManager:
    """Manages the lifecycle of a single service process."""

    def __init__(self, config: ServiceConfig) -> None:
        self.config = config
        self.state = StateManager(config.name)
        self._proc: subprocess.Popen[str] | None = None
        self._log_capture: LogCapture | None = None
        self._watcher_thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._restart_timestamps: deque[float] = deque()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def start(self) -> None:
        """Spawn the process and begin watching it."""
        current = self.state.read()
        if current.is_running() and pid_alive(current.pid):
            raise RuntimeError(f"Service '{self.config.name}' is already running (pid {current.pid})")

        self._stop_event.clear()
        self.state.reset_restart_count()
        self._restart_timestamps.clear()
        self._spawn()
        self._start_watcher()

    def stop(self) -> None:
        """Stop the managed process and its watcher."""
        self._stop_event.set()
        current = self.state.read()
        if current.pid and pid_alive(current.pid):
            kill_process_tree(current.pid)
        if self._proc is not None:
            try:
                self._proc.wait(timeout=12)
            except subprocess.TimeoutExpired:
                self._proc.kill()
            self._proc = None
        if self._log_capture:
            self._log_capture.close()
            self._log_capture = None
        self.state.mark_stopped(exit_code=None)

    def restart(self) -> None:
        self.stop()
        time.sleep(0.5)
        self.start()

    @property
    def log_path(self) -> Path:
        return self.config.resolved_cwd / "logs" / f"{self.config.name}.log"

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _spawn(self) -> None:
        cwd = self.config.resolved_cwd
        env = load_env_file(self.config.env_file, cwd)
        cmd = [self.config.interpreter, self.config.script, *self.config.args]

        forwarder = create_forwarder(self.config.logging.forward)
        self._log_capture = LogCapture(
            self.config.name,
            log_dir=cwd / "logs",
            logging_config=self.config.logging,
            forwarder=forwarder,
        )

        self._proc = subprocess.Popen(
            cmd,
            cwd=str(cwd),
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1,
        )

        self._log_capture.capture_stream(self._proc.stdout, label="stdout")
        self._log_capture.capture_stream(self._proc.stderr, label="stderr")
        self.state.mark_running(self._proc.pid)

    def _start_watcher(self) -> None:
        self._watcher_thread = threading.Thread(
            target=self._watch_loop,
            daemon=True,
            name=f"konstant-watcher-{self.config.name}",
        )
        self._watcher_thread.start()

    def _watch_loop(self) -> None:
        """Block until the process exits, then decide whether to restart."""
        while not self._stop_event.is_set():
            if self._proc is None:
                break
            try:
                exit_code = self._proc.wait(timeout=1)
            except subprocess.TimeoutExpired:
                continue

            if self._stop_event.is_set():
                break

            # Check if an external CLI invocation marked us stopped
            ext_state = self.state.read()
            if ext_state.status == "stopped":
                break

            if self._log_capture:
                self._log_capture.close()
                self._log_capture = None

            if not self.config.autorestart:
                self.state.mark_stopped(exit_code=exit_code)
                break

            if self._should_backoff():
                self.state.mark_errored(exit_code=exit_code)
                break

            self.state.record_restart()
            self.state.update(status="starting")
            self._stop_event.wait(self.config.restart_delay)
            if self._stop_event.is_set():
                break
            self._spawn()

    def _should_backoff(self) -> bool:
        """Return True if we've exceeded max_restarts within the backoff window."""
        now = time.monotonic()
        self._restart_timestamps.append(now)
        while self._restart_timestamps and (now - self._restart_timestamps[0]) > _BACKOFF_WINDOW:
            self._restart_timestamps.popleft()
        return len(self._restart_timestamps) > self.config.max_restarts
