"""Marketplace backend refresh loop — periodically sync remote nodes with the dispatcher.

Runs as a background ``asyncio.Task``, calling :func:`load_and_register` at a
configurable interval (default 5 min) to keep the dispatcher's remote backend
list in sync with the marketplace catalog.

Usage::

    refresher = MarketplaceRefresher()
    refresher.start(dispatcher, catalog_url="https://llmhosts.com/api/marketplace/nodes")
    ...
    await refresher.stop()
"""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING

from llmhosts.marketplace.router import load_and_register

if TYPE_CHECKING:
    from llmhosts.proxy.dispatcher import BackendDispatcher

logger = logging.getLogger(__name__)

_DEFAULT_INTERVAL = 300  # 5 minutes


class MarketplaceRefresher:
    """Periodically fetches marketplace nodes and updates the dispatcher.

    Tracks the set of registered node names between iterations so it can
    log additions and removals.
    """

    def __init__(self) -> None:
        self._task: asyncio.Task[None] | None = None
        self._stop_event: asyncio.Event = asyncio.Event()
        self._known_names: set[str] = set()
        self._dispatcher: BackendDispatcher | None = None
        self._catalog_url: str = ""
        self._interval: float = _DEFAULT_INTERVAL

    @property
    def running(self) -> bool:
        """True if the refresh loop is currently active."""
        return self._task is not None and not self._task.done()

    def start(
        self,
        dispatcher: BackendDispatcher,
        catalog_url: str,
        interval: float = _DEFAULT_INTERVAL,
    ) -> None:
        """Start the background refresh loop.

        Parameters
        ----------
        dispatcher:
            The :class:`BackendDispatcher` whose remote backends will be updated.
        catalog_url:
            Full URL to the marketplace catalog endpoint (GET JSON).
        interval:
            Seconds between refresh cycles (default 300 = 5 min).
        """
        if self.running:
            logger.warning("MarketplaceRefresher already running, ignoring start()")
            return
        if not (catalog_url or "").strip():
            logger.warning("MarketplaceRefresher: empty catalog_url, not starting")
            return
        self._dispatcher = dispatcher
        self._catalog_url = catalog_url
        self._interval = max(interval, 10.0)  # floor at 10s to avoid hammering
        self._stop_event.clear()
        self._task = asyncio.create_task(self._loop(), name="marketplace-refresh")
        logger.info(
            "MarketplaceRefresher started (catalog=%s, interval=%.0fs)",
            catalog_url,
            self._interval,
        )

    async def stop(self) -> None:
        """Signal the refresh loop to stop and wait for it to finish."""
        if self._task is None:
            return
        self._stop_event.set()
        try:
            await asyncio.wait_for(self._task, timeout=10.0)
        except (asyncio.TimeoutError, asyncio.CancelledError):
            self._task.cancel()
        self._task = None
        logger.info("MarketplaceRefresher stopped")

    async def _loop(self) -> None:
        """Run refresh iterations until the stop event is set."""
        assert self._dispatcher is not None
        while not self._stop_event.is_set():
            try:
                await self._refresh()
            except Exception:
                logger.warning("MarketplaceRefresher iteration failed", exc_info=True)
            # Wait for the interval or until stop is signalled
            try:
                await asyncio.wait_for(self._stop_event.wait(), timeout=self._interval)
                break  # stop event was set
            except asyncio.TimeoutError:
                pass  # interval elapsed, loop again

    async def _refresh(self) -> None:
        """Single refresh: fetch nodes, register, log changes."""
        assert self._dispatcher is not None
        # Snapshot remote backend names before refresh
        before_names = {b.name for b in self._dispatcher._backends if b.backend_type == "remote"}

        count = await load_and_register(self._dispatcher, self._catalog_url)

        # Snapshot after
        after_names = {b.name for b in self._dispatcher._backends if b.backend_type == "remote"}

        added = after_names - before_names
        removed = before_names - after_names

        if added:
            logger.info("Marketplace refresh: +%d node(s): %s", len(added), ", ".join(sorted(added)))
        if removed:
            logger.info("Marketplace refresh: -%d node(s): %s", len(removed), ", ".join(sorted(removed)))

        self._known_names = after_names
        logger.debug("Marketplace refresh complete: %d remote backend(s)", count)
