import collections
import dataclasses
import json
from collections import Mapping
from datetime import datetime
from enum import Enum
from typing import Callable, Optional, Any, Dict, Type, Union, List

from dataclass_dict_convert.rfc3339 import dump_rfc3339, parse_rfc3339


class DataclassConvertError(Exception):
    def __init__(self, message):
        self.message = message


class UnknownFieldError(Exception):
    def __init__(self, message):
        self.message = message


@dataclasses.dataclass
class _DataclassDictConvertFieldMetaData:
    field_name: str
    dict_field_name: str
    to_dict_convertor: Callable[[Any], Any]
    from_dict_convertor: Callable[[Any], Any]


def _remove_none_recursive(val: Any) -> Any:
    if val is None:
        return None
    if isinstance(val, str) or isinstance(val, int) or isinstance(val, float) or isinstance(val, bool):
        return val
    if isinstance(val, list):
        # return [remove_none_recursive(v) for v in val if v is not None]
        return [_remove_none_recursive(v) for v in val]  # List is allowed to include None!
    if isinstance(val, dict):
        return {k: _remove_none_recursive(v) for k, v in val.items() if v is not None}
    raise ValueError('remove_none_recursive does not know how to handle type "{}"'.format(type(val)))


def _is_optional(a_type: type):
    if not (hasattr(a_type, '__origin__') and \
        a_type.__origin__ is Union and \
        len(a_type.__args__) == 2):
        return False
    t1 = a_type.__args__[0]
    t2 = a_type.__args__[1]
    for t in (t1,t2):
        if hasattr(t, '__origin__'):
            continue
        if isinstance(None, t):
            return True
    return False


def _get_optional_type(a_type: type):
    return a_type.__args__[0] if a_type.__args__[0] else a_type.__args__[1]


def _is_list(a_type: type):
    return hasattr(a_type, '__origin__') and a_type.__origin__ is list


def _is_dict(a_type: type):
    return hasattr(a_type, '__origin__') and a_type.__origin__ is dict


def _is_enum(a_type: type):
    if hasattr(a_type, '__origin__'):
        return False
    return issubclass(a_type, Enum)


def _is_json_primitive(a_type: type):
    return a_type is int or a_type is bool or a_type is float or a_type is str


def _find_convertor(
        field_name, field_type,
        default_datetime_convertor: Optional[Callable[[datetime], Any]],
        custom_dict_convertors: Dict[str, Callable[[Any], Any]],
        direct_fields: List[str],
        is_from: bool) -> Optional[Callable[[Any], Any]]:
    try:
        if field_name in custom_dict_convertors:
            return custom_dict_convertors[field_name]
        if _is_json_primitive(field_type) or field_name in direct_fields:
            return lambda p: p
        if field_type is datetime:
            return default_datetime_convertor
        if _is_optional(field_type):
            sub_convertor = _find_convertor(
                field_name, _get_optional_type(field_type),
                default_datetime_convertor, custom_dict_convertors, direct_fields,
                is_from)
            if sub_convertor is None:
                return None

            def _opt_convert_helper(orig_val):
                if orig_val is None:
                    return None
                else:
                    return sub_convertor(orig_val)
            return _opt_convert_helper
        if _is_list(field_type):
            sub_convertor = _find_convertor(
                field_name, field_type.__args__[0],
                default_datetime_convertor, custom_dict_convertors, direct_fields,
                is_from)
            if sub_convertor is None:
                return None

            def _opt_convert_helper(orig_val):
                return [sub_convertor(orig_el) for orig_el in orig_val]
            return _opt_convert_helper
        if _is_dict(field_type):
            if is_from:
                def dict_handler(d):
                    if not isinstance(d, Mapping):
                        raise DataclassConvertError(
                            "field {} of expected type dict is a {} instead".format(field_name, type(d)))
                    return dict(d)
                return dict_handler
            else:
                return lambda d: d
        if _is_enum(field_type):
            if is_from:
                return field_type.__members__.get
            else:
                return lambda enum_value: enum_value.name if enum_value is not None else None
        if dataclasses.is_dataclass(field_type):
            # assume dataclass has to_dict and from_dict
            if not hasattr(field_type, 'from_dict'):
                raise DataclassConvertError("Subclass '{}' of field {} does not have from_dict()".format(field_type, field_name))
            if not hasattr(field_type, 'to_dict'):
                raise DataclassConvertError("Subclass '{}' of field {} does not have to_dict()".format(field_type, field_name))
            if is_from:
                return lambda value: field_type.from_dict(value)
            else:
                return lambda value: value.to_dict()
    except:
        raise DataclassConvertError('Error while searching convertor for field "{}" of type "{}" is_from={}'
                                    .format(field_name, field_type, is_from))
    return None


