import ast
import dataclasses
import sys
import typing
from collections.abc import AsyncGenerator
from functools import lru_cache
from types import UnionType
from typing import (  # type: ignore
    Annotated,
    Any,
    ClassVar,
    ForwardRef,
    Generic,
    TypeGuard,
    TypeVar,
    Union,
    _eval_type,
    _GenericAlias,
    _SpecialForm,
    cast,
    get_args,
    get_origin,
)


@lru_cache
def get_generic_alias(type_: type) -> type:
    """Get the generic alias for a type.

    Given a type, its generic alias from `typing` module will be returned
    if it exists. For example:

    ```python
    get_generic_alias(list)
    # typing.List

    get_generic_alias(dict)
    # typing.Dict
    ```

    This is mostly useful for python versions prior to 3.9, to get a version
    of a concrete type which supports `__class_getitem__`. In 3.9+ types like
    `list`/`dict`/etc are subscriptable and can be used directly instead
    of their generic alias version.
    """
    if isinstance(type_, _SpecialForm):
        return type_

    for attr_name in dir(typing):
        # ignore private attributes, they are not Generic aliases
        if attr_name.startswith("_"):  # pragma: no cover
            continue

        attr = getattr(typing, attr_name)
        if is_generic_alias(attr) and attr.__origin__ is type_:
            return attr

    raise AssertionError(f"No GenericAlias available for {type_}")  # pragma: no cover


def is_generic_alias(type_: Any) -> TypeGuard[_GenericAlias]:
    """Returns True if the type is a generic alias."""
    # _GenericAlias overrides all the methods that we can use to know if
    # this is a subclass of it. But if it has an "_inst" attribute
    # then it for sure is a _GenericAlias
    return hasattr(type_, "_inst")


def is_list(annotation: object) -> bool:
    """Returns True if annotation is a List."""
    annotation_origin = getattr(annotation, "__origin__", None)

    return annotation_origin is list


def is_union(annotation: object) -> bool:
    """Returns True if annotation is a Union."""
    # this check is needed because unions declared with the new syntax `A | B`
    # don't have a `__origin__` property on them, but they are instances of
    if isinstance(annotation, UnionType):
        return True

    # unions declared as Union[A, B] fall through to this check, even on python 3.10+

    annotation_origin = getattr(annotation, "__origin__", None)

    return annotation_origin == Union


def is_optional(annotation: type) -> bool:
    """Returns True if the annotation is Optional[SomeType]."""
    # Optionals are represented as unions

    if not is_union(annotation):
        return False

    types = annotation.__args__  # type: ignore[attr-defined]

    # A Union to be optional needs to have at least one None type
    return any(x == None.__class__ for x in types)


def get_optional_annotation(annotation: type) -> type:
    types = annotation.__args__  # type: ignore[attr-defined]

    non_none_types = tuple(x for x in types if x != None.__class__)

    # if we have multiple non none types we want to return a copy of this
    # type (normally a Union type).
    if len(non_none_types) > 1:
        return Union[non_none_types]  # type: ignore  # noqa: UP007

    return non_none_types[0]


def get_list_annotation(annotation: type) -> type:
    return annotation.__args__[0]  # type: ignore[attr-defined]


def is_concrete_generic(annotation: type) -> bool:
    ignored_generics = (list, tuple, Union, ClassVar, AsyncGenerator)
    return (
        isinstance(annotation, _GenericAlias)
        and annotation.__origin__ not in ignored_generics
    )


def is_generic_subclass(annotation: type) -> bool:
    return isinstance(annotation, type) and issubclass(annotation, Generic)


def is_generic(annotation: type) -> bool:
    """Returns True if the annotation is or extends a generic."""
    return (
        # TODO: These two lines appear to have the same effect. When will an
        #       annotation have parameters but not satisfy the first condition?
        (is_generic_subclass(annotation) or is_concrete_generic(annotation))
        and bool(get_parameters(annotation))
    )


