"""
"nodelink.server
~~~~~~~~~~~~~~~~
The multilink Server.
"""

import asyncio
import logging
import uuid
from typing import Any, Callable, Dict, List, Optional, Set

from .player import Player
from .sync import SyncEngine
from .transport.websocket import WebSocketTransport

logger = logging.getLogger("nodelink.server")


class Server:
    """
    A multilink game server.

    Args:
        host:       hostname to bind to (default "0.0.0.0")
        port:       port to listen on (default 5000)
        tick_rate:  state syncs per second (default 20)
        delta_only: only send changed fields per sync (default True)

    Example::

        from nodelink import Server

        server = Server(port=5000)

        @server.on("connect")
        def on_connect(player):
            server.broadcast("joined", {"id": player.id})

        @server.on("move")
        def on_move(player, data):
            player.update_state(data)

        @server.on("disconnect")
        def on_disconnect(player):
            server.broadcast("left", {"id": player.id})

        @server.on_error()
        def on_error(error, player):
            print(f"Error from {player.id}: {error}")

        server.start()
    """

    def __init__(
        self,
        host: str = "0.0.0.0",
        port: int = 5000,
        tick_rate: int = 20,
        delta_only: bool = True,
    ):
        self.host = host
        self.port = port
        self._players: Dict[str, Player] = {}
        self._handlers: Dict[str, List[Callable]] = {}
        self._error_handlers: List[Callable] = []

        self._transport = WebSocketTransport(host, port)
        self._transport.on_connect(self._handle_connect)
        self._transport.on_disconnect(self._handle_disconnect)
        self._transport.on_message(self._handle_message)

        self._sync = SyncEngine(
            broadcast_fn=self._broadcast_raw,
            tick_rate=tick_rate,
            delta_only=delta_only,
        )

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

    def on(self, event: str) -> Callable:
        """
        Register a handler for an event.

        Built-in events: ``"connect"``, ``"disconnect"``

        Handler signatures:

        - connect / disconnect: ``fn(player)``
        - all others: ``fn(player, data)``

        Example::

            @server.on("shoot")
            def on_shoot(player, data):
                server.broadcast("hit", {"by": player.id, "pos": data["pos"]})
        """
        def decorator(fn: Callable) -> Callable:
            self._handlers.setdefault(event, []).append(fn)
            return fn
        return decorator

    def on_error(self) -> Callable:
        """
        Register an error handler. Fires when any event handler raises.

        Example::

            @server.on_error()
            def on_error(error, player):
                print(f"Error from {player.id}: {error}")
        """
        def decorator(fn: Callable) -> Callable:
            self._error_handlers.append(fn)
            return fn
        return decorator

    # ------------------------------------------------------------------
    # Transport callbacks (internal)
    # ------------------------------------------------------------------

    async def _handle_connect(self, conn_id: str, address) -> None:
        player = Player(str(uuid.uuid4())[:8])

        async def _send(event: str, payload: Any) -> None:
            await self._transport.send(conn_id, event, payload)

        player._send_callback = _send
        self._players[conn_id] = player
        self._sync.register(player)
        logger.info(f"Player {player.id} connected from {address}")
        await self._fire_single("connect", player)

    async def _handle_disconnect(self, conn_id: str) -> None:
        player = self._players.pop(conn_id, None)
        if player:
            self._sync.unregister(player.id)
            logger.info(f"Player {player.id} disconnected")
            await self._fire_single("disconnect", player)

    async def _handle_message(self, conn_id: str, message: dict) -> None:
        event = message.get("event")
        data = message.get("data", {})
        if not event:
            return
        player = self._players.get(conn_id)
        if not player:
            return
        player.touch()
        await self._fire(event, player, data)

    # ------------------------------------------------------------------
    # Event firing (internal)
    # ------------------------------------------------------------------

    async def _fire_single(self, event: str, player: Player) -> None:
        for handler in self._handlers.get(event, []):
            try:
                result = handler(player)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as e:
                logger.error(f"Error in '{event}' handler: {e}", exc_info=True)
                await self._fire_error(e, player)

    async def _fire(self, event: str, player: Player, data: Any) -> None:
        for handler in self._handlers.get(event, []):
            try:
                result = handler(player, data)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as e:
                logger.error(f"Error in '{event}' handler: {e}", exc_info=True)
                await self._fire_error(e, player)

    async def _fire_error(self, error: Exception, player: Optional[Player]) -> None:
        if self._error_handlers:
            for handler in self._error_handlers:
                try:
                    result = handler(error, player)
                    if asyncio.iscoroutine(result):
                        await result
                except Exception as e:
                    logger.error(f"Error in error handler: {e}", exc_info=True)
        else:
            logger.error(f"Unhandled error (player={player}): {error}")

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

    async def _broadcast_raw(self, event: str, data: Any, exclude: Optional[Set[str]] = None) -> None:
        await self._transport.broadcast(event, data, exclude=exclude)

    def broadcast(self, event: str, data: Any = None) -> None:
        """Send an event to every connected player."""
        asyncio.ensure_future(self._broadcast_raw(event, data))

    def sync_state(self, full: bool = False) -> None:
        """
        Trigger an immediate state sync to all players.

        The auto tick handles this normally — only use this when you
        need something sent right now rather than on the next tick.

        Args:
            full: send complete state instead of just the delta
        """
        asyncio.ensure_future(self._sync.sync_now(full=full))

    def get_players(self) -> List[Player]:
        """Return all currently connected :class:`Player` objects."""
        return list(self._players.values())

    def get_player(self, player_id: str) -> Optional[Player]:
        """Look up a player by their ID. Returns None if not found."""
        for p in self._players.values():
            if p.id == player_id:
                return p
        return None

    @property
    def player_count(self) -> int:
        """Number of currently connected players."""
        return len(self._players)

    @property
    def sync_stats(self) -> dict:
        """Stats from the sync engine."""
        return self._sync.stats

    def start(self) -> None:
        """Start the server. Blocks until stopped (Ctrl+C)."""
        logger.info(f"Starting multilink server on {self.host}:{self.port}")
        try:
            asyncio.run(self._run())
        except KeyboardInterrupt:
            logger.info("Server stopped.")

    async def _run(self) -> None:
        await self._transport.start()
        self._sync.start()
        print(f"multilink ready ✓  ws://{self.host}:{self.port}")
        try:
            await asyncio.Future()
        finally:
            self._sync.stop()
            await self._transport.stop()