def create_wrap_in_list_from_convertor(type_in_list: Type) -> Callable[[Any], List]:
    def wrap(val: str):
        val_type = type(val)
        if val_type is list:
            return val
        if val_type is type_in_list:
            return [val]
        raise TypeError('Unexpected type {} (expected {type_in_list} or List[{type_in_list}])'
                        .format(val_type, type_in_list=type_in_list))

    return wrap


def create_dict_of_dataclasses_from_convertor(type_of_dict_value, field_name_for_debug: Optional[str] = None) \
        -> Callable[[Dict[str, Any]], Dict]:
    def wrap(the_dict: dict):
        assert type(the_dict) is dict
        if not the_dict:
            return the_dict
        try:
            return {key: type_of_dict_value.from_dict(val) for key, val in the_dict.items()}
        except AttributeError:
            firstval = next(iter(the_dict.values()))
            raise DataclassConvertError(
                'Field {!r} has type {!r} which has no from_dict()'
                    .format(field_name_for_debug if field_name_for_debug else 'unknown', type(firstval)))

    return wrap


def create_dict_of_dataclasses_to_convertor(field_name_for_debug: Optional[str] = None) \
        -> Callable[[Dict[str, Any]], Dict]:
    def wrap(the_dict: dict):
        assert type(the_dict) is dict
        if not the_dict:
            return the_dict
        try:
            return {key: val.to_dict() for key, val in the_dict.items()}
        except AttributeError:
            _, firstval = next(iter(the_dict.values()))
            raise DataclassConvertError(
                'Field {} has type {} which has no to_dict()'
                    .format(field_name_for_debug if field_name_for_debug else 'unknown', type(firstval)))

    return wrap


@dataclasses.dataclass(frozen=True)
class _DataclassDictConvertMetaData:
    on_unknown_field: Callable[[str], None]
    metadata_by_fields: Dict[str, _DataclassDictConvertFieldMetaData]
    metadata_by_dict_fields: Dict[str, _DataclassDictConvertFieldMetaData]


