"""In-memory typed event bus for instance lifecycle and health events."""

import asyncio
import inspect
import logging
import uuid
from collections.abc import Awaitable, Callable
from typing import Any

logger = logging.getLogger(__name__)

InstanceEventPayload = dict[str, Any]
EventCallback = Callable[[InstanceEventPayload], Awaitable[None] | None]  # noqa: UP044


class InstanceEventBus:
    """Singleton in-memory event bus with typed pub/sub and exception isolation."""

    _instance: "InstanceEventBus | None" = None
    _lock: asyncio.Lock = asyncio.Lock()

    def __init__(self) -> None:
        self._subscribers: dict[str, list[tuple[str, EventCallback]]] = {}

    def __new__(cls) -> "InstanceEventBus":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._subscribers = {}
        return cls._instance

    def _reset_for_testing(self) -> None:
        """Clear all subscribers. For test use only."""
        self._subscribers.clear()

    def subscribe(
        self,
        event_type: str,
        callback: EventCallback,
    ) -> Callable[[], None]:
        """Register a callback for an event type.

        Args:
            event_type: The event type to subscribe to.
            callback: A sync or async callable that receives the payload.

        Returns:
            An unsubscribe function.
        """
        if event_type not in self._subscribers:
            self._subscribers[event_type] = []
        callback_id = str(uuid.uuid4())
        self._subscribers[event_type].append((callback_id, callback))

        def unsubscribe() -> None:
            self.unsubscribe(event_type, callback_id)

        return unsubscribe

    def unsubscribe(self, event_type: str, callback_id: str) -> None:
        """Remove a specific callback by ID."""
        if event_type in self._subscribers:
            self._subscribers[event_type] = [
                (cid, cb)
                for cid, cb in self._subscribers[event_type]
                if cid != callback_id
            ]
            if not self._subscribers[event_type]:
                del self._subscribers[event_type]

    def unsubscribe_all(self, event_type: str) -> None:
        """Remove all subscribers for an event type."""
        self._subscribers.pop(event_type, None)

    async def publish(self, event_type: str, payload: InstanceEventPayload) -> None:
        """Deliver payload to all subscribers of event_type.

        Also delivers to subscribers registered under the wildcard "*".
        Exceptions from individual subscribers are caught and logged;
        delivery continues to remaining subscribers.
        """
        callbacks: list[tuple[str, EventCallback]] = []
        callbacks.extend(self._subscribers.get(event_type, []))
        callbacks.extend(self._subscribers.get("*", []))

        for _callback_id, callback in callbacks:
            try:
                if inspect.iscoroutinefunction(callback):
                    await callback(payload)
                else:
                    callback(payload)
            except Exception:
                correlation_id = payload.get("correlation_id", "unknown")
                logger.exception(
                    "Event subscriber failed for %s",
                    event_type,
                    extra={"correlation_id": correlation_id},
                )
