# Copyright (c) 2021 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
import typing
from datetime import datetime
from functools import wraps
from inspect import isclass
from typing import Type, Any, Union, Callable
from voluptuous import Schema, Required, REMOVE_EXTRA, Optional, Coerce, Maybe, All, PREVENT_EXTRA

from czc import json

T = typing.TypeVar('T')


def deserializable(schema: typing.Optional[Schema] = None) -> Callable[[Type[T]], Type[T]]:
    def _deserializable(cls: Type[T]) -> Type[T]:
        if not (_issubclass(cls, tuple) and typing.get_type_hints(cls)):
            raise TypeError(f'@deserializable is only supported on typed NamedTuple, not {cls}')
        orig_new = cls.__new__

        @wraps(orig_new)
        def new_wrapper(inner_cls, *args, **kwargs):
            if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], dict):
                if schema:
                    return orig_new(inner_cls, **schema(args[0]))
                return coerce(inner_cls, args[0])
            return orig_new(inner_cls, *args, **kwargs)
        setattr(cls, '__new__', new_wrapper)

        return cls
    return _deserializable


def coerce(to_type: Type[T], value: Any) -> T:
    return create_schema_from_type(to_type)(value)


def _is_optional(origin, type_args):
    return origin is Union and len(type_args) == 2 and isinstance(None, type_args[1])


def _schema_key(key_name: str, to_type: Any):
    has_default = hasattr(to_type, '_field_defaults') and key_name in to_type._field_defaults
    return Optional(key_name) if has_default else Required(key_name)


def _issubclass(cls, from_type):
    return isclass(cls) and issubclass(cls, from_type)


def create_schema_from_type(to_type: Type[T], prevent_extra=False) -> Callable[[Any], T]:
    def create_schema(t):
        origin = typing.get_origin(t)
        if origin:
            type_args = typing.get_args(t)
            if _is_optional(origin, type_args):
                return Maybe(create_schema(type_args[0]))
            elif _issubclass(origin, list):
                return [create_schema(type_args[0])]
            elif _issubclass(origin, dict):
                return {create_schema(type_args[0]): create_schema(type_args[1])}
            elif _issubclass(origin, set):
                return All([create_schema(type_args[0])], Coerce(set))
        elif _issubclass(t, tuple):
            hints = typing.get_type_hints(t)
            if hints:
                return All(Schema({_schema_key(k, t): create_schema(v) for k, v in hints.items()},
                                  extra=PREVENT_EXTRA if prevent_extra else REMOVE_EXTRA),
                           lambda x: t(**{k: v for k, v in x.items()}))
        elif t is datetime:
            return lambda x: datetime.fromisoformat(x)

        return Coerce(t)

    return create_schema(to_type)


def serializable(blob: Any) -> Any:
    return json.loads(json.dumps(blob))
