import socketio
from loguru import logger
from observable import Observable

from .error import SteamlabsSIOConnectionError
from .models import as_dataclass


class Client:
    EVENT_TYPES: set[str] = (
        {'donation'}  # streamlabs
        | {'follow', 'subscription', 'host', 'bits', 'raid'}  # twitch
        | {'follow', 'subscription', 'superchat'}  # youtube
    )

    def __init__(self, *, token: str, raw: bool = False):
        self._logger = logger.bind(name=self.__class__.__name__)
        self._token = token
        self._raw = raw
        self._sio = socketio.Client()
        self._sio.on('connect', self._connect_handler)
        self._sio.on('event', self._event_handler)
        self._sio.on('disconnect', self._disconnect_handler)
        self.obs = Observable()

    def __enter__(self) -> 'Client':
        try:
            self._sio.connect(f'https://sockets.streamlabs.com?token={self._token}')
        except socketio.exceptions.ConnectionError as e:
            self._logger.exception('Connection to Streamlabs Socket API failed')
            ERR_MSG = 'Failed to connect to Streamlabs Socket API. Please check your token and network connection.'
            raise SteamlabsSIOConnectionError(ERR_MSG) from e
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        self._sio.disconnect()

    def wait(self, seconds: float | None = None) -> None:
        """Exposes the wait and sleep methods of the socketio client to allow the user to keep the connection alive.


        Args:
            seconds (float | None): If None, the method will block indefinitely.

        Returns:
            None
        """
        if seconds is None:
            self._sio.wait()
        else:
            self._sio.sleep(seconds)

    def _connect_handler(self) -> None:
        self._logger.info('Connected to Streamlabs Socket API')

    def _event_handler(self, data: dict) -> None:
        """
        Handles incoming events and triggers the callback functions.


        Args:
            data (dict): The event data containing information about the event.
                Expected keys:
                    - 'for': The target of the event.
                    - 'type': The type of the event.
                    - 'message': A list containing the event message.
        Returns:
            None
        """

        match data:
            case {
                'for': str() as target,
                'type': str() as event_type,
                'message': list() as message_list,
            } if event_type in Client.EVENT_TYPES:
                # The message type is expected to be a list, however, in practice, it often contains only one item.
                # To ensure compatibility with the actual data structure,
                # we will iterate over the message list and trigger events for each message item.
                for message in message_list:
                    self.obs.trigger(
                        target,
                        event_type,
                        message if self._raw else as_dataclass(event_type, message),
                    )
                    self._logger.debug(
                        f'Triggered event: {target} {event_type} {message}'
                    )
            case _:
                self._logger.debug(f'Unexpected event data: {data}')

    def _disconnect_handler(self) -> None:
        self._logger.info('Disconnected from Streamlabs Socket API')


def request_client_object(*, token: str, raw: bool = False) -> Client:
    """Entry point for users to request a Client object.

    Args:
        token (str): The authentication token for the Streamlabs Socket API.
        raw (bool, optional): If True, the client will return raw event data. Defaults to False.

    Returns:
        Client: An instance of the Client class.
    """
    return Client(token=token, raw=raw)
