import json
import logging
from datetime import datetime
from enum import Enum
from typing import (
    TypeVar,
    List,
    Type,
)


default_logger = logging.getLogger(__name__)


T = TypeVar("T")


def logged_object(cls, logger=None, log_level=logging.INFO):
    return build_wrapper(
        original_cls=cls,
        logger=logger,
        log_level=log_level,
    )


def wrap_types(clss: List[Type], logger=None, log_level=logging.INFO):
    unwrappers = []
    for cls in clss:
        unwrappers.append(
            wrap_type(
                cls=cls,
                logger=logger,
                log_level=log_level,
            )
        )

    def unwrapper():
        for u in unwrappers:
            u()

    return unwrapper()


def wrap_type(cls, logger=None, log_level=logging.INFO):
    wrapper_cls = build_wrapper(
        original_cls=cls,
        logger=logger,
        log_level=log_level,
    )
    cls._original_init = cls.__init__
    cls.__init__ = wrapper_cls.__init__
    cls.__new__ = wrapper_cls.__new__

    def unwrapper():
        cls.__init__ = cls._original_init

    return unwrapper


def build_wrapper(original_cls, logger, log_level):
    class Wrapper(object):
        def __new__(cls, *args, **kwargs):
            wrapper = object.__new__(Wrapper)

            wrapper.inner_object = object.__new__(cls)
            cls._original_init(wrapper.inner_object, *args, **kwargs)

            wrapper.class_name = f"{original_cls.__module__}.{original_cls.__name__}"

            wrapper.logger = logger or default_logger
            wrapper.log_level = log_level

            return wrapper

        def __getattr__(self, item):
            attr = getattr(self.inner_object, item)
            if not callable(attr):
                return attr

            def wrapper(*args, **kwargs):
                self._log_input(attr.__name__, *args, **kwargs)
                ret = attr(*args, **kwargs)
                self._log_output(attr.__name__, ret)
                return ret

            return wrapper

        def _log_input(self, name: str, *args, **kwargs):
            self._log_dict(
                {
                    **self._build_base_dict(
                        operation=name,
                        log_type="parameters"
                    ),
                    "args": self._build_args(*args),
                    "kwargs": self._build_kwargs(**kwargs),
                }
            )

        def _log_output(self, name: str, ret):
            self._log_dict(
                {
                    **self._build_base_dict(
                        operation=name,
                        log_type="return"
                    ),
                    "ret": self._build_representation(ret),
                }
            )

        def _build_base_dict(self, operation: str, log_type: str):
            return {
                "className": self.class_name,
                "operation": operation,
                "type": log_type,
                "time": datetime.utcnow().isoformat(),
            }

        def _log_dict(self, d):
            self.logger.log(self.log_level, json.dumps(d))

        def _build_args(self, *args):
            ret = []
            for arg in args:
                ret.append(self._build_representation(arg))

            return ret

        def _build_kwargs(self, **kwargs):
            ret = {}
            for key, value in kwargs.items():
                ret[key] = self._build_representation(value)

            return ret

        def _build_representation(self, value):
            if isinstance(value, str):
                return value

            if isinstance(value, bytes):
                try:
                    return value.decode("utf-8")
                except UnicodeDecodeError:
                    return value.hex()

            if isinstance(value, Enum):
                return value.name

            if hasattr(value, "__dict__"):
                ret = {}
                for key, value in value.__dict__.items():
                    ret[key] = self._build_representation(value)
                return ret

            if hasattr(value, "__iter__"):
                new_list = []
                for v in value:
                    new_list.append(self._build_representation(v))
                return str(new_list)

            return str(value)

    return Wrapper
