import ast
import base64
import json
from datetime import datetime
from typing import (
    List,
    Optional,
    Dict,
    Union,
)
from uuid import UUID

import iso8601
from event_manager_api.shared.error import (
    CANNOT_PROCESS_SYNC_EVENT,
    ASYNC_TARGET_MUST_HAVE_DATA,
    INVALID_UTF8_DATA,
    ASYNC_TARGET_MUST_HAVE_JSON_DATA,
    ASYNC_TARGET_MUST_DATA_MUST_CONTAIN_AN_ID,
    CANNOT_PROCESS_TARGET_AND_ASYNC_TARGET,
)
from event_manager_api.exception import (
    CannotProcessSyncEvent,
    AsyncTargetForEventWithNoData,
    AsyncTargetForEventWithNoUtf8Data,
    AsyncTargetForEventWithNoJsonData,
    AsyncTargetForEventWithDataWithoutId,
    CannotProcessATargetAndAsyncTarget,
)
from event_manager_api.shared.token import (
    TK_EVENT_MANAGER_PUBLISH,
    TK_EVENT_MANAGER_GET_PENDING,
    TK_EVENT_MANAGER_GET_ABANDONED,
    TK_EVENT_MANAGER_GET_DETAILS,
    TK_EVENT_MANAGER_DEFINITION_NOT_FOUND,
)
from messagebus import (
    MessageBus,
    Event,
    Message,
    DataType,
    DefaultToken,
    BadRequestMessage,
)
from event_manager_api.shared._AsyncTarget import AsyncTarget
from event_manager_api.shared._EventPublishDetailDto import EventPublishDetailDto
from event_manager_api.shared._PublicationAction import PublicationAction
from event_manager_api.shared._PublicationHistoryDto import PublicationHistoryDto