def _wrap_dataclass_dict_convert(
        cls,
        dict_letter_case: Callable[[str], str],
        on_unknown_field: Callable[[str], None],
        direct_fields: List[str],
        default_to_datetime_convertor: Callable[[Any], datetime],
        default_from_datetime_convertor: Optional[Callable[[datetime], Any]],
        custom_to_dict_convertors: Dict[str, Callable[[Any], Any]],
        custom_from_dict_convertors: Dict[str, Callable[[Any], Any]]):
    metadata_by_fields = {}
    metadata_by_dict_fields = {}

    all_fields = dataclasses.fields(cls)

    for field in all_fields:
        meta = _DataclassDictConvertFieldMetaData(
            field.name,
            dict_letter_case(field.name),
            _find_convertor(field.name, field.type,
                            default_to_datetime_convertor, custom_to_dict_convertors, direct_fields,
                            is_from=False),
            _find_convertor(field.name, field.type,
                            default_from_datetime_convertor, custom_from_dict_convertors, direct_fields,
                            is_from=True)
        )
        if meta.to_dict_convertor is None:
            raise DataclassConvertError("""Unhandled "to" type '{}' of field '{}'""".format(field.type, field.name))
        if meta.from_dict_convertor is None:
            raise DataclassConvertError("""Unhandled "from" type '{}' of field '{}'""".format(field.type, field.name))
        metadata_by_fields[field.name] = meta
        metadata_by_dict_fields[meta.dict_field_name] = meta

    meta = _DataclassDictConvertMetaData(
        on_unknown_field, metadata_by_fields, metadata_by_dict_fields
    )
    # cls._dataclass_dict_convert_metadata = meta

    def _from_dict(cls2, d: dict, *, on_unknown_field_override: Optional[Callable[[str], None]] = None):
        if not isinstance(d, collections.Mapping):
            raise ValueError('from_dict(d) (possibly nested) where d is {} instead of dict: {!r}'.format(type(d), d))
        assert cls is cls2  # minor sanity check
        init_args = {}
        for key, value in d.items():
            field_meta = meta.metadata_by_dict_fields.get(key, None)
            if not field_meta:
                on_unknown_field_override(key) if on_unknown_field_override else meta.on_unknown_field(key)
            assert key == field_meta.dict_field_name
            init_args[field_meta.field_name] = field_meta.from_dict_convertor(value)
        return cls2(**init_args)

    def _to_dict(self, *, remove_none=False):
        res = {}
        for key, field_meta in meta.metadata_by_fields.items():
            assert key == field_meta.field_name
            value = getattr(self, field_meta.field_name)
            used_val = field_meta.to_dict_convertor(value)
            if used_val is not None or not remove_none:
                res[field_meta.dict_field_name] = _remove_none_recursive(used_val) if remove_none else used_val
        return res

    def _from_json(cls2, json_in: str, *, on_unknown_field_override: Optional[Callable[[str], None]] = None):
        assert cls is cls2  # minor sanity check
        return _from_dict(cls, json.loads(json_in), on_unknown_field_override=on_unknown_field_override)

    def _to_json(self) -> str:
        return json.dumps(self.to_dict())

    def _from_dict_list(cls2, l: list, *, on_unknown_field_override: Optional[Callable[[str], None]] = None):
        assert cls is cls2  # minor sanity check
        return [_from_dict(cls, d, on_unknown_field_override=on_unknown_field_override) for d in l]

    def _to_dict_list(inst_list: list):
        return [inst.to_dict() for inst in inst_list]

    cls.from_dict = classmethod(_from_dict)
    cls.to_dict = _to_dict
    cls.from_json = classmethod(_from_json)
    cls.to_json = _to_json
    cls.from_dict_list = classmethod(_from_dict_list)
    cls.to_dict_list = staticmethod(_to_dict_list)
    return cls


def dataclass_dict_convert(
        _cls=None, *,
        dict_letter_case: Optional[Callable[[str], str]]=None,
        on_unknown_field: Optional[Callable[[str], None]]=None,
        direct_fields: Optional[List[str]]=None,
        default_to_datetime_convertor: Optional[Callable[[Any], datetime]]=None,
        default_from_datetime_convertor: Optional[Callable[[datetime], Any]]=None,
        custom_to_dict_convertors: Optional[Dict[str, Callable[[Any], Any]]]=None,
        custom_from_dict_convertors: Optional[Dict[str, Callable[[Any], Any]]]=None):
    """
    This has complex logic to allow decorating with both:
      @dataclass_dict_convert
      @dataclass_dict_convert()

    dataclass_dict_convert takes parameters:

    example:
    @dataclass_dict_convert
    @dataclass(frozen=True)
    class DemoSub:
        int nr: int

    @dataclass_dict_convert(
         dict_letter_case=camelcase,
         on_undefined=None,  # default: raise exception
         default_to_datetime_convertor=parse_rfc3339,
         default_from_datetime_convertor=dump_rfc3339,
         custom_to_dict_convertors={
         },
         custom_from_dict_convertors={
             'str_lst': str_or_str_list_from_dict
         },
    )
    @dataclass(frozen=True)
    class Demo:
       nr: int
       text: str
       sometime: datetime
       opt_val: Optional[float]
       str_lst: List[str]
       sub: DemoSub
       list_sub: List[DemoSub]
       opt_sub: Optional[DemoSub]


    """
    def default_on_unknown(fieldname: str):
        raise UnknownFieldError('Unknown field encountered: {}'.format(fieldname))

    def wrap(cls):
        return _wrap_dataclass_dict_convert(
            cls,
            dict_letter_case if dict_letter_case else lambda s: s,
            on_unknown_field if on_unknown_field else default_on_unknown,
            direct_fields if direct_fields else [],
            default_to_datetime_convertor if default_to_datetime_convertor else dump_rfc3339,
            default_from_datetime_convertor if default_to_datetime_convertor else parse_rfc3339,
            custom_to_dict_convertors if custom_to_dict_convertors else {},
            custom_from_dict_convertors if custom_from_dict_convertors else {},)

    if _cls is None:
        return wrap
    return wrap(_cls)
