import inspect
import json
from typing import Dict, Union, Tuple

from messagebus import HttpResponseMessage
from ._util import convert_to_json
from .exception import PropertyError


class ExceptionFormatter(object):
    def __init__(self, exception_map):
        # type: (Dict[type, Union[int, Tuple[int, str]]]) -> None
        self.exception_map = exception_map
        self.property_error_code_message_map = {
            PropertyError.UNKNOWN_ERROR: "unknown error",
            PropertyError.CANNOT_BE_NULL: "cannot be null",
            PropertyError.NOT_AN_INTEGER: "not an integer",
            PropertyError.NOT_A_FLOAT: "not a float",
            PropertyError.NOT_A_STRING: "not a string",
            PropertyError.NOT_A_BOOLEAN: "not a boolean",
            PropertyError.NOT_A_DATE: "not a date",
            PropertyError.NOT_A_DATETIME: "not a datetime",
            PropertyError.NOT_A_UUID: "not a uuid",
            PropertyError.NOT_A_LIST: "not a list",
            PropertyError.LIST_CONTENT_IS_INVALID: "list content is invalid",
            PropertyError.NOT_A_DICT: "not a dict",
            PropertyError.DICT_KEY_IS_INVALID: "dict key is invalid",
            PropertyError.DICT_VALUE_IS_INVALID: "dict value is invalid",
            PropertyError.NOT_FOUND_IN_HEADERS: "not found in headers",
            PropertyError.NOT_FOUND_IN_QUERY_STRING: "not found in query string",
            PropertyError.NOT_FOUND_IN_PATH: "not found in path",
        }

    def format(self, message_bus, message, exception, default_status_code):
        try:
            if isinstance(exception, PropertyError) and type(exception) not in self.exception_map:
                error_dict = {
                    "property": exception.property_name,
                    "reason": self.property_error_code_message_map[exception.error_code]
                }
                message_bus.reply_message(
                    message,
                    HttpResponseMessage(
                        400,
                        json.dumps(error_dict).encode("utf-8"),
                        {
                            "Content-Type": "application/json; charset=UTF-8"
                        }
                    )
                )
                return

            map_info = self.exception_map[type(exception)]
            if isinstance(map_info, int):
                exception_cls = type(exception)
                body = b""
                headers = {}
                if self.has_own_init(exception_cls):
                    body = convert_to_json(exception).encode("utf-8")
                    headers["Content-Type"] = "application/json; charset=UTF-8"

                message_bus.reply_message(message, HttpResponseMessage(map_info, body, headers))
            elif callable(map_info):
                response = map_info(exception)
                if isinstance(response, HttpResponseMessage):
                    message_bus.reply_message(message, response)
                else:
                    body = convert_to_json(response).encode("utf-8")
                    headers = {
                        "Content-Type": "application/json; charset=UTF-8"
                    }
                    message_bus.reply_message(message, HttpResponseMessage(400, body, headers))
            else:
                status_code = map_info[0]
                body = map_info[1]
                if isinstance(body, dict):
                    message_bus.reply_message(message,
                                              HttpResponseMessage(
                                                  status_code,
                                                  json.dumps(body).encode("utf-8"),
                                                  {"Content-Type": "application/json; charset=UTF-8"}))
                else:
                    message_bus.reply_message(message, HttpResponseMessage(status_code, body))

        except TypeError:
            self._create_response_when_exception_map_is_none(message_bus, message, default_status_code)
        except KeyError:
            self._create_response_when_exception_map_does_not_have_exception(message_bus,
                                                                             message,
                                                                             exception,
                                                                             default_status_code)

    @staticmethod
    def _create_response_when_exception_map_is_none(message_bus, message, default_status_code):
        message_bus.reply_message(message, HttpResponseMessage(default_status_code))

    @staticmethod
    def _create_response_when_exception_map_does_not_have_exception(message_bus,
                                                                    message,
                                                                    exception,
                                                                    default_status_code):
        cls = type(exception)
        status_code = default_status_code
        body = b""

        if hasattr(cls, "_cls_attrs"):
            status_code = cls._cls_attrs.get("status_code", status_code)
            body = cls._cls_attrs.get("body", b"")

        if isinstance(body, dict):
            message_bus.reply_message(message,
                                      HttpResponseMessage(
                                          status_code,
                                          json.dumps(body).encode("utf-8"),
                                          {"Content-Type": "application/json; charset=UTF-8"}))
        else:
            message_bus.reply_message(message, HttpResponseMessage(status_code, body))

    @staticmethod
    def has_own_init(cls):
        mro = inspect.getmro(cls)
        for i in range(1, len(mro)):
            if mro[i].__init__ == cls.__init__:
                return False
        return True
