import inspect
import json
from enum import Enum
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Type,
    TypeVar,
    Union,
)

from ._util import (
    is_primitive,
    to_camel_case,
    transform_primitive,
)

T = TypeVar('T')
T2 = TypeVar('T2')


def parse(
    s: str,
    for_type: Type[T],
    custom_mapping: Dict[Type[T2], Callable[[Dict[str, Any]], T2]] = None,
) -> T:
    obj_dict = json.loads(s)
    return parse_dict(obj_dict, for_type=for_type, custom_mapping=custom_mapping)


def parse_dict(
    obj_dict: Dict[str, Any],
    for_type: Type,
    custom_mapping: Dict[Type, Callable[[Dict[str, Any]], Type]] = None,
) -> T:

    if custom_mapping is None:
        custom_mapping = {}

    sig = inspect.signature(for_type.__init__)

    constructor_args = []

    for prop in sig.parameters.keys():
        if prop == 'self':
            continue

        if prop in obj_dict:
            value = obj_dict[prop]
        else:
            camel_prop = to_camel_case(prop)
            if camel_prop in obj_dict:
                value = obj_dict[camel_prop]
            else:
                raise RuntimeError(f"Could not find {prop} in {obj_dict}")

        prop_type = sig.parameters[prop].annotation

        constructor_args.append(
            _transform_value(
                prop_type=prop_type,
                value=value,
                custom_mapping=custom_mapping
            )
        )

    return for_type(*constructor_args)


def _transform_value(
    prop_type: Type,
    value: Any,
    custom_mapping: Dict[Type, Callable[[Dict[str, Any]], Any]],
):
    if prop_type == Any:
        prop_type = type(value)
        if prop_type == list:
            if len(value) == 0:
                return []
            prop_type = List[type(value[0])]

        if prop_type == dict:
            prop_type = Dict[str, Any]

    if is_primitive(prop_type):
        return transform_primitive(
            t=prop_type,
            value=value,
        )

    elif prop_type in custom_mapping:
        mapped = custom_mapping[prop_type](value)
        if isinstance(mapped, type):
            return _transform_value(
                prop_type=mapped,
                value=value,
                custom_mapping=custom_mapping,
            )

        return mapped

    elif hasattr(prop_type, "__origin__"):
        if prop_type.__origin__ == list:
            inner_type = getattr(prop_type, "__args__")[0]
            if not isinstance(value, list):
                raise RuntimeError(f"not a list")

            ret = []
            for item in value:
                ret.append(
                    _transform_value(
                        prop_type=inner_type,
                        value=item,
                        custom_mapping=custom_mapping,
                    )
                )

            return ret

        elif prop_type.__origin__ == dict:
            inner_key_type = getattr(prop_type, "__args__")[0]
            inner_value_type = getattr(prop_type, "__args__")[1]
            if not isinstance(value, dict):
                raise RuntimeError(f"not a dict")

            ret = {}
            for key, item in value.items():
                transformed_key = _transform_value(
                    prop_type=inner_key_type,
                    value=key,
                    custom_mapping=custom_mapping,
                )
                value = _transform_value(
                    prop_type=inner_value_type,
                    value=item,
                    custom_mapping=custom_mapping,
                )
                ret[transformed_key] = value

            return ret

        elif prop_type.__origin__ == Union:
            none_type = getattr(prop_type, "__args__")[1]
            if not isinstance(None, none_type):
                raise RuntimeError("Union type not supported")

            inner_type = getattr(prop_type, "__args__")[0]
            if value is None:
                return None

            return _transform_value(
                prop_type=inner_type,
                value=value,
                custom_mapping=custom_mapping,
            )

        else:
            raise RuntimeError(f"Unsupported origin: {prop_type.__origin__}")

    elif issubclass(prop_type, Enum):
        try:
            return prop_type(value)
        except ValueError:
            return prop_type[value]

    elif prop_type == dict:
        return value

    else:
        return parse_dict(
            obj_dict=value,
            for_type=prop_type,
            custom_mapping=custom_mapping,
        )