def is_type_var(annotation: type) -> bool:
    """Returns True if the annotation is a TypeVar."""
    return isinstance(annotation, TypeVar)


def is_classvar(cls: type, annotation: ForwardRef | str) -> bool:
    """Returns True if the annotation is a ClassVar."""
    # This code was copied from the dataclassses cpython implementation to check
    # if a field is annotated with ClassVar or not, taking future annotations
    # in consideration.
    if dataclasses._is_classvar(annotation, typing):  # type: ignore
        return True

    annotation_str = (
        annotation.__forward_arg__ if isinstance(annotation, ForwardRef) else annotation
    )
    return isinstance(annotation_str, str) and dataclasses._is_type(  # type: ignore
        annotation_str,
        cls,
        typing,
        typing.ClassVar,
        dataclasses._is_classvar,  # type: ignore
    )


def type_has_annotation(type_: object, annotation: type) -> bool:
    """Returns True if the type_ has been annotated with annotation."""
    if get_origin(type_) is Annotated:
        return any(isinstance(argument, annotation) for argument in get_args(type_))

    return False


def get_parameters(annotation: type) -> tuple[object] | tuple[()]:
    if isinstance(annotation, _GenericAlias) or (
        isinstance(annotation, type)
        and issubclass(annotation, Generic)
        and annotation is not Generic
    ):
        return annotation.__parameters__  # type: ignore[union-attr]
    return ()  # pragma: no cover


def _get_namespace_from_ast(
    expr: ast.Expr | ast.expr,
    globalns: dict | None = None,
    localns: dict | None = None,
) -> dict[str, Any]:
    from strawberry.types.lazy_type import StrawberryLazyReference

    extra: dict[str, Any] = {}

    if isinstance(expr, ast.Expr) and isinstance(
        expr.value, (ast.BinOp, ast.Subscript)
    ):
        extra.update(_get_namespace_from_ast(expr.value, globalns, localns))
    elif isinstance(expr, ast.BinOp):
        for elt in (expr.left, expr.right):
            extra.update(_get_namespace_from_ast(elt, globalns, localns))
    elif (
        isinstance(expr, ast.Subscript)
        and isinstance(expr.value, ast.Name)
        and expr.value.id == "Union"
    ):
        if hasattr(ast, "Index") and isinstance(expr.slice, ast.Index):
            expr_slice = cast("Any", expr.slice).value
        else:
            expr_slice = expr.slice

        for elt in cast("ast.Tuple", expr_slice).elts:
            extra.update(_get_namespace_from_ast(elt, globalns, localns))
    elif (
        isinstance(expr, ast.Subscript)
        and isinstance(expr.value, ast.Name)
        and expr.value.id in {"list", "List"}
    ):
        extra.update(_get_namespace_from_ast(expr.slice, globalns, localns))
    elif (
        isinstance(expr, ast.Subscript)
        and isinstance(expr.value, ast.Name)
        and expr.value.id == "Annotated"
    ):
        if hasattr(ast, "Index") and isinstance(expr.slice, ast.Index):
            expr_slice = cast("Any", expr.slice).value
        else:
            expr_slice = expr.slice

        args: list[str] = []
        for elt in cast("ast.Tuple", expr_slice).elts:
            extra.update(_get_namespace_from_ast(elt, globalns, localns))
            args.append(ast.unparse(elt))

        # When using forward refs, the whole
        # Annotated[SomeType, strawberry.lazy("type.module")] is a forward ref,
        # and trying to _eval_type on it will fail. Take a different approach
        # here to resolve lazy types by execing the annotated args, resolving the
        # type directly and then adding it to extra namespace, so that _eval_type
        # can properly resolve it later
        type_name = args[0].strip(" '\"\n")
        already_resolved = (globalns and type_name in globalns) or (
            localns and type_name in localns
        )
        for arg in args[1:]:
            evaled_arg = eval(arg, globalns, localns)  # noqa: S307
            if isinstance(evaled_arg, StrawberryLazyReference) and not already_resolved:
                extra[type_name] = evaled_arg.resolve_forward_ref(ForwardRef(type_name))

    elif isinstance(expr, ast.Subscript):
        if hasattr(ast, "Index") and isinstance(expr.slice, ast.Index):
            expr_slice = cast("Any", expr.slice).value
        else:
            expr_slice = expr.slice

        if isinstance(expr_slice, ast.Tuple):
            for elt in expr_slice.elts:
                extra.update(_get_namespace_from_ast(elt, globalns, localns))
        else:
            extra.update(_get_namespace_from_ast(expr_slice, globalns, localns))

    return extra


