import asyncio
import fnmatch
import inspect
from abc import ABC, abstractmethod
from typing import Dict, Any, Callable, List


class EventBusError(Exception):
    pass


class Event:
    """
    Represents an event with a topic and associated payload.

    This class is designed to encapsulate an event, typically used in event-driven
    systems or messaging patterns. The class ensures that the topic is a string and
    the payload is a dictionary. It provides a representation method for easy
    inspection of event instances.

    :ivar topic: The topic of the event.
    :type topic: str
    :ivar payload: The payload of the event, containing related data.
    :type payload: Dict[str, Any]
    """

    def __init__(self, topic: str, payload: Dict[str, Any]):
        if not isinstance(topic, str):
            raise TypeError("Event topic must be a string.")
        if not isinstance(payload, dict):
            raise TypeError("Event payload must be a dictionary.")
        self.topic = topic
        self.payload = payload

    def __repr__(self):
        return f"Event(topic={self.topic!r}, payload={self.payload!r})"

    def __str__(self):
        return f"Event(topic={self.topic!r}, payload={self.payload!r})"

    def to_dict(self) -> Dict[str, Any]:
        """
        Converts the event instance to a dictionary representation.

        :return: A dictionary containing the topic and payload of the event.
        """
        return {
            'topic': self.topic,
            'payload': self.payload,
        }


class EventChannel(ABC):
    """
    Defines an abstract base class for an event channel.

    The purpose of this class is to serve as a blueprint for creating
    concrete event channels that can send events. It ensures that
    all subclasses implement the `send` method to handle the process
    of dispatching events.
    """

    @abstractmethod
    def send(self, event: Event) -> None:
        pass

    @classmethod
    @abstractmethod
    def from_config(cls, config: Dict[str, str]) -> 'EventChannel':
        """
        Creates an instance of the class based on the provided configuration.

        :param config: A dictionary containing configuration properties.
        :return: An initialized instance of the class.
        """
        pass

    @classmethod
    def validate_config(cls, config: Dict[str, str], required_keys: List[str]) -> None:
        """
        Validates that all required keys are present in the provided configuration.

        :param config: A dictionary containing configuration properties.
        :param required_keys: A list of keys that must be present.
        :raises ValueError: If any of the required keys are missing.
        """
        missing = [key for key in required_keys if key not in config]
        if missing:
            raise ValueError(f"Missing required configuration keys: {missing}")


class EventHandler:
    """
    Handles event-based communication within a system.

    This class is responsible for managing subscription to event topics, emitting events to a channel,
    and triggering callbacks registered for specific topics. Its primary purpose is to provide a
    centralized mechanism for event-driven programming, where subscribers can listen and respond
    to specific topics without direct coupling between components.

    :ivar channel: The communication channel through which events are sent.
    :type channel: EventChannel
    :ivar _handlers: A mapping of topic patterns to lists of callback functions subscribed to the
        respective patterns.
    :type _handlers: Dict[str, List[Callable[[Dict[str, Any]], None]]]
    """

    def __init__(self, channel: EventChannel):
        self.channel = channel
        self._handlers: Dict[str, List[Callable[[Dict[str, Any], Any], None]]] = {}

    def emit(self, topic: str, payload: Dict[str, Any]) -> None:
        """
        Emit an event using the specified topic and payload values.

        Creates an event with the given topic and payload, and sends it
        through the associated channel instance.

        :param topic: The topic to associate with the event.
        :param payload: A dictionary containing the event payload data.
        :return: None
        """
        event = Event(topic, payload)
        self.channel.send(event)

    def on(self, topic: str, callback: Callable[[Dict[str, Any], Any], Any]) -> None:
        """
        Registers a callback function for a specific topic. If the topic does not already
        exist in the handlers, it initializes a new list for the topic and appends the
        callback to it.

        The callback will be triggered when the associated topic is processed.

        :param topic: The name of the topic to register the callback for.
        :type topic: str
        :param callback: A callable function that will handle messages with the given
            topic. The callable must accept a dictionary containing message data.
        :type callback: Callable[[Dict[str, Any]], None]
        :return: None
        """
        if topic not in self._handlers:
            self._handlers[topic] = []
        self._handlers[topic].append(callback)

    def trigger(self, event: Event) -> None:
        """
        Triggers the execution of all callbacks that are associated with patterns
        matching the topic of the given event. It iterates through all registered
        patterns and checks if the event's topic matches any pattern. If a match
        is found, all associated callbacks for the matching pattern are executed
        with the event payload as an argument. This allows for dynamic and flexible
        event handling based on topic patterns.

        :param event: The event object containing a topic and payload.
        :type event: Event
        :return: None
        """
        matched = set()

        for pattern, callbacks in self._handlers.items():
            if fnmatch.fnmatch(event.topic, pattern):
                matched.update(callbacks)

        for callback in matched:
            if inspect.iscoroutinefunction(callback):
                asyncio.run(callback(event.payload, self))
            else:
                callback(event.payload, self)
