#  Copyright (C) 2023
#  ABM, Moscow
#
#  UNPUBLISHED PROPRIETARY MATERIAL.
#  ALL RIGHTS RESERVED.
#
#  Authors: Mike Orlov <m.orlov@abm-jsc.ru>
import dataclasses
import itertools
import types
import typing
from typing import Any, Type, Callable

from more_itertools import first
from no_value import NoValue
from type_cast import is_instance, get_dataclass_field_name_to_field

from .defs import Json
from .typed_In_dumps import TypedInDumps

UntypedValueWrapper = type[TypedInDumps] | Callable[[dict | list | str | float | int | bool | None], Any]


class TypedJsonParser:
    def __init__(self, untyped_value_wrapper: UntypedValueWrapper | None = None, skip_unknown_t: bool = False):
        self._untyped_value_wrapper = (lambda x: x) if untyped_value_wrapper is None else untyped_value_wrapper
        self._wrap_literal = self._prepare_wrap_literal()
        self.skip_unknown_t = skip_unknown_t

    def _prepare_wrap_literal(self) -> Callable[[dict | list | str | float | int | bool | None], Any]:
        if not isinstance(self._untyped_value_wrapper, type):
            return self._untyped_value_wrapper
        if not issubclass(self._untyped_value_wrapper, TypedInDumps):
            return self._untyped_value_wrapper
        # todo: assert is_dataclass_type(self._untyped_value_wrapper)
        wrapper_field_name_to_field = get_dataclass_field_name_to_field(self._untyped_value_wrapper)
        wrapper_field_name_to_field = {name: field for name, field in wrapper_field_name_to_field.items() if field.init}
        if len(wrapper_field_name_to_field) != 1:
            wrapper_field_name_to_field = {
                name: field for name, field in wrapper_field_name_to_field.items()
                if field.default == dataclasses.MISSING and field.default_factory == dataclasses.MISSING
            }

        if len(wrapper_field_name_to_field) != 1:
            raise TypeError(
                "Cannot figure out how to pass args to literal_type_wrapper: "
                "it should have one field with init=True and no defaults")

        field_name = first(wrapper_field_name_to_field)
        return lambda x: self._untyped_value_wrapper(**{field_name: x})

    def _wrap_literal(
            self,
            value: dict[str, TypedInDumps] | list[TypedInDumps] | str | float | int | bool | None
    ) -> Any:
        pass

    def parse(self, value: Any, target_type: Type[TypedInDumps | Json] | types.UnionType = TypedInDumps):
        if isinstance(target_type, (type, types.UnionType)) or getattr(target_type, '__origin__', None) is typing.Union:
            if is_instance(value, [target_type]):
                return value

        if target_type is self._untyped_value_wrapper:
            if not isinstance(value, TypedInDumps):
                value = self.parse(
                    value, dict[str, TypedInDumps] | list[TypedInDumps] | str | float | int | bool | None)
            return self._wrap_literal(value)

        if isinstance(target_type, type) and issubclass(target_type, TypedInDumps):
            if isinstance(value, target_type):
                return value
            prefixed_name_to_non_abstract_type = target_type.get_prefixed_name_to_subclass()
            if isinstance(value, dict) and '_t' in value:
                type_name = value.get('_t')
                try:
                    type_ = prefixed_name_to_non_abstract_type[type_name]  # TODO add some kind of skip for unknown
                except (KeyError, TypeError) as e:
                    if not self.skip_unknown_t:
                        raise KeyError(
                            f"required {target_type.__name__}({list(prefixed_name_to_non_abstract_type)}), "
                            f"got: {type_name}") from e
                else:
                    kwargs = {}
                    for key, field in get_dataclass_field_name_to_field(type_).items():
                        val = value.get(key, NoValue) if field.init else NoValue
                        if val is NoValue:
                            if dataclasses.is_dataclass(field.type):
                                val = self.parse({}, field.type)
                        else:
                            val = self.parse(val, field.type)
                        if val is not NoValue:
                            kwargs[key] = val
                    return type_(**kwargs)

            result = self.parse(value, self._untyped_value_wrapper)
            if not issubclass(type(result), target_type):
                raise ValueError(
                    f"Required {target_type.__name__}({list(prefixed_name_to_non_abstract_type)}), "
                    f"got untyped {type(value).__name__}, applied untyped_wrapper, got: {type(result).__name__}"
                )
            return result

        target_type_origin = getattr(target_type, '__origin__', None)
        target_type_args = getattr(target_type, '__args__', None)

        if isinstance(target_type, types.UnionType):
            if is_instance(value, target_type_args):
                return value
            possible_type_to_exception_description = {}
            for possible_type in target_type_args:
                try:
                    return self.parse(value, possible_type)
                except (KeyError, ValueError, TypeError, AssertionError) as e:
                    description = list(e.args)
                    if len(description) == 1:
                        description = first(description)
                    if not description:
                        description = type(e).__name__
                    possible_type_to_exception_description[str(possible_type)] = description
                    continue
            raise ValueError(f"failed all attempts to cast {value!r}", possible_type_to_exception_description)

        if target_type_origin is list:
            assert isinstance(value, (list, tuple)), f"Value is not a list or tuple: {type(value).__name__}"
            assert len(target_type_args) == 1, f"Bad target type: list must have exactly one type. Contact developer"
            return [self.parse(val, target_type_args[0]) for val in value]

        if target_type_origin is tuple:
            assert isinstance(value, (list, tuple)), f"Value is not a list or tuple: {type(value).__name__}"
            assert target_type_args, f"Bad target type 'tuple': required type args, use 'tuple[...]' to allow anything"
            if target_type_args[-1] is Ellipsis:
                if len(target_type_args) == 1:
                    return tuple(value)
                target_type_args = itertools.cycle(target_type_args[:-1])
            elif len(target_type_args) != len(value):
                raise ValueError(f"Required tuple with {len(target_type_args)} elements, got {len(value)}")
            return tuple(self.parse(val, target_type_) for val, target_type_ in zip(value, target_type_args))

        if target_type_origin is dict:
            assert isinstance(value, dict), f"Value is not a dict: {type(value).__name__}"
            assert len(target_type_args) == 2, f"Bad target type: list must have exactly two types. Contact developer"
            key_type, value_type = target_type_args
            return {self.parse(key, key_type): self.parse(val, value_type) for key, val in value.items()}

        if target_type in (str, bool) and not isinstance(value, target_type):
            raise ValueError(f"Value is not a {target_type.__name__}: {type(value).__name__}")
        return target_type(value)