class EventManager(object):
    def __init__(
        self,               # type: EventManager
        message_bus,        # type: MessageBus
    ):
        self.message_bus = message_bus

        self.error_code_exception_map = {
            CANNOT_PROCESS_SYNC_EVENT[0]: CannotProcessSyncEvent,
            ASYNC_TARGET_MUST_HAVE_DATA[0]: AsyncTargetForEventWithNoData,
            INVALID_UTF8_DATA[0]: AsyncTargetForEventWithNoUtf8Data,
            ASYNC_TARGET_MUST_HAVE_JSON_DATA[0]: AsyncTargetForEventWithNoJsonData,
            ASYNC_TARGET_MUST_DATA_MUST_CONTAIN_AN_ID[0]: AsyncTargetForEventWithDataWithoutId,
            CANNOT_PROCESS_TARGET_AND_ASYNC_TARGET[0]: CannotProcessATargetAndAsyncTarget,
        }

    def publish(
        self,                    # type: EventManager
        event,                   # type: Event,
        target=None,             # type: Optional[str]
        async_target=None,       # type: Optional[AsyncTarget]
        external_code=None,      # type: Optional[str]
        fifo=False,              # type: bool
    ):
        # type: (...) -> EventPublishDetailDto

        request = self._build_request(
            event=event,
            target=target,
            async_target=async_target,
            external_code=external_code,
            fifo=fifo,
        )

        reply = self.message_bus.send_message(
            component="CustomEventManager",
            message=Message(
                token=TK_EVENT_MANAGER_PUBLISH,
                data_type=DataType.param,
                data=json.dumps(request).encode("utf-8"),
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return self._build_event_detail_dto(reply)

        raise self._build_exception_from_reply(reply)

    def publish_list(
        self,                    # type: EventManager
        event,                   # type: Event,
        targets=None,            # type: List[str]
        async_target=None,       # type: Optional[AsyncTarget]
        external_code=None,      # type: Optional[str]
        fifo=False,              # type: bool
    ):
        # type: (...) -> List[Union[EventPublishDetailDto, Exception]]

        detail_return = []
        for target in targets:
            ret = self.publish(
                event=event,
                target=target,
                async_target=async_target,
                external_code=external_code,
                fifo=fifo,
            )

            detail_return.append(ret)

        return detail_return

    def get_pending(self):
        # type: (...) -> List[EventPublishDetailDto]

        reply = self.message_bus.send_message(
            component="CustomEventManager",
            message=Message(
                token=TK_EVENT_MANAGER_GET_PENDING,
                data_type=DataType.string,
                data=b"",
            )
        )
        if reply.token != DefaultToken.TK_SYS_ACK:
            raise self._build_internal_error(message=reply)

        return self._build_event_detail_dto_list(reply)

    def get_abandoned(
        self,       # type: EventManager
    ):
        # type: (...) -> List[EventPublishDetailDto]

        reply = self.message_bus.send_message(
            component="CustomEventManager",
            message=Message(
                token=TK_EVENT_MANAGER_GET_ABANDONED,
                data_type=DataType.string,
                data=b"",
            )
        )
        if reply.token != DefaultToken.TK_SYS_ACK:
            raise self._build_internal_error(message=reply)

        return self._build_event_detail_dto_list(reply)

    def get_details(
        self,               # type: EventManager
        publish_detail_id,  # type: UUID
    ):
        # type: (...) -> Optional[EventPublishDetailDto]

        reply = self.message_bus.send_message(
            component="CustomEventManager",
            message=Message(
                token=TK_EVENT_MANAGER_GET_DETAILS,
                data_type=DataType.string,
                data=str(publish_detail_id).encode("utf-8"),
            )
        )
        if reply.token == TK_EVENT_MANAGER_DEFINITION_NOT_FOUND:
            return None

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise self._build_internal_error(message=reply)

        return self._build_event_detail_dto(reply=reply)

    def _build_exception_from_reply(
        self,       # type: EventManager,
        reply,      # type: Message
    ):
        # type: (...) -> Exception

        if reply.token == DefaultToken.TK_SYS_BAD_REQUEST:
            bad_request = BadRequestMessage.build_from_message(message=reply)
            if bad_request.error_code in self.error_code_exception_map:
                return self.error_code_exception_map[bad_request.error_code]()

        return self._build_internal_error(message=reply)

    @staticmethod
    def _build_internal_error(
        message,        # type: Message
    ):
        # type: (...) -> RuntimeError

        try:
            data_text = message.data.decode("utf-8")
        except UnicodeDecodeError:
            data_text = message.data.hex()
        return RuntimeError("Invalid response: {} - {}".format(message.token, data_text))

    def _build_request(
        self,                   # type: EventManager
        event,                  # type: Event
        target=None,            # type: Optional[str]
        async_target=None,      # type: Optional[AsyncTarget]
        external_code=None,     # type: Optional[str]
        fifo=False,             # type: bool
    ):
        # type: (...) -> Dict

        request = {
            "event": self._convert_event(
                event=event,
            )
        }
        if target is not None:
            request["target"] = target
        if async_target is not None:
            request["asyncTarget"] = {
                "replySubject": async_target.reply_subject,
            }
            if async_target.id_field != "id":
                request["asyncTarget"]["idField"] = async_target.id_field
        if external_code is not None:
            request["externalCode"] = external_code

        request["fifo"] = fifo

        return request

    def _build_event_detail_dto_list(
        self,           # type: EventManager
        message,        # type: Message
    ):
        # type: (...) -> List[EventPublishDetailDto]

        definitions_dict = json.loads(message.data.decode("utf-8"))
        ret = []
        for definition in definitions_dict:
            ret.append(self._build_event_detail_from_dict(definition))
        return ret

    def _build_event_detail_dto(
        self,       # type: EventManager
        reply,      # type: Message
    ):
        # type: (...) -> EventPublishDetailDto

        definition_dict = json.loads(reply.data.decode("utf-8"))
        return self._build_event_detail_from_dict(event_detail=definition_dict)

    def _build_event_detail_from_dict(
        self,           # type: EventManager
        event_detail,   # type: Dict
    ):
        # type: (...) -> EventPublishDetailDto

        return EventPublishDetailDto(
            id=event_detail["id"],
            external_code=event_detail.get("externalCode"),
            event=(self._build_event_from_dict(event_dict=event_detail["event"])),
            acked_at=self._build_optional_datetime(event_detail.get("ackedAt")),
            next_retry_at=self._build_optional_datetime(event_detail.get("nextRetryAt")),
            abandoned_at=self._build_optional_datetime(event_detail.get("abandonedAt")),
            publication_history=self._build_publication_history_from_list(event_detail.get("publicationHistory", [])),
            fifo=event_detail["fifo"],
            target=event_detail["target"],
        )

    @staticmethod
    def _build_event_from_dict(
        event_dict,     # type: Dict
    ):
        # type: (...) -> Event

        data = None
        if "data" in event_dict:
            data = base64.b64decode(event_dict["data"].encode("utf-8"))
        return Event(
            subject=event_dict["subject"],
            event_type=event_dict["eventType"] if "eventType" in event_dict else None,
            data=data,
            headers=ast.literal_eval(event_dict["headers"]),
        )

    @staticmethod
    def _build_publication_history_from_list(
        publication_history,        # type: List[Dict]
    ):
        # type: (...) -> List[PublicationHistoryDto]

        ret = []
        for history_dict in publication_history:
            ret.append(
                PublicationHistoryDto(
                    created_at=iso8601.parse_date(history_dict["createdAt"]),
                    action=PublicationAction(history_dict["action"])
                )
            )

        return ret

    @staticmethod
    def _convert_event(
        event,      # type: Event
    ):
        # type: (...) -> Dict

        event_json = {
            "subject": event.subject,
            "type": event.event_type,
            "headers": str(event.headers),
        }

        data = event.data
        if event.data is not None:
            if not isinstance(data, (bytes, bytearray)):
                try:
                    data = data.encode()
                except (AttributeError, UnicodeDecodeError):
                    pass

            event_json["data"] = base64.b64encode(data).decode("utf-8")

        if event.synchronous is True:
            event_json["synchronous"] = True

        return event_json

    @staticmethod
    def _build_optional_datetime(
        value,      # type: str
    ):
        # type: (...) -> Optional[datetime]

        if value is None:
            return None
        return iso8601.parse_date(value)
