import ast
import base64
import json
from json import JSONDecodeError
from typing import (
    Dict,
    Any,
)

from event_manager_api.shared.error import (
    EVENT_SUBJECT_MANDATORY,
    INVALID_JSON,
    CANNOT_PROCESS_SYNC_EVENT,
    ASYNC_TARGET_MUST_HAVE_REPLY_SUBJECT,
    EVENT_MANDATORY,
    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 messagebus import (
    BadRequestMessage,
    AckMessage,
    Event,
    Message,
    InternalErrorMessage,
)
from messageprocessorutil import UuidMessageProcessor
from model import (
    EventPublishDetail,
)
from model.mapper import EventPublishDetailMapper
from ._PublishEventInteractor import PublishEventInteractor
from .dto import (
    PublishEventRequest,
    AsyncTargetDto,
    CannotProcessSyncEvent,
)
from .._ParseException import ParseException


class PublishEventProcessor(UuidMessageProcessor):
    def __init__(
        self,
        publish_event_interactor: PublishEventInteractor,
    ):
        super().__init__()

        self.publish_event_interactor = publish_event_interactor

    def parse_data(
        self,
        message: Message,
        data: bytes,
    ):
        input_json = self._parse_data_as_json(data=data)
        event = self._parse_event(input_json=input_json)

        if input_json.get("target") is not None and input_json.get("asyncTarget") is not None:
            raise ParseException(
                error_code=CANNOT_PROCESS_TARGET_AND_ASYNC_TARGET[0],
                error_message=CANNOT_PROCESS_TARGET_AND_ASYNC_TARGET[1],
            )

        async_target = None
        if "asyncTarget" in input_json:
            async_target = self._parse_async_target(
                input_json=input_json,
                event=event
            )
        return PublishEventRequest(
            event=event,
            target=input_json.get("target"),
            async_target=async_target,
            external_code=input_json.get("externalCode"),
            fifo=input_json.get("fifo", False),
        )

    def call_business(
        self,
        request: PublishEventRequest,
    ):
        return self.publish_event_interactor.publish_event(request=request)

    def format_response(
        self,
        message_bus,
        message,
        event,
        input_event,
        event_definition: EventPublishDetail,
    ):
        message_bus.reply_message(
            message=message,
            reply_message=AckMessage(
                data=json.dumps(EventPublishDetailMapper.map(publish_detail=event_definition).to_dict()).encode("utf-8")
            ),
        )

    def format_exception(
        self,
        message_bus,
        message,
        event,
        input_obj,
        exception
    ):
        if isinstance(exception, CannotProcessSyncEvent):
            message_bus.reply_message(
                message=message,
                reply_message=BadRequestMessage(
                    error_code=CANNOT_PROCESS_SYNC_EVENT[0],
                    error_message=CANNOT_PROCESS_SYNC_EVENT[1],
                )
            )
        else:
            message_bus.reply_message(
                message=message,
                reply_message=InternalErrorMessage(ex=exception),
            )

    def format_parse_exception(
        self,
        message_bus,
        message,
        event,
        data,
        exception
    ):
        if isinstance(exception, ParseException):
            message_bus.reply_message(
                message=message,
                reply_message=BadRequestMessage(
                    error_code=exception.error_code,
                    error_message=exception.error_message,
                ),
            )
        else:
            message_bus.reply_message(
                message=message,
                reply_message=InternalErrorMessage(ex=exception),
            )

    @staticmethod
    def _parse_data_as_json(
        data: bytes,
    ) -> Dict[str, Any]:
        if data is None:
            raise ParseException(
                error_code=INVALID_JSON[0],
                error_message=INVALID_JSON[1],
            )
        try:
            return json.loads(data.decode("utf-8"))
        except (UnicodeDecodeError, JSONDecodeError):
            raise ParseException(
                error_code=INVALID_JSON[0],
                error_message=INVALID_JSON[1],
            )

    def _parse_event(
        self,
        input_json: Dict[str, Any],
    ) -> Event:
        if "event" not in input_json:
            raise ParseException(
                error_code=EVENT_MANDATORY[0],
                error_message=EVENT_MANDATORY[1],
            )
        if "subject" not in input_json["event"]:
            raise ParseException(
                error_code=EVENT_SUBJECT_MANDATORY[0],
                error_message=EVENT_SUBJECT_MANDATORY[1],
            )
        event = self._create_event_from_dict(input_obj=input_json["event"])
        return event

    @staticmethod
    def _create_event_from_dict(
        input_obj: Dict[str, Any],
    ) -> Event:
        data = None
        if "data" in input_obj and input_obj["data"] is not None:
            data = base64.b64decode(input_obj["data"].encode("utf-8"))
        return Event(
            subject=input_obj["subject"],
            event_type=input_obj["type"] if "type" in input_obj else None,
            data=data,
            headers=ast.literal_eval(input_obj["headers"]) if "headers" in input_obj else None,
            synchronous=True if "synchronous" in input_obj and input_obj["synchronous"] is True else False,
        )

    @staticmethod
    def _parse_async_target(
        input_json: Dict[str, Any],
        event: Event,
    ) -> AsyncTargetDto:
        async_target_json = input_json["asyncTarget"]
        if "replySubject" not in async_target_json:
            raise ParseException(
                error_code=ASYNC_TARGET_MUST_HAVE_REPLY_SUBJECT[0],
                error_message=ASYNC_TARGET_MUST_HAVE_REPLY_SUBJECT[1],
            )
        if event.data is None or event.data == b"":
            raise ParseException(
                error_code=ASYNC_TARGET_MUST_HAVE_DATA[0],
                error_message=ASYNC_TARGET_MUST_HAVE_DATA[1],
            )
        try:
            data_json = json.loads(event.data.decode("utf-8"))
        except UnicodeDecodeError:
            raise ParseException(
                error_code=INVALID_UTF8_DATA[0],
                error_message=INVALID_UTF8_DATA[1],
            )
        except JSONDecodeError:
            raise ParseException(
                error_code=ASYNC_TARGET_MUST_HAVE_JSON_DATA[0],
                error_message=ASYNC_TARGET_MUST_HAVE_JSON_DATA[1],
            )
        id_field = "id"
        if "idField" in async_target_json:
            id_field = async_target_json["idField"]
        if id_field not in data_json:
            raise ParseException(
                error_code=ASYNC_TARGET_MUST_DATA_MUST_CONTAIN_AN_ID[0],
                error_message=ASYNC_TARGET_MUST_DATA_MUST_CONTAIN_AN_ID[1],
            )
        return AsyncTargetDto(
            reply_subject=input_json["asyncTarget"]["replySubject"],
            id_field=input_json["asyncTarget"].get("idField"),
        )
