import sys

if sys.version_info[0] < 3:
    import inspect2 as inspect
else:
    import inspect

import json
from datetime import datetime, date
from typing import Any, Union, List
from uuid import UUID

import iso8601
from httpmessageprocessor import ApiRequest
from httpmessageprocessorutil.automapped.exception import PropertyError

from ._util import to_camel_case, convert_underscore_to_hyphen
from .exception import BodyNotJson, PropertyNotSent


class ApiRequestParser(object):
    def __init__(self, request_type, extra_mapping):
        self.request_type = request_type
        self.extra_mapping = extra_mapping if extra_mapping is not None else {}
        self.context_mappings = []

        if self.request_type is not None:
            self.attributes = self.build_type_attributes(self.request_type)
        self.json_body = None

    def parse(self, api_request):
        # type: (ApiRequest) -> Any
        if self.request_type is None:
            return None
        constructor_args = self.get_variables_from_request(api_request)
        if self.extra_mapping.get("removeNotSent", False):
            full_constructor_args = {}
            for attr_name in self.attributes.keys():
                if attr_name in constructor_args:
                    full_constructor_args[attr_name] = constructor_args[attr_name]
                else:
                    full_constructor_args[attr_name] = None
        else:
            full_constructor_args = constructor_args
        value = self.request_type(**full_constructor_args)
        self.remove_not_sent(full_constructor_args, constructor_args, value)

        return value

    def remove_not_sent(self, full_constructor_args, constructor_args, value):
        if self.extra_mapping.get("removeNotSent", False):
            for attr_name in full_constructor_args:
                if getattr(value, attr_name) is None and attr_name not in constructor_args:
                    delattr(value, attr_name)

    def get_variables_from_request(self, api_request):
        constructor_args = {}
        not_found_variables = []
        self.get_variables_from_path_and_query(api_request, constructor_args, not_found_variables)
        if len(not_found_variables) > 0:
            self.get_variables_from_body(api_request, constructor_args, not_found_variables)
        return constructor_args

    def get_variables_from_path_and_query(self, api_request, constructor_args, not_found_variables):
        for attr_name in self.attributes.keys():
            value = self.get_from_path_or_query(api_request, attr_name)
            if value is not None:
                constructor_args[attr_name] = value
            else:
                not_found_variables.append(attr_name)

    def get_variables_from_body(self, api_request, constructor_args, not_found_variables):
        body_variables = self.get_from_body(api_request, not_found_variables)
        for variable_name in body_variables:
            if variable_name not in constructor_args:
                constructor_args[variable_name] = body_variables[variable_name]

    def build_type_attributes(self, cls):
        attributes = {}
        if hasattr(cls, "_real_cls"):
            init_params = inspect.signature(cls._real_cls.__init__).parameters
        else:
            init_params = inspect.signature(cls.__init__).parameters
        for arg_name in init_params.keys():
            value = init_params[arg_name]
            if arg_name == "self":
                continue
            attrs = {}
            if value.annotation != inspect.Signature.empty:
                attrs["type"] = value.annotation
            else:
                attrs["type"] = Any

            if hasattr(cls, "_cls_attrs") and arg_name in cls._cls_attrs:
                for key in cls._cls_attrs[arg_name].keys():
                    attrs[key] = cls._cls_attrs[arg_name][key]

            extra_mapping = self.get_current_mapping()
            if extra_mapping is not None and arg_name in extra_mapping:
                for key in extra_mapping[arg_name].keys():
                    extra_mapping_value = extra_mapping[arg_name][key]
                    if not isinstance(extra_mapping_value, dict):
                        attrs[key] = extra_mapping_value

            attributes[arg_name] = attrs

        return attributes

    def get_from_path_or_query(self, api_request, attr_name):
        attr_map_properties = self.attributes[attr_name]
        name = attr_name
        if "name" in attr_map_properties:
            name = attr_map_properties["name"]

        if "location" in attr_map_properties:
            location = attr_map_properties["location"]
            if location == "query":
                selected_dict = api_request.query_parameters
                selected_dict_name = "query string"
            elif location == "path":
                selected_dict = api_request.path_parameters
                selected_dict_name = "path parameters"
            elif location == "header":
                selected_dict = api_request.headers
                selected_dict_name = "header parameters"
            elif location == "body":
                return None
            else:
                raise Exception("Invalid Location: {}".format(location))

            found_name = name
            if name not in selected_dict:
                camel_name = to_camel_case(name)
                if camel_name not in selected_dict:
                    raise Exception("Attribute {} not found in {}".format(name, selected_dict_name))
                else:
                    found_name = camel_name

            if "type" in attr_map_properties:
                return self.transform_value(selected_dict[found_name], attr_map_properties["type"])
            else:
                return selected_dict[name]
        else:
            return self.get_attr_value_from_path_or_query(attr_name, name, api_request)

    def get_attr_value_from_path_or_query(self, attr_name, variable_name, api_request):
        value = self.get_attr_value_from_dicts(variable_name, api_request)
        if value is None:
            return value
        if "type" in self.attributes[attr_name]:
            return self.transform_value(value, self.attributes[attr_name]["type"])
        return value

    def get_attr_value_from_dicts(self, attr_name, api_request):
        if attr_name in api_request.path_parameters:
            return api_request.path_parameters[attr_name]
        camel_name = to_camel_case(attr_name)
        if camel_name in api_request.path_parameters:
            return api_request.path_parameters[camel_name]

        if attr_name in api_request.query_parameters:
            return api_request.query_parameters[attr_name]
        if camel_name in api_request.query_parameters:
            return api_request.query_parameters[camel_name]

    def get_from_body(self, api_request, variable_names):
        body_str, force_json = self.encode_body(api_request)

        body_variables = {}
        for attr_name in variable_names:
            attr_map_properties = self.attributes[attr_name]
            name = attr_name
            if "name" in attr_map_properties:
                name = attr_map_properties["name"]

            body_type = Any
            if "type" in attr_map_properties:
                body_type = attr_map_properties["type"]

            try:
                body_variables[attr_name] = self.build_object_from_body(body_type, body_str, name, force_json)
            except PropertyNotSent:
                pass
            except BodyNotJson:
                raise
            except Exception as ex:
                raise PropertyError(attr_name)
        return body_variables

    @staticmethod
    def encode_body(api_request):
        encoding_name = "utf-8"
        force_json = False
        if "Content-Type" in api_request.headers:
            if api_request.headers["Content-Type"].startswith("application/json"):
                force_json = True

            separator_index = api_request.headers["Content-Type"].find(";")
            if separator_index >= 0:
                charset_specification = api_request.headers["Content-Type"][separator_index + 1:].strip()
                parts = charset_specification.split("=")
                if len(parts) > 1:
                    encoding_name = parts[1]
        body_str = api_request.body.decode(encoding_name)
        return body_str, force_json

    def build_object_from_body(self, type, body, attr_name, force_json):
        try:
            if self.json_body is None:
                self.json_body = json.loads(body)
            if attr_name in self.json_body:
                try:
                    if attr_name in self.extra_mapping:
                        self.context_mappings.append(self.extra_mapping[attr_name])
                    else:
                        self.context_mappings.append(None)
                    return self.transform_value(self.json_body[attr_name], type)
                finally:
                    self.context_mappings.pop()

            camel_attr_name = to_camel_case(attr_name)
            if camel_attr_name in self.json_body:
                return self.transform_value(self.json_body[camel_attr_name], type)
            hyphen_attr_name = convert_underscore_to_hyphen(attr_name)
            if hyphen_attr_name in self.json_body:
                return self.transform_value(self.json_body[hyphen_attr_name], type)

            return self.transform_value(None, type, True)
        except ValueError:
            if force_json:
                raise BodyNotJson(body)

            if body == "":
                return None

            body_type = str
            if attr_name is not None and self.is_scalar_type(type):
                body_type = type
            return self.transform_value(body, body_type)

    @staticmethod
    def is_scalar_type(type):
        return type == int or type == float or type == bool or type == str or type == datetime or type == date

    def transform_value(self, value, new_type, property_not_sent=False):
        if new_type == Any:
            return value

        if new_type == str:
            if value is None:
                raise Exception("str type does not accept None values")
            return str(value)

        if new_type == int:
            return int(value)

        if new_type == float:
            return float(value)

        if new_type == bool:
            if isinstance(value, str):
                return value.lower() == "true"
            if isinstance(value, bool):
                return value
            if isinstance(value, int):
                return value != 0
            raise Exception("Value cannot be converted to bool: {}".format(value))

        if new_type == datetime or new_type == date:
            return iso8601.parse_date(value)

        if new_type == UUID:
            return UUID(hex=value)

        if hasattr(new_type, "__origin__"):
            if new_type.__origin__ == list or new_type.__origin__ == List:
                inner_type = new_type.__args__[0]
                if not isinstance(value, list):
                    raise Exception("Value is not a list: {}".format(value))

                ret = []
                for item in value:
                    ret.append(self.transform_value(item, inner_type))
                return ret

            if new_type.__origin__ == dict:
                key_type = new_type.__args__[0]
                value_type = new_type.__args__[1]
                if not isinstance(value, dict):
                    raise Exception("Value is not a dict: {}".format(value))
                ret = {}
                for json_key in value.keys():
                    ret[self.transform_value(json_key, key_type)] = self.transform_value(value[json_key], value_type)
                return ret

            if new_type.__origin__ == Union:
                if len(new_type.__args__) == 2 and \
                        (isinstance(None, new_type.__args__[1]) or isinstance(None, new_type.__args__[0])):
                    if value is None:
                        if property_not_sent and self.extra_mapping.get("removeNotSent", False):
                            raise PropertyNotSent()
                        return None
                    else:
                        if isinstance(None, new_type.__args__[1]):
                            return self.transform_value(value, new_type.__args__[0])
                        else:
                            return self.transform_value(value, new_type.__args__[1])

            raise Exception("Unsupported type: {}".format(new_type))

        type_attributes = self.build_type_attributes(new_type)
        constructor_args = {}
        full_constructor_args = {}
        for attr_name in type_attributes.keys():
            search_name = self.get_search_name(attr_name)
            try:
                if search_name in value:
                    constructor_args[attr_name] = self.transform_value(value[search_name], type_attributes[attr_name]["type"])
                else:
                    camel_attr_name = to_camel_case(search_name)
                    hyphen_attr_name = convert_underscore_to_hyphen(search_name)
                    if camel_attr_name in value:
                        constructor_args[attr_name] = self.transform_value(value[camel_attr_name],
                                                                           type_attributes[attr_name]["type"])
                    elif hyphen_attr_name in value:
                        constructor_args[attr_name] = self.transform_value(value[hyphen_attr_name],
                                                                           type_attributes[attr_name]["type"])
                    else:
                        try:
                            constructor_args[attr_name] = self.transform_value(None,
                                                                               type_attributes[attr_name]["type"],
                                                                               property_not_sent=True)
                        except PropertyNotSent:
                            full_constructor_args[attr_name] = None
            except Exception as ex:
                raise PropertyError(attr_name)

        for attr_name in constructor_args.keys():
            full_constructor_args[attr_name] = constructor_args[attr_name]

        value = new_type(**full_constructor_args)
        self.remove_not_sent(full_constructor_args, constructor_args, value)
        return value

    def get_search_name(self, attr_name):
        current_mapping = self.get_current_mapping()
        if current_mapping is not None and attr_name in current_mapping and "name" in current_mapping[attr_name]:
            return current_mapping[attr_name]["name"]
        return attr_name

    def get_current_mapping(self):
        if len(self.context_mappings) > 0:
            return self.context_mappings[-1]
        else:
            return self.extra_mapping
