from __future__ import annotations

import inspect
import threading
from dataclasses import dataclass, field
from fnmatch import fnmatchcase
from itertools import count
from typing import Any, Callable


EventCallback = Callable[..., Any]


@dataclass(order=True, slots=True)
class _Listener:
    sort_key: tuple[int, int] = field(init=False, repr=False)
    priority: int
    order: int
    callback: EventCallback
    once: bool = False
    is_async: bool = field(init=False)

    def __post_init__(self) -> None:
        self.is_async = inspect.iscoroutinefunction(self.callback)
        self.sort_key = (-self.priority, self.order)


@dataclass(frozen=True, slots=True)
class EventResult:
    event: str
    listener_count: int
    results: tuple[Any, ...]


class EventBus:
    def __init__(self) -> None:
        self._listeners: dict[str, list[_Listener]] = {}
        self._counter = count()
        self._lock = threading.RLock()

    def on(self, pattern: str, *, priority: int = 0, once: bool = False):
        def decorator(func: EventCallback) -> EventCallback:
            self.add_listener(pattern, func, priority=priority, once=once)
            return func
        return decorator

    def once(self, pattern: str, *, priority: int = 0):
        return self.on(pattern, priority=priority, once=True)

    def add_listener(self, pattern: str, callback: EventCallback, *, priority: int = 0, once: bool = False) -> None:
        if not isinstance(pattern, str) or not pattern:
            raise ValueError("pattern must be a non-empty string")
        if not callable(callback):
            raise TypeError("callback must be callable")
        listener = _Listener(priority=priority, order=next(self._counter), callback=callback, once=once)
        with self._lock:
            bucket = self._listeners.setdefault(pattern, [])
            bucket.append(listener)
            bucket.sort()

    def remove_listener(self, pattern: str, callback: EventCallback) -> bool:
        with self._lock:
            bucket = self._listeners.get(pattern)
            if not bucket:
                return False
            before = len(bucket)
            bucket[:] = [listener for listener in bucket if listener.callback is not callback]
            if not bucket:
                self._listeners.pop(pattern, None)
            return len(bucket) != before

    def clear(self) -> None:
        with self._lock:
            self._listeners.clear()

    def listeners_for(self, event: str) -> list[EventCallback]:
        with self._lock:
            return [listener.callback for _, listener in self._snapshot(event)]

    async def emit(self, event: str, *args: Any, **kwargs: Any) -> EventResult:
        listeners = self._snapshot(event)
        results: list[Any] = []
        to_remove: list[tuple[str, EventCallback]] = []
        for pattern, listener in listeners:
            value = listener.callback(*args, **kwargs)
            if inspect.isawaitable(value):
                value = await value
            results.append(value)
            if listener.once:
                to_remove.append((pattern, listener.callback))
        for pattern, callback in to_remove:
            self.remove_listener(pattern, callback)
        return EventResult(event=event, listener_count=len(listeners), results=tuple(results))

    def _snapshot(self, event: str) -> list[tuple[str, _Listener]]:
        out: list[tuple[str, _Listener]] = []
        for pattern, listeners in self._listeners.items():
            if pattern == event or fnmatchcase(event, pattern):
                out.extend((pattern, listener) for listener in listeners)
        out.sort(key=lambda item: item[1].sort_key)
        return out