"""
"nodelink.sync
~~~~~~~~~~~~~~
State synchronization engine.
Handles delta sync, full snapshots, and tick-rate based auto-broadcasting.
"""

import asyncio
import logging
import time
from typing import TYPE_CHECKING, Callable, Dict, Optional

if TYPE_CHECKING:
    from .player import Player

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


class SyncEngine:
    """
    Manages state synchronization across all connected players.

    Runs an internal tick loop that broadcasts only changed state
    at a configurable rate (default 20 ticks/sec).
    """

    def __init__(
        self,
        broadcast_fn: Callable,
        tick_rate: int = 20,
        delta_only: bool = True,
    ):
        self._broadcast = broadcast_fn
        self.tick_rate = tick_rate
        self.delta_only = delta_only
        self._players: Dict[str, "Player"] = {}
        self._dirty: set = set()
        self._running = False
        self._task: Optional[asyncio.Task] = None
        self._sync_count: int = 0
        self._last_sync: float = 0.0

    def register(self, player: "Player") -> None:
        self._players[player.id] = player
        self._dirty.add(player.id)

    def unregister(self, player_id: str) -> None:
        self._players.pop(player_id, None)
        self._dirty.discard(player_id)

    def mark_dirty(self, player: "Player") -> None:
        """Flag a player's state for inclusion in the next sync."""
        self._dirty.add(player.id)

    def mark_all_dirty(self) -> None:
        self._dirty.update(self._players.keys())

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._task = asyncio.ensure_future(self._sync_loop())
        logger.debug(f"Sync engine started at {self.tick_rate} ticks/sec")

    def stop(self) -> None:
        self._running = False
        if self._task:
            self._task.cancel()
            self._task = None

    async def _sync_loop(self) -> None:
        interval = 1.0 / self.tick_rate
        while self._running:
            start = time.monotonic()
            try:
                await self._do_sync()
            except Exception as e:
                logger.error(f"Sync error: {e}", exc_info=True)
            elapsed = time.monotonic() - start
            await asyncio.sleep(max(0.0, interval - elapsed))

    async def _do_sync(self) -> None:
        if not self._players:
            return

        if self.delta_only:
            changed = {
                pid: p for pid, p in self._players.items()
                if pid in self._dirty and p.state_changed()
            }
        else:
            changed = dict(self._players)

        if not changed:
            return

        payload = {
            "t": time.time(),
            "players": {
                pid: (p.delta() if self.delta_only else p.state)
                for pid, p in changed.items()
            }
        }

        await self._broadcast("__state_sync__", payload)

        for p in changed.values():
            p.mark_synced()
        self._dirty -= set(changed.keys())

        self._last_sync = time.time()
        self._sync_count += 1

    async def sync_now(self, full: bool = False) -> None:
        """Force an immediate sync outside the normal tick."""
        self.mark_all_dirty()
        if full:
            prev = self.delta_only
            self.delta_only = False
            await self._do_sync()
            self.delta_only = prev
        else:
            await self._do_sync()

    @property
    def stats(self) -> dict:
        return {
            "tick_rate": self.tick_rate,
            "delta_only": self.delta_only,
            "players_tracked": len(self._players),
            "total_syncs": self._sync_count,
            "last_sync": self._last_sync,
        }
