import time
from typing import (
    List,
    Optional,
    Union,
    Callable,
    Sequence,
)

from edpwaiter import wait_for
from messagebus import (
    MessageBus,
    Message,
    Event,
    NakMessage,
    AckMessage,
    SubscriptionInfo,
)
from ._SendMessageMock import SendMessageMock
from ._SendMessageMockBuilder import SendMessageMockBuilder
from ._SentMessage import SentMessage
from ._SyncEventMock import SyncEventMock
from ._SyncEventMockBuilder import SyncEventMockBuilder


class ExpertMessageBusSpy(MessageBus):
    def __init__(self):
        self.send_message_mocks: List[SendMessageMock] = []
        self.sync_event_mocks: List[SyncEventMock] = []
        self.async_event_listeners: List[Callable[[Event], None]] = []

        self.published_events: List[Event] = []
        self.sent_messages: List[SentMessage] = []
        self.replied_message: Optional[Message] = None
        self.subscriptions: List[SubscriptionInfo] = []
        self.subscribe_listener: Optional[Callable[[List[SubscriptionInfo]], None]] = None

    def publish_event(self, event: Event) -> Message:

        self.published_events.append(event)
        if not event.synchronous:
            for listener in self.async_event_listeners:
                listener(event)
            return AckMessage()

        for mock in self.sync_event_mocks:
            if mock.is_valid_for(event=event):
                if isinstance(mock.response, Exception):
                    raise mock.response
                return mock.response
        return NakMessage()

    def send_message(
        self,
        component: str,
        message: Message
    ) -> Message:
        self.sent_messages.append(
            SentMessage(
                component=component,
                message=message
            )
        )
        for mock in self.send_message_mocks:
            if mock.is_valid_for(
                to_component=component,
                message=message,
            ):
                if isinstance(mock.response, Exception):
                    raise mock.response
                if callable(mock.response):
                    return mock.response(message)
                return mock.response
        return NakMessage()

    def subscribe(
        self,
        subscriptions
    ):
        if self.subscribe_listener is not None:
            self.subscribe_listener(subscriptions)
        self.subscriptions.extend(subscriptions)

    def reply_message(
        self,
        message: Message,
        reply_message: Message,
    ) -> None:

        self.replied_message = reply_message

    def reply_event(
        self,
        event: Event,
        reply_message: Message,
    ) -> None:

        self.replied_message = reply_message

    def add_send_message_response(
        self,
        mock: Union[SendMessageMockBuilder, SendMessageMock]
    ):
        if isinstance(mock, SendMessageMockBuilder):
            mock = mock.build()
        self.send_message_mocks.append(mock)

    def add_sync_event_response(
        self,
        mock: Union[SyncEventMockBuilder, SyncEventMock]
    ):
        if isinstance(mock, SyncEventMockBuilder):
            mock = mock.build()
        self.sync_event_mocks.append(mock)

    def add_async_event_listener(self, listener: Callable[[Event], None]):
        self.async_event_listeners.append(listener)

    def get_published_event(self, subject: str) -> Event:
        found = self.get_published_events(subject=subject)
        if len(found) == 0:
            raise AttributeError(f"Event with subject {subject} was never published")
        if len(found) > 1:
            raise AttributeError(f"Event with subject {subject} was published more than one time")

        return found[0]

    def wait_published_event(self, subject: str, timeout: int = 1) -> Event:
        end = time.time() + timeout
        now = time.time()
        while now < end:
            published = self.get_published_events(subject=subject)
            if len(published) > 1:
                raise AttributeError(f"Event with subject {subject} was published more than one time")

            if len(published) == 1:
                return published[0]

            time.sleep(0)
            now = time.time()

        raise RuntimeError(f"Event with subject {subject} was never published")

    def get_published_events(self, subject: str) -> List[Event]:
        found = []
        for event in self.published_events:
            if event.subject == subject:
                found.append(event)

        return found

    def wait_sent_message_to_component(self, component: str, timeout: float = 1) -> SentMessage:
        return self.wait_sent_message(
            func=lambda sent_message: sent_message.component == component,
            timeout=timeout
        )

    def wait_sent_message(self, func: Callable[[SentMessage], bool], timeout: float = 1) -> SentMessage:
        sent = self.wait_sent_messages(
            func=func,
            timeout=timeout
        )
        if len(sent) > 1:
            raise RuntimeError("More than one target message was sent")

        return sent[0]

    def wait_sent_messages(self, func: Callable[[SentMessage], bool], timeout: float = 1) -> Sequence[SentMessage]:
        end = time.time() + timeout
        now = time.time()
        while now < end:
            sent = self.get_sent_messages(func=func)
            if len(sent) > 0:
                return sent

            time.sleep(0)
            now = time.time()

        raise RuntimeError("Target message was never published")

    def get_sent_messages(self, func: Callable[[SentMessage], bool]) -> Sequence[SentMessage]:
        return tuple(filter(func, self.sent_messages))

    def wait_for_subscription(self, subject: str, timeout: float = 5):
        def has_subscription():
            return len(list(filter(lambda s: s.subject == subject, self.subscriptions))) > 0

        wait_for(
            func=has_subscription,
            message=f"Subscription for subject [{subject}] never happened",
            timeout=timeout,

        )

    def get_subscriptions(self, subject: str) -> Sequence[SubscriptionInfo]:
        return list(filter(lambda s: s.subject == subject, self.subscriptions))

    def clear_subscriptions(self):
        self.subscriptions = []
