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

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

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

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


# ──────────────────────────────────────────────────────────────────────────────
# ServerState
# ──────────────────────────────────────────────────────────────────────────────

class ServerState:
    """
    Shared server-side key/value store that is automatically broadcast to
    **all** clients on every sync tick (under the ``"server"`` key of the
    ``__state_sync__`` event).

    Use it for round timers, game phase, global scores — any state that
    every client needs without being tied to a specific player.

    Example::

        server.state["phase"] = "lobby"
        server.state["round"] = 1

        @server.on_tick()
        def tick(dt):
            if server.state["phase"] == "playing":
                server.state["timer"] = max(0, server.state["timer"] - dt)
    """

    def __init__(self) -> None:
        self._data: Dict[str, Any] = {}
        self._prev: Dict[str, Any] = {}

    # dict-like interface
    def __setitem__(self, key: str, value: Any) -> None:
        self._data[key] = value

    def __getitem__(self, key: str) -> Any:
        return self._data[key]

    def __delitem__(self, key: str) -> None:
        del self._data[key]

    def __contains__(self, key: str) -> bool:
        return key in self._data

    def get(self, key: str, default: Any = None) -> Any:
        """Return the value for *key*, or *default* if absent."""
        return self._data.get(key, default)

    def update(self, data: Dict[str, Any]) -> None:
        """Merge *data* dict into the server state."""
        self._data.update(data)

    # internal change-tracking helpers (used by SyncEngine)
    def _changed(self) -> bool:
        return self._data != self._prev

    def _delta(self) -> Dict[str, Any]:
        changed = {k: v for k, v in self._data.items() if self._prev.get(k) != v}
        deleted = {k: None for k in self._prev if k not in self._data}
        return {**changed, **deleted}

    def _mark_synced(self) -> None:
        self._prev = dict(self._data)

    def __repr__(self) -> str:
        return f"ServerState({self._data!r})"


# ──────────────────────────────────────────────────────────────────────────────
# Server
# ──────────────────────────────────────────────────────────────────────────────

class Server:
    """
    A nodelink game server.

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

    **Quick-start 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.start()

    **Rooms**::

        lobby = server.create_room("lobby")

        @server.on("connect")
        def on_connect(player):
            lobby.add(player)

    **on_tick**::

        @server.on_tick()
        def tick(dt):
            # dt = seconds elapsed since last tick
            pass

    **Middleware**::

        @server.middleware("move")
        def validate_move(player, data):
            data["x"] = max(0, min(800, data["x"]))  # clamp x
            # return False to block the event entirely

    **Shared server state**::

        server.state["phase"] = "lobby"

        @server.on_tick()
        def tick(dt):
            server.state["timer"] -= dt
    """

    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

        # Shared server state — broadcast to every client each tick
        self.state: ServerState = ServerState()

        self._players:       Dict[str, Player]       = {}   # conn_id → Player
        self._players_by_id: Dict[str, Player]       = {}   # player.id → Player
        self._handlers:      Dict[str, List[Callable]] = {}
        self._error_handlers: List[Callable]          = []
        self._tick_handlers:  List[Callable]          = []
        self._middleware:    Dict[str, List[Callable]] = {}
        self._rooms:         Dict[str, Room]           = {}

        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,
            tick_callback=self._fire_tick,
            server_state=self.state,
        )

    # ──────────────────────────────────────────────────────────────────
    # 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 whenever any event handler raises.

        Handler signature: ``fn(error, player)``

        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

    def on_tick(self) -> Callable:
        """
        Register a handler called every server tick.

        The handler receives ``dt`` — the number of **seconds** elapsed since
        the previous tick — as its only argument.  Use it for game logic that
        must run at the server's tick rate (physics, timers, AI…).

        Example::

            @server.on_tick()
            def tick(dt):
                for player in server.get_players():
                    vx = player.get_state("vx", 0)
                    x  = player.get_state("x",  0)
                    player.set_state("x", x + vx * dt)
        """
        def decorator(fn: Callable) -> Callable:
            self._tick_handlers.append(fn)
            return fn
        return decorator

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

        Middleware runs **before** the event handler.  If the middleware
        returns ``False`` the event is blocked and the handler is never called.
        Middleware may also mutate *data* in-place to sanitise / validate it.

        Handler signature: ``fn(player, data)`` — same as a normal handler.

        Example::

            @server.middleware("move")
            def clamp_move(player, data):
                # Clamp positions server-side so clients can't cheat
                data["x"] = max(0, min(800, int(data.get("x", 0))))
                data["y"] = max(0, min(600, int(data.get("y", 0))))

            @server.middleware("chat")
            def mute_check(player, data):
                if player.get_state("muted"):
                    return False   # block the event entirely
        """
        def decorator(fn: Callable) -> Callable:
            self._middleware.setdefault(event, []).append(fn)
            return fn
        return decorator

    # ──────────────────────────────────────────────────────────────────
    # Room management
    # ──────────────────────────────────────────────────────────────────

    def create_room(self, name: str) -> Room:
        """
        Create (or retrieve if already existing) a room with *name*.

        Example::

            lobby = server.create_room("lobby")
            vip   = server.create_room("vip")
        """
        if name not in self._rooms:
            self._rooms[name] = Room(name)
        return self._rooms[name]

    def get_room(self, name: str) -> Optional[Room]:
        """Return the :class:`Room` named *name*, or ``None`` if it doesn't exist."""
        return self._rooms.get(name)

    def remove_room(self, name: str) -> None:
        """Delete the room named *name* (no-op if it doesn't exist)."""
        self._rooms.pop(name, None)

    @property
    def rooms(self) -> Dict[str, Room]:
        """A snapshot dict of all currently active rooms."""
        return dict(self._rooms)

    # ──────────────────────────────────────────────────────────────────
    # 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._players_by_id[player.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._players_by_id.pop(player.id, None)
            self._sync.unregister(player.id)
            # Auto-remove from every room
            for room in self._rooms.values():
                room._remove_by_id(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") or {}
        if not event:
            return
        player = self._players.get(conn_id)
        if not player:
            return
        player.touch()

        # Run middleware — abort if any returns False
        allowed = await self._run_middleware(event, player, data)
        if not allowed:
            return

        await self._fire(event, player, data)

    # ──────────────────────────────────────────────────────────────────
    # Internal event / middleware firing
    # ──────────────────────────────────────────────────────────────────

    async def _run_middleware(
        self, event: str, player: Player, data: Any
    ) -> bool:
        """Run all middleware for *event*.  Returns False if blocked."""
        for handler in self._middleware.get(event, []):
            try:
                result = handler(player, data)
                if asyncio.iscoroutine(result):
                    result = await result
                if result is False:
                    return False
            except Exception as e:
                logger.error(
                    f"Error in middleware for '{event}': {e}", exc_info=True
                )
                await self._fire_error(e, player)
        return True

    async def _fire_tick(self, dt: float) -> None:
        """Called by SyncEngine every tick with dt in seconds."""
        for handler in self._tick_handlers:
            try:
                result = handler(dt)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as e:
                logger.error(f"Error in tick handler: {e}", exc_info=True)

    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 call this when you need
        something sent *right now* rather than on the next tick.

        Args:
            full: send complete state instead of just the delta.  Recommended
                  when a new player joins so they see everyone.
        """
        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."""
        return self._players_by_id.get(player_id)

    @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 nodelink 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"nodelink ready ✓  ws://{self.host}:{self.port}")
        try:
            await asyncio.Future()
        finally:
            self._sync.stop()
            await self._transport.stop()
