"""Dev server engine — orchestrates server subprocess, file watcher, and restart loop."""

from __future__ import annotations

import asyncio
import platform
import shlex
import signal
from pathlib import Path

from hatchdx.devserver import logger as dev_logger
from hatchdx.devserver.watcher import FileWatcher
from hatchdx.harness.simulator import SimulatorError, StdioSimulator
from hatchdx.validator.runner import run_validation_with_simulator


class DevServerEngine:
    """Start an MCP server with hot-reload on file changes.

    Usage::

        engine = DevServerEngine(
            command="python -m my_server",
            watch_paths=["src/", "tests/"],
        )
        asyncio.run(engine.run())
    """

    def __init__(
        self,
        command: str,
        watch_paths: list[str | Path] | None = None,
        debounce_seconds: float = 0.3,
        repl: bool = True,
        server_name: str = "",
    ) -> None:
        self._raw_command = command
        self._command = self._resolve_command(command)
        self._watch_paths = watch_paths or ["src/", "tests/"]
        self._debounce_seconds = debounce_seconds
        self._repl_enabled = repl
        self._server_name = server_name
        self._simulator: StdioSimulator | None = None
        self._watcher: FileWatcher | None = None
        self._restart_event = asyncio.Event()
        self._shutdown_event = asyncio.Event()
        self._loop: asyncio.AbstractEventLoop | None = None
        self._tools: list[str] = []

    @property
    def tools(self) -> list[str]:
        """Names of tools discovered on the running server."""
        return list(self._tools)

    @property
    def simulator(self) -> StdioSimulator | None:
        """The active simulator instance, or None if the server is not running."""
        return self._simulator

    async def run(self) -> None:
        """Main loop: start server, watch files, restart on change. Blocks until shutdown."""
        self._loop = asyncio.get_running_loop()
        self._install_signal_handlers()

        # Start the file watcher
        self._watcher = FileWatcher(
            watch_paths=self._watch_paths,
            on_change=self._on_file_change,
            debounce_seconds=self._debounce_seconds,
        )
        self._watcher.start()

        # Optionally start the interactive REPL
        repl_task: asyncio.Task | None = None
        if self._repl_enabled:
            from hatchdx.devserver.repl import DevRepl

            self._repl = DevRepl(self)
            repl_task = asyncio.create_task(self._run_repl())

        try:
            while not self._shutdown_event.is_set():
                await self._start_server()

                # Refresh REPL completions after server starts
                if self._repl_enabled and hasattr(self, "_repl"):
                    self._repl.refresh_tools()

                # Wait for either a restart or shutdown signal
                self._restart_event.clear()
                restart_task = asyncio.create_task(self._restart_event.wait())
                shutdown_task = asyncio.create_task(self._shutdown_event.wait())

                done, pending = await asyncio.wait(
                    {restart_task, shutdown_task},
                    return_when=asyncio.FIRST_COMPLETED,
                )
                for task in pending:
                    task.cancel()

                await self._stop_server()

                if self._shutdown_event.is_set():
                    break
        finally:
            if repl_task is not None:
                repl_task.cancel()
            if self._watcher is not None:
                self._watcher.stop()

    async def _run_repl(self) -> None:
        """Run the REPL and trigger shutdown when it exits."""
        try:
            await self._repl.run()
        except asyncio.CancelledError:
            return
        # REPL exited normally (user typed quit/exit or Ctrl+D) — shut down
        self._shutdown_event.set()

    async def _start_server(self) -> None:
        """Spawn the server subprocess and perform MCP initialization."""
        dev_logger.log_server_start(self._raw_command)

        self._simulator = StdioSimulator(self._command, timeout=10.0)
        try:
            await self._simulator.start()
        except SimulatorError as exc:
            dev_logger.log_server_error(f"Failed to start: {exc}")
            dev_logger.log_server_error("Waiting for file changes to retry...")
            self._simulator = None
            return

        # Discover tools
        try:
            tools = await self._simulator.list_tools()
            self._tools = [t.name for t in tools]
        except SimulatorError as exc:
            dev_logger.log_server_error(f"Failed to list tools: {exc}")
            self._tools = []

        dev_logger.log_server_ready(self._tools)

        # Run non-blocking validation after server is ready
        if self._simulator is not None and self._tools:
            asyncio.create_task(self._run_validation_check())

    async def _run_validation_check(self) -> None:
        """Run protocol validation and log warnings. Non-blocking."""
        if self._simulator is None:
            return

        try:
            report = await run_validation_with_simulator(
                self._simulator,
                server_name=self._server_name or "server",
            )
        except Exception:
            # Validation failure should never block the dev server
            return

        if report.has_errors:
            dev_logger.log_server_error(
                f"Validation: {report.error_count} error(s), "
                f"{report.warning_count} warning(s). Run 'hdx server validate' for details."
            )
        elif report.warning_count > 0:
            from hatchdx.devserver import logger as _logger
            _logger._log(
                "VALIDATE",
                f"{report.warning_count} warning(s). Run 'hdx server validate --verbose' for details.",
                style="bold yellow",
            )

    async def _stop_server(self) -> None:
        """Shut down the running server subprocess."""
        if self._simulator is not None:
            dev_logger.log_server_stop()
            await self._simulator.shutdown()
            self._simulator = None

    def _on_file_change(self, path: str) -> None:
        """Callback invoked by the file watcher (from a watchdog thread)."""
        dev_logger.log_server_restart(path)
        # Signal the async run loop from the watchdog thread
        if self._loop is not None:
            self._loop.call_soon_threadsafe(self._restart_event.set)

    def _install_signal_handlers(self) -> None:
        """Install SIGINT/SIGTERM handlers for graceful shutdown."""
        loop = asyncio.get_running_loop()

        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.add_signal_handler(sig, self._handle_signal)

    def _handle_signal(self) -> None:
        """Signal handler that triggers graceful shutdown."""
        self._shutdown_event.set()

    @staticmethod
    def _resolve_command(raw_command: str) -> list[str]:
        """Parse the command string and resolve python to a local venv if present."""
        parts = shlex.split(raw_command)
        if not parts:
            return parts

        if parts[0] in ("python", "python3"):
            if platform.system() == "Windows":
                venv_python = Path(".venv/Scripts/python.exe")
            else:
                venv_python = Path(".venv/bin/python")

            if venv_python.is_file():
                parts[0] = str(venv_python.absolute())

        return parts
