import collections.abc
import itertools
import types
from typing import Any, Iterable, Union, Sequence, get_args, get_origin

from init_helpers.to_list import to_list


def get_container_type(type_origin) -> type:
    if not getattr(type_origin, "__abstractmethods__", None):
        return type_origin
    container_type = ORIGIN_TO_CONTAINER_TYPE.get(type_origin)
    if container_type is None:
        raise TypeError(f'Attempt to get_container_type from unexpected origin: {type_origin}')

    return container_type


def is_tuple(value: Any, tuple_args: Iterable[Union[type, types.GenericAlias]]) -> bool:
    if not isinstance(value, tuple):
        return False

    has_ellipsis = False
    type_checks = []
    for inner_type in tuple_args:
        if inner_type is ...:
            has_ellipsis = True
        else:
            if has_ellipsis:
                raise TypeError("Ellipsis (three dots (...)) should be the last"
                                " in tuple to make it variable-length")
            type_checks.append(inner_type)
    if has_ellipsis:
        type_checks = itertools.cycle(type_checks)
    if not has_ellipsis and type_checks:
        if len(type_checks) != len(value):
            return False

    for val, inner_type in zip(value, type_checks):
        if not is_instance(val, [inner_type]):
            return False

    return True


def is_mapping(value: Any, mapping_args: Sequence[Union[type, types.GenericAlias]]) -> bool:
    if not isinstance(value, collections.abc.Mapping):
        return False

    if not mapping_args:
        return True

    if len(mapping_args) != 2:
        raise TypeError(f"Mapping requires exactly 2 type args, got: {mapping_args}")

    key_type_list = [mapping_args[0]]
    value_type_list = [mapping_args[1]]

    for key, val in value.items():
        if not is_instance(key, key_type_list) or not is_instance(val, value_type_list):
            return False
    return True


def is_collection(value: Any, collection_args: Sequence[Union[type, types.GenericAlias]]) -> bool:
    if not isinstance(value, collections.abc.Collection):
        return False

    if not collection_args:
        return True

    if len(collection_args) != 1:
        raise TypeError(f"Collection requires exactly 2 type args, got: {collection_args}")

    collection_type_list = [collection_args[0]]

    for val in value:
        if not is_instance(val, collection_type_list):
            return False
    return True


def is_instance(
        val: Any,
        possible_types: type | types.GenericAlias | types.UnionType | Iterable[
                        type | types.GenericAlias | types.UnionType]
) -> bool:
    for type_ in to_list(possible_types):
        if type_ == Any:
            return True

        type_origin = get_origin(type_)
        type_args = get_args(type_)
        if not type_origin and isinstance(type_, type):  # basic check
            if isinstance(val, type_):
                return True
        elif type_origin is Union or isinstance(type_, types.UnionType):
            if is_instance(val, type_args):
                return True
        else:
            container_type = get_container_type(type_origin)
            if container_type is None or not isinstance(val, container_type):
                continue

            if issubclass(container_type, collections.abc.Mapping):
                if is_mapping(val, type_args):
                    return True

            elif container_type is tuple:  # tuple has specific typing behaviour
                if is_tuple(val, type_args):
                    return True

            elif issubclass(container_type, collections.abc.Collection):
                if is_collection(val, type_args):
                    return True
    return False


ORIGIN_TO_CONTAINER_TYPE: dict[collections.abc.Container, type] = {
    collections.abc.Mapping: dict,
    collections.abc.Collection: list,
    collections.abc.Set: set,
}
