"""AsyncEventBus - in-process async event bus with routing and structured logging."""
from __future__ import annotations

import asyncio
import logging
import time
from collections import defaultdict
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any
from uuid import uuid4

logger = logging.getLogger(__name__)

EventHandler = Callable[["Event"], Awaitable[Any]]


@dataclass
class Event:
    """Structured event dispatched through the bus."""

    name: str
    payload: dict[str, Any] = field(default_factory=dict)
    id: str = field(default_factory=lambda: uuid4().hex)
    timestamp: float = field(default_factory=time.time)

    @property
    def trace(self) -> dict[str, Any]:
        return {"event_id": self.id, "name": self.name, "ts": self.timestamp}


class AsyncEventBus:
    """Async publish/subscribe event bus supporting routing and fan-out.

    Subscribers register async callables per event name. Publishing fans out the
    event to every subscriber; handler exceptions are logged and isolated so a
    single failing handler cannot abort delivery to the others.
    """

    def __init__(self) -> None:
        self._subscribers: dict[str, list[EventHandler]] = defaultdict(list)
        self._events: list[Event] = []

    @property
    def event_names(self) -> list[str]:
        """Names of all events that currently have subscribers."""
        return sorted(self._subscribers)

    def subscribers(self, event_name: str) -> int:
        return len(self._subscribers.get(event_name, []))

    def subscribe(self, event_name: str, handler: EventHandler) -> EventHandler:
        """Register `handler` for `event_name` and return it for chaining."""
        subs = self._subscribers[event_name]
        if handler not in subs:
            subs.append(handler)
        logger.debug(
            "subscribed handler %r to %r",
            getattr(handler, "__name__", handler),
            event_name,
            extra={"event_name": event_name},
        )
        return handler

    def unsubscribe(self, event_name: str, handler: EventHandler) -> bool:
        """Remove a previously subscribed handler. Returns whether it existed."""
        subs = self._subscribers.get(event_name)
        if not subs:
            return False
        try:
            subs.remove(handler)
            existed = True
        except ValueError:
            existed = False
        if existed:
            logger.debug(
                "unsubscribed handler from %r", event_name, extra={"event_name": event_name}
            )
        return existed

    def routing(self, event_name: str) -> list[EventHandler]:
        """Return the ordered list of handlers that will receive an event."""
        return list(self._subscribers.get(event_name, []))

    async def publish(self, event_name: str, payload: dict[str, Any] | None = None) -> list[Any]:
        """Publish an event to all subscribers, returning the list of handler results."""
        event = Event(name=event_name, payload=payload or {})
        self._events.append(event)
        handlers = self.routing(event_name)
        logger.info(
            "publishing %r [%s] to %d subscribers",
            event_name,
            event.id,
            len(handlers),
            extra=event.trace,
        )
        results: list[Any] = []
        for handler in handlers:
            try:
                results.append(await handler(event))
            except Exception as exc:
                logger.exception(
                    "handler %r failed for event %r",
                    getattr(handler, "__name__", handler),
                    event_name,
                    extra={
                        "event_name": event_name,
                        "event_id": event.id,
                        "handler": getattr(handler, "__name__", repr(handler)),
                        "error": str(exc),
                    },
                )
        return results

    async def wait_for(self, event_name: str, timeout: float = 5.0) -> Event:
        """Await the next event of `event_name` (primarily for tests/observation)."""
        loop = asyncio.get_running_loop()
        future: asyncio.Future[Event] = loop.create_future()

        async def _resolver(event: Event) -> None:
            if not future.done():
                future.set_result(event)

        self.subscribe(event_name, _resolver)
        try:
            return await asyncio.wait_for(future, timeout=timeout)
        finally:
            self.unsubscribe(event_name, _resolver)


__all__ = ["AsyncEventBus", "Event", "EventHandler"]
