from __future__ import annotations

import dataclasses
from functools import cached_property
from typing import (
    TYPE_CHECKING,
    Any,
    Generic,
)
from typing_extensions import TypeVar

from .nodes import convert_selections

if TYPE_CHECKING:
    from graphql import GraphQLResolveInfo, OperationDefinitionNode
    from graphql.pyutils.path import Path

    from strawberry.schema import Schema
    from strawberry.types.arguments import StrawberryArgument
    from strawberry.types.field import FieldType, StrawberryField

    from .nodes import Selection

ContextType = TypeVar("ContextType", default=Any)
RootValueType = TypeVar("RootValueType", default=Any)


@dataclasses.dataclass
class Info(Generic[ContextType, RootValueType]):
    """Class containing information about the current execution.

    This class is passed to resolvers when there's an argument with type `Info`.

    Example:
    ```python
    import strawberry


    @strawberry.type
    class Query:
        @strawberry.field
        def hello(self, info: strawberry.Info) -> str:
            return f"Hello, {info.context['name']}!"
    ```

    It also supports passing the type of the context and root types:

    ```python
    import strawberry


    @strawberry.type
    class Query:
        @strawberry.field
        def hello(self, info: strawberry.Info[str, str]) -> str:
            return f"Hello, {info.context}!"
    ```
    """

    _raw_info: GraphQLResolveInfo
    _field: StrawberryField

    def __class_getitem__(cls, types: type | tuple[type, ...]) -> type[Info]:
        """Workaround for when passing only one type.

        Python doesn't yet support directly passing only one type to a generic class
        that has typevars with defaults. This is a workaround for that.

        See:
        https://discuss.python.org/t/passing-only-one-typevar-of-two-when-using-defaults/49134
        """
        if not isinstance(types, tuple):
            types = (types, Any)

        return super().__class_getitem__(types)  # type: ignore

    @property
    def field_name(self) -> str:
        """The name of the current field being resolved."""
        return self._raw_info.field_name

    @property
    def schema(self) -> Schema:
        """The schema of the current execution."""
        return self._raw_info.schema._strawberry_schema  # type: ignore

    @cached_property
    def selected_fields(self) -> list[Selection]:
        """The fields that were selected on the current field's type."""
        info = self._raw_info
        return convert_selections(info, info.field_nodes)

    @property
    def context(self) -> ContextType:
        """The context passed to the query execution."""
        return self._raw_info.context

    @property
    def input_extensions(self) -> dict[str, Any]:
        """The input extensions passed to the query execution."""
        return self._raw_info.operation_extensions  # type: ignore

    @property
    def root_value(self) -> RootValueType:
        """The root value passed to the query execution."""
        return self._raw_info.root_value

    @property
    def variable_values(self) -> dict[str, Any]:
        """The variable values passed to the query execution."""
        return self._raw_info.variable_values

    @property
    def return_type(
        self,
    ) -> FieldType:
        """The return type of the current field being resolved."""
        return self._field.type

    @property
    def python_name(self) -> str:
        """The name of the current field being resolved in Python format."""
        return self._field.python_name

    # TODO: create an abstraction on these fields
    @property
    def operation(self) -> OperationDefinitionNode:
        """The operation being executed."""
        return self._raw_info.operation

    @property
    def query(self) -> str | None:
        """The full GraphQL document string sent in the request."""
        operation = self.operation
        if operation.loc is not None:
            return operation.loc.source.body
        return None

    @property
    def path(self) -> Path:
        """The path of the current field being resolved."""
        return self._raw_info.path

    # TODO: parent_type as strawberry types

    # Helper functions
    def get_argument_definition(self, name: str) -> StrawberryArgument | None:
        """Get the StrawberryArgument definition for the current field by name."""
        try:
            return next(arg for arg in self._field.arguments if arg.python_name == name)
        except StopIteration:
            return None


__all__ = ["Info"]