def eval_type(
    type_: Any,
    globalns: dict | None = None,
    localns: dict | None = None,
) -> type:
    """Evaluates a type, resolving forward references."""
    from strawberry.parent import StrawberryParent
    from strawberry.types.auto import StrawberryAuto
    from strawberry.types.lazy_type import StrawberryLazyReference
    from strawberry.types.private import StrawberryPrivate

    globalns = globalns or {}

    # If this is not a string, maybe its args are (e.g. list["Foo"])
    if isinstance(type_, ForwardRef):
        ast_obj = cast("ast.Expr", ast.parse(type_.__forward_arg__).body[0])

        globalns.update(_get_namespace_from_ast(ast_obj, globalns, localns))

        type_ = ForwardRef(ast.unparse(ast_obj))

        extra: dict[str, Any] = {}

        if sys.version_info >= (3, 13):
            extra = {"type_params": None}

        return _eval_type(type_, globalns, localns, **extra)

    origin = get_origin(type_)
    if origin is not None:
        args = get_args(type_)
        if origin is Annotated:
            for arg in args[1:]:
                if isinstance(arg, StrawberryPrivate):
                    return type_

                if isinstance(arg, StrawberryLazyReference):
                    remaining_args = [
                        a
                        for a in args[1:]
                        if not isinstance(a, StrawberryLazyReference)
                    ]
                    type_arg = (
                        arg.resolve_forward_ref(args[0])
                        if isinstance(args[0], ForwardRef)
                        else args[0]
                    )
                    args = (type_arg, *remaining_args)
                    break

                if isinstance(arg, StrawberryAuto):
                    remaining_args = [
                        a for a in args[1:] if not isinstance(a, StrawberryAuto)
                    ]
                    args = (args[0], arg, *remaining_args)
                    break

                if isinstance(arg, StrawberryParent):
                    remaining_args = [
                        a for a in args[1:] if not isinstance(a, StrawberryParent)
                    ]
                    try:
                        type_arg = (
                            eval_type(args[0], globalns, localns)
                            if isinstance(args[0], ForwardRef)
                            else args[0]
                        )
                    except (NameError, TypeError):
                        type_arg = args[0]
                    args = (type_arg, arg, *remaining_args)
                    break

            # If we have only a StrawberryLazyReference and no more annotations,
            # we need to return the argument directly because Annotated
            # will raise an error if trying to instantiate it with only
            # one argument.
            if len(args) == 1:
                return args[0]

        # python 3.10 will return UnionType for origin, and it cannot be
        # subscripted like Union[Foo, Bar]
        if origin is UnionType:
            origin = Union

        type_ = (
            origin[tuple(eval_type(a, globalns, localns) for a in args)]
            if args
            else origin
        )

    return type_


__all__ = [
    "eval_type",
    "get_generic_alias",
    "get_list_annotation",
    "get_optional_annotation",
    "get_parameters",
    "is_classvar",
    "is_concrete_generic",
    "is_generic",
    "is_generic_alias",
    "is_generic_subclass",
    "is_list",
    "is_optional",
    "is_type_var",
    "is_union",
    "type_has_annotation",
]
