from __future__ import annotations

import asyncio
import warnings
from asyncio import ensure_future
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Iterable
from functools import lru_cache
from inspect import isawaitable
from typing import (
    TYPE_CHECKING,
    Any,
    NamedTuple,
    cast,
)

from graphql import ExecutionResult as GraphQLExecutionResult
from graphql import (
    ExecutionResult as OriginalExecutionResult,
)
from graphql import (
    FieldNode,
    FragmentDefinitionNode,
    GraphQLBoolean,
    GraphQLDirective,
    GraphQLError,
    GraphQLField,
    GraphQLNamedType,
    GraphQLNonNull,
    GraphQLObjectType,
    GraphQLOutputType,
    GraphQLSchema,
    OperationDefinitionNode,
    get_introspection_query,
    parse,
    validate_schema,
)
from graphql.execution.middleware import MiddlewareManager
from graphql.type.directives import specified_directives
from graphql.validation import validate

from strawberry import relay
from strawberry.annotation import StrawberryAnnotation
from strawberry.exceptions import MissingQueryError
from strawberry.execution import optimized_is_awaitable
from strawberry.extensions import SchemaExtension
from strawberry.extensions.directives import (
    DirectivesExtension,
    DirectivesExtensionSync,
)
from strawberry.extensions.runner import SchemaExtensionsRunner
from strawberry.printer import print_schema
from strawberry.schema.schema_converter import GraphQLCoreConverter
from strawberry.schema.validation_rules.maybe_null import MaybeNullValidationRule
from strawberry.schema.validation_rules.one_of import OneOfInputValidationRule
from strawberry.types.base import (
    StrawberryObjectDefinition,
    WithStrawberryObjectDefinition,
    has_object_definition,
)
from strawberry.types.execution import (
    ExecutionContext,
    ExecutionResult,
    PreExecutionError,
)
from strawberry.types.graphql import OperationType
from strawberry.utils import IS_GQL_32, IS_GQL_33
from strawberry.utils.aio import aclosing
from strawberry.utils.await_maybe import await_maybe

from . import compat
from ._graphql_core import (
    GraphQLExecutionContext,
    GraphQLIncrementalExecutionResults,
    GraphQLIncrementalResult,
    ResultType,
    execute,
    execution_context_class_kwargs,
    experimental_execute_incrementally,
    incremental_execution_directives,
    subscribe,
)
from .base import BaseSchema
from .config import StrawberryConfig
from .directive_collector import SchemaDirectiveCollector
from .exceptions import CannotGetOperationTypeError, InvalidOperationTypeError

if TYPE_CHECKING:
    from collections.abc import Iterable, Mapping
    from typing import TypeAlias

    from graphql.language import DocumentNode
    from graphql.pyutils import Path
    from graphql.type import GraphQLResolveInfo
    from graphql.validation import ASTValidationRule

    from strawberry.directive import StrawberryDirective
    from strawberry.schema.exception_handlers import ExceptionHandler
    from strawberry.types.base import StrawberryType
    from strawberry.types.enum import StrawberryEnumDefinition
    from strawberry.types.field import StrawberryField
    from strawberry.types.scalar import ScalarDefinition, ScalarWrapper
    from strawberry.types.union import StrawberryUnion


SubscriptionResult: TypeAlias = AsyncGenerator[
    PreExecutionError | ExecutionResult, None
]

# Unlike a pure subscription, ``stream`` can also run queries and mutations, and
# expands incremental delivery (``@defer``/``@stream``) into its initial result
# followed by raw graphql-core patch frames. Its element type is therefore wider
# than ``SubscriptionResult``.
StreamResult: TypeAlias = AsyncGenerator[
    PreExecutionError | ExecutionResult | GraphQLIncrementalResult, None
]

OriginSubscriptionResult: TypeAlias = (
    OriginalExecutionResult | AsyncIterator[OriginalExecutionResult]
)


DEFAULT_ALLOWED_OPERATION_TYPES = {
    OperationType.QUERY,
    OperationType.MUTATION,
    OperationType.SUBSCRIPTION,
}
ProcessErrors: TypeAlias = (
    "Callable[[list[GraphQLError], ExecutionContext | None], None]"
)
_DirectiveEntry: TypeAlias = tuple[GraphQLDirective, object, str]


def _register_graphql_directive(
    registry: dict[str, _DirectiveEntry],
    directive: GraphQLDirective,
    definition: object,
    source: str,
) -> None:
    """Add ``directive`` to ``registry`` unless its name is already taken.

    Registering the same Strawberry definition twice is harmless; two distinct
    definitions sharing a GraphQL name raise an error naming both sources.
    """
    if (existing := registry.get(directive.name)) is None:
        registry[directive.name] = (directive, definition, source)
        return

    _, existing_definition, existing_source = existing
    if existing_definition is definition:
        return

    raise ValueError(
        f"Schema directive '@{directive.name}' is defined by both "
        f"{existing_source} and {source}. Use a different "
        "GraphQL name for one of them."
    )


def _is_specified_one_of_directive(
    directive: GraphQLDirective, registry: dict[str, _DirectiveEntry]
) -> bool:
    """Whether ``directive`` is a compatible spelling of graphql-core's @oneOf.

    Strawberry's OneOf schema directive (and user-defined ones) predate
    graphql-core exposing the specified @oneOf directive. A compatible
    definition stands in for the built-in regardless of the Python class used.
    Legacy definitions normally have no description; a supplied description is
    only accepted when it matches the specified one so custom metadata is not
    silently discarded.
    """
    if directive.name != "oneOf" or (existing := registry.get("oneOf")) is None:
        return False

    specified, definition, _ = existing
    return (
        specified is definition
        and directive.locations == specified.locations
        and directive.args.keys() == specified.args.keys()
        and directive.is_repeatable == specified.is_repeatable
        and directive.description in (None, specified.description)
    )


# TODO: merge with below
def validate_document(
    schema: GraphQLSchema,
    document: DocumentNode,
    validation_rules: tuple[type[ASTValidationRule], ...],
) -> list[GraphQLError]:
    validation_rules = (
        *validation_rules,
        MaybeNullValidationRule,
        OneOfInputValidationRule,
    )
    return validate(
        schema,
        document,
        validation_rules,
    )


def _run_validation(execution_context: ExecutionContext) -> None:
    # Check if there are any validation rules or if validation has
    # already been run by an extension
    if (
        len(execution_context.validation_rules) > 0
        and execution_context.pre_execution_errors is None
    ):
        assert execution_context.graphql_document
        execution_context.pre_execution_errors = validate_document(
            execution_context.schema._schema,
            execution_context.graphql_document,
            execution_context.validation_rules,
        )


def _parse_operation_document(execution_context: ExecutionContext) -> None:
    if execution_context.graphql_document is not None:
        return

    if not execution_context.query:
        raise MissingQueryError

    execution_context.graphql_document = parse(
        execution_context.query,
        **execution_context.parse_options,
    )


def _coerce_error(error: GraphQLError | Exception) -> GraphQLError:
    if isinstance(error, GraphQLError):
        return error
    return GraphQLError(str(error), original_error=error)


def _coerce_pre_execution_error(error: Exception) -> GraphQLError:
    if isinstance(error, CannotGetOperationTypeError):
        return GraphQLError(error.as_http_error_reason(), original_error=error)

    if isinstance(error, InvalidOperationTypeError):
        operation_type = {
            OperationType.QUERY: "queries",
            OperationType.MUTATION: "mutations",
            OperationType.SUBSCRIPTION: "subscriptions",
        }[error.operation_type]
        return GraphQLError(f"{operation_type} are not allowed", original_error=error)

    return _coerce_error(error)


class _OperationContextAwareGraphQLResolveInfo(NamedTuple):  # pyright: ignore
    field_name: str
    field_nodes: list[FieldNode]
    return_type: GraphQLOutputType
    parent_type: GraphQLObjectType
    path: Path
    schema: GraphQLSchema
    fragments: dict[str, FragmentDefinitionNode]
    root_value: Any
    operation: OperationDefinitionNode
    variable_values: dict[str, Any]
    context: Any
    is_awaitable: Callable[[Any], bool]
    abort_signal: Any
    async_helpers: Any
    operation_extensions: dict[str, Any]


class StrawberryGraphQLCoreExecutionContext(GraphQLExecutionContext):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        operation_extensions = kwargs.pop("operation_extensions", None)

        super().__init__(*args, **kwargs)

        self.operation_extensions = operation_extensions

    if IS_GQL_33:

        def build_resolve_info(
            self,
            field_def: GraphQLField,
            field_nodes: list[FieldNode],
            parent_type: GraphQLObjectType,
            path: Path,
        ) -> GraphQLResolveInfo:
            fragments = getattr(self, "fragment_definitions", self.fragments)
            variable_values = getattr(
                self.variable_values, "coerced", self.variable_values
            )

            return _OperationContextAwareGraphQLResolveInfo(  # type: ignore
                field_nodes[0].name.value,
                field_nodes,
                field_def.type,
                parent_type,
                path,
                self.schema,
                fragments,
                self.root_value,
                self.operation,
                variable_values,
                self.context_value,
                self.is_awaitable,
                getattr(self, "abort_signal", None),
                getattr(self, "async_helpers", None),
                self.operation_extensions,
            )


class Schema(BaseSchema):
    def __init__(  # noqa: PLR0917
        self,
        # TODO: can we make sure we only allow to pass
        # something that has been decorated?
        query: type,
        mutation: type | None = None,
        subscription: type | None = None,
        directives: Iterable[StrawberryDirective] = (),
        types: Iterable[type | StrawberryType] = (),
        extensions: Iterable[
            type[SchemaExtension] | Callable[[], SchemaExtension]
        ] = (),
        execution_context_class: type[GraphQLExecutionContext] | None = None,
        config: StrawberryConfig | None = None,
        scalar_overrides: (
            Mapping[object, type | ScalarWrapper | ScalarDefinition] | None
        ) = None,
        schema_directives: Iterable[object] = (),
        exception_handlers: Iterable[ExceptionHandler[Any]] = (),
    ) -> None:
        """Default Schema to be used in a Strawberry application.

        A GraphQL Schema class used to define the structure and configuration
        of GraphQL queries, mutations, and subscriptions.

        This class allows the creation of a GraphQL schema by specifying the types
        for queries, mutations, and subscriptions, along with various configuration
        options such as directives, extensions, and scalar overrides.

        Args:
            query: The entry point for queries.
            mutation: The entry point for mutations.
            subscription: The entry point for subscriptions.
            directives: A list of operation directives that clients can use.
                The built-in `@include` and `@skip` are included by default.
            types: A list of additional types that will be included in the schema.
            extensions: A list of Strawberry extensions.
            execution_context_class: The execution context class.
            config: The configuration for the schema.
            scalar_overrides: A dictionary of overrides for scalars.
            schema_directives: A list of schema directives for the schema.
            exception_handlers: A list of handlers that can convert Python
                exceptions into explicit GraphQL union return values.

        Example:
        ```python
        import strawberry


        @strawberry.type
        class Query:
            name: str = "Patrick"


        schema = strawberry.Schema(query=Query)
        ```
        """
        self.query = query
        self.mutation = mutation
        self.subscription = subscription

        # Materialize once so generators / one-shot iterables work on later
        # ``get_extensions`` calls and so the deprecation check below doesn't
        # consume the iterable.
        self.extensions = tuple(extensions)
        if any(isinstance(ext, SchemaExtension) for ext in self.extensions):
            warnings.warn(
                (
                    "Passing an extension instance to `extensions=[...]` is "
                    "deprecated and will be removed in a future release. "
                    "Pass the class itself, or a factory callable "
                    "(e.g. `lambda: MyExtension(arg=...)`), so that a fresh "
                    "extension is constructed for each request. See "
                    "https://github.com/strawberry-graphql/strawberry/issues/4369."
                ),
                DeprecationWarning,
                stacklevel=2,
            )
        self.execution_context_class = (
            execution_context_class or StrawberryGraphQLCoreExecutionContext
        )
        self.config = config or StrawberryConfig()
        self.exception_handlers = tuple(exception_handlers)

        self.schema_converter = GraphQLCoreConverter(
            self.config,
            scalar_overrides=scalar_overrides or {},  # type: ignore
            scalar_map=self.config.scalar_map,
            get_fields=self.get_fields,
            exception_handlers=self.exception_handlers,
        )

        self.directives = tuple(directives)
        self.schema_directives = list(schema_directives)

        query_type = self.schema_converter.from_object(
            cast(
                "type[WithStrawberryObjectDefinition]", query
            ).__strawberry_definition__
        )
        mutation_type = (
            self.schema_converter.from_object(
                cast(
                    "type[WithStrawberryObjectDefinition]", mutation
                ).__strawberry_definition__
            )
            if mutation
            else None
        )
        subscription_type = (
            self.schema_converter.from_object(
                cast(
                    "type[WithStrawberryObjectDefinition]", subscription
                ).__strawberry_definition__
            )
            if subscription
            else None
        )

        graphql_types: list[GraphQLNamedType] = []
        explicit_directive_types: list[type] = []
        for type_ in types:
            if compat.is_schema_directive(type_):
                explicit_directive_types.append(type_)
            else:
                if (
                    has_object_definition(type_)
                    and type_.__strawberry_definition__.is_graphql_generic
                ):
                    type_ = StrawberryAnnotation(type_).resolve()  # noqa: PLW2901
                graphql_type = self.schema_converter.from_maybe_optional(type_)
                if isinstance(graphql_type, GraphQLNonNull):
                    graphql_type = graphql_type.of_type
                if not isinstance(graphql_type, GraphQLNamedType):
                    raise TypeError(f"{graphql_type} is not a named GraphQL Type")
                graphql_types.append(graphql_type)

        try:
            # graphql-core adds the argument types of every directive passed to
            # GraphQLSchema to its type map. It cannot, however, discover
            # directives stored only in Strawberry definitions. Collect and
            # convert those directives first so the one GraphQLSchema we build
            # receives the complete directive and type graph.
            schema_directive_types = self._collect_schema_directives(
                explicit_directive_types,
                [query_type, mutation_type, subscription_type, *graphql_types],
            )
            self._schema = GraphQLSchema(
                query=query_type,
                mutation=mutation_type,
                subscription=subscription_type,
                directives=self._collect_graphql_directives(schema_directive_types),
                types=graphql_types,
                extensions={
                    GraphQLCoreConverter.DEFINITION_BACKREF: self,
                },
            )

        except TypeError as error:
            # GraphQL core throws a TypeError if there's any exception raised
            # during the schema creation, so we check if the cause was a
            # StrawberryError and raise it instead if that's the case.

            from strawberry.exceptions import StrawberryException

            if isinstance(error.__cause__, StrawberryException):
                raise error.__cause__ from None

            raise

        # attach our schema to the GraphQL schema instance
        self._schema._strawberry_schema = self  # type: ignore

        self._warn_for_federation_directives()
        self._resolve_node_ids()
        self._extend_introspection()

        # Validate schema early because we want developers to know about
        # possible issues as soon as possible
        errors = validate_schema(self._schema)
        if errors:
            formatted_errors = "\n\n".join(f"❌ {error.message}" for error in errors)
            raise ValueError(f"Invalid Schema. Errors:\n\n{formatted_errors}")

    def _collect_schema_directives(
        self,
        explicit_directive_types: Iterable[type],
        graphql_types: Iterable[GraphQLNamedType | None],
    ) -> list[type]:
        collector = SchemaDirectiveCollector(self.schema_converter)
        for directive_type in explicit_directive_types:
            collector.add_directive_type(directive_type)
        collector.add_schema_directives(self.schema_directives)
        collector.add_graphql_types(graphql_types)
        collector.collect()

        # Federation reads the applications found so far to generate @link and
        # @composeDirective, appending them to ``schema_directives``. Fold what
        # it added into the same collection so the single GraphQLSchema built
        # from it is validated with every generated directive and support type.
        self._schema_directives_in_use = collector.directives_in_use
        prepared_count = len(self.schema_directives)
        self._prepare_schema_directives()
        collector.add_schema_directives(self.schema_directives[prepared_count:])
        collector.collect()

        self._schema_graphql_directives = collector.graphql_directives
        return collector.directive_types

    def _collect_graphql_directives(
        self, schema_directive_types: Iterable[type]
    ) -> tuple[GraphQLDirective, ...]:
        # GraphQLSchema expects its directive collection to be canonical. Seed
        # the registry with the specified directives so a custom definition
        # cannot silently replace a built-in, then keep one insertion-ordered
        # entry per GraphQL name.
        registry: dict[str, _DirectiveEntry] = {
            directive.name: (directive, directive, "the built-in GraphQL directive")
            for directive in specified_directives
        }

        for directive in self.directives:
            _register_graphql_directive(
                registry,
                self.schema_converter.from_directive(directive),
                directive,
                f"operation directive '{directive.python_name}'",
            )

        for directive_type in schema_directive_types:
            graphql_directive = self._schema_graphql_directives[directive_type]
            if _is_specified_one_of_directive(graphql_directive, registry):
                continue

            _register_graphql_directive(
                registry,
                graphql_directive,
                cast("Any", directive_type).__strawberry_directive__,
                (
                    "schema directive "
                    f"'{directive_type.__module__}.{directive_type.__qualname__}'"
                ),
            )

        if self.config.enable_experimental_incremental_execution:
            for directive in incremental_execution_directives:
                _register_graphql_directive(
                    registry,
                    directive,
                    directive,
                    f"the experimental GraphQL directive '@{directive.name}'",
                )

        return tuple(directive for directive, _, _ in registry.values())

    def get_extensions(self, sync: bool = False) -> list[SchemaExtension]:
        # Deprecated instances are passed through as-is. The DeprecationWarning
        # is emitted once at ``Schema.__init__``; users are expected to migrate
        # to a class or factory for per-request isolation.
        resolved: list[SchemaExtension] = [
            ext if isinstance(ext, SchemaExtension) else ext()
            for ext in self.extensions
        ]
        if self.directives:
            resolved.append(
                DirectivesExtensionSync() if sync else DirectivesExtension()
            )
        return resolved

    def create_extensions_runner(
        self, execution_context: ExecutionContext, extensions: list[SchemaExtension]
    ) -> SchemaExtensionsRunner:
        return SchemaExtensionsRunner(
            execution_context=execution_context,
            extensions=extensions,
        )

    def _get_custom_context_kwargs(
        self,
        operation_extensions: dict[str, Any] | None = None,
        *,
        sync: bool = False,
    ) -> dict[str, Any]:
        if not IS_GQL_33:
            return {}

        kwargs: dict[str, Any] = {"operation_extensions": operation_extensions}
        if sync:
            kwargs["is_async_iterable"] = lambda _x: False
        return kwargs

    def _get_middleware_manager(
        self, extensions: list[SchemaExtension]
    ) -> MiddlewareManager:
        # Build a fresh middleware manager per request: the manager holds
        # references to extension instances, which are now constructed
        # per-request to avoid concurrency leaks (see #4369).
        return MiddlewareManager(
            *(ext for ext in extensions if ext._implements_resolve())
        )

    def _create_execution_context(  # noqa: PLR0917
        self,
        query: str | None,
        allowed_operation_types: Iterable[OperationType],
        variable_values: dict[str, Any] | None = None,
        context_value: Any | None = None,
        root_value: Any | None = None,
        operation_name: str | None = None,
        operation_extensions: dict[str, Any] | None = None,
    ) -> ExecutionContext:
        return ExecutionContext(
            query=query,
            schema=self,
            allowed_operations=allowed_operation_types,
            context=context_value,
            root_value=root_value,
            variables=variable_values,
            provided_operation_name=operation_name,
            operation_extensions=operation_extensions,
        )

    @lru_cache
    def get_type_by_name(
        self, name: str
    ) -> (
        StrawberryObjectDefinition
        | ScalarDefinition
        | StrawberryEnumDefinition
        | StrawberryUnion
        | None
    ):
        # TODO: respect auto_camel_case
        if name in self.schema_converter.type_map:
            return self.schema_converter.type_map[name].definition

        return None

    def get_field_for_type(
        self, field_name: str, type_name: str
    ) -> StrawberryField | None:
        type_ = self.get_type_by_name(type_name)

        if not type_:
            return None  # pragma: no cover

        assert isinstance(type_, StrawberryObjectDefinition)

        return next(
            (
                field
                for field in type_.fields
                if self.config.name_converter.get_graphql_name(field) == field_name
            ),
            None,
        )

    @lru_cache
    def get_directive_by_name(self, graphql_name: str) -> StrawberryDirective | None:
        return next(
            (
                directive
                for directive in self.directives
                if self.config.name_converter.from_directive(directive) == graphql_name
            ),
            None,
        )

    def get_fields(
        self, type_definition: StrawberryObjectDefinition
    ) -> list[StrawberryField]:
        return type_definition.fields

    async def _prepare_operation_async(
        self, context: ExecutionContext, extensions_runner: SchemaExtensionsRunner
    ) -> PreExecutionError | None:
        """Parse, check the operation type, and validate before execution."""
        if not context.query and context.graphql_document is None:
            raise MissingQueryError

        async with extensions_runner.parsing():
            try:
                _parse_operation_document(context)

            except GraphQLError as error:
                context.pre_execution_errors = [error]
                return PreExecutionError(data=None, errors=[error])

            except Exception as error:  # noqa: BLE001
                error = GraphQLError(str(error), original_error=error)
                context.pre_execution_errors = [error]
                return PreExecutionError(data=None, errors=[error])

        try:
            operation_type = context.operation_type
        except RuntimeError as error:
            raise CannotGetOperationTypeError(context.operation_name) from error

        if operation_type not in context.allowed_operations:
            raise InvalidOperationTypeError(operation_type)

        async with extensions_runner.validation():
            _run_validation(context)
            if context.pre_execution_errors:
                return PreExecutionError(
                    data=None,
                    errors=context.pre_execution_errors,
                )

        return None

    def _prepare_operation_sync(
        self, context: ExecutionContext, extensions_runner: SchemaExtensionsRunner
    ) -> ExecutionResult | None:
        """Parse, check the operation type, and validate before execution."""
        if not context.query and context.graphql_document is None:
            raise MissingQueryError

        with extensions_runner.parsing():
            try:
                _parse_operation_document(context)

            except GraphQLError as error:
                context.pre_execution_errors = [error]
                self._process_errors([error], context)
                return ExecutionResult(
                    data=None,
                    errors=[error],
                    extensions=extensions_runner.get_extensions_results_sync(),
                )

        try:
            operation_type = context.operation_type
        except RuntimeError as error:
            raise CannotGetOperationTypeError(context.operation_name) from error

        if operation_type not in context.allowed_operations:
            raise InvalidOperationTypeError(operation_type)

        with extensions_runner.validation():
            _run_validation(context)
            if context.pre_execution_errors:
                self._process_errors(context.pre_execution_errors, context)
                return ExecutionResult(
                    data=None,
                    errors=context.pre_execution_errors,
                    extensions=extensions_runner.get_extensions_results_sync(),
                )

        return None

    async def _handle_execution_result(
        self,
        context: ExecutionContext,
        result: ResultType,
        extensions_runner: SchemaExtensionsRunner,
        *,
        # TODO: can we remove this somehow, see comment in execute
        skip_process_errors: bool = False,
    ) -> ExecutionResult:
        # TODO: handle this, also, why do we have both GraphQLExecutionResult and ExecutionResult?
        if isinstance(result, GraphQLIncrementalExecutionResults):
            return result

        # Set errors on the context so that it's easier
        # to access in extensions
        if result.errors:
            context.pre_execution_errors = result.errors
            if not skip_process_errors:
                self._process_errors(result.errors, context)
        if isinstance(result, GraphQLExecutionResult):
            result = ExecutionResult(data=result.data, errors=result.errors)
        result.extensions = await extensions_runner.get_extensions_results(context)
        context.result = result
        return result

    def _get_execute_function(self) -> Callable[..., Any]:
        """Return the graphql-core execution function for the current config.

        Uses ``experimental_execute_incrementally`` when incremental execution is
        enabled, falling back to ``execute`` otherwise. Single-sourced so the
        graphql-core version guard cannot drift between execution paths.
        """
        if self.config.enable_experimental_incremental_execution:
            if experimental_execute_incrementally is None:
                raise RuntimeError(
                    "Incremental execution is enabled but experimental_execute_incrementally is not available, "
                    "please install graphql-core>=3.3.0"
                )
            return experimental_execute_incrementally
        return execute

    async def _execute_operation(
        self,
        execution_context: ExecutionContext,
        extensions_runner: SchemaExtensionsRunner,
        middleware_manager: MiddlewareManager,
        execute_function: Callable[..., Any],
        custom_context_kwargs: dict[str, Any],
    ) -> ResultType:
        assert execution_context.graphql_document is not None

        async with extensions_runner.executing():
            if not execution_context.result:
                result = await await_maybe(
                    execute_function(
                        self._schema,
                        execution_context.graphql_document,
                        root_value=execution_context.root_value,
                        middleware=middleware_manager,
                        variable_values=execution_context.variables,
                        operation_name=execution_context.operation_name,
                        context_value=execution_context.context,
                        is_awaitable=optimized_is_awaitable,
                        **execution_context_class_kwargs(self.execution_context_class),
                        **custom_context_kwargs,
                    )
                )
                execution_context.result = result
            else:
                result = execution_context.result

            # Also set errors on the execution_context so that it's easier
            # to access in extensions.
            #
            # TODO: maybe here use the first result from incremental execution
            # if it exists.
            if isinstance(result, GraphQLExecutionResult) and result.errors:
                execution_context.pre_execution_errors = result.errors

                # Run the `Schema.process_errors` function here before
                # extensions have a chance to modify them (see the MaskErrors
                # extension). That way we can log the original errors but
                # only return a sanitised version to the client.
                self._process_errors(result.errors, execution_context)

        return result

    async def execute(  # noqa: PLR0917
        self,
        query: str | None,
        variable_values: dict[str, Any] | None = None,
        context_value: Any | None = None,
        root_value: Any | None = None,
        operation_name: str | None = None,
        allowed_operation_types: Iterable[OperationType] | None = None,
        operation_extensions: dict[str, Any] | None = None,
    ) -> ExecutionResult:
        if allowed_operation_types is None:
            allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES

        execution_context = self._create_execution_context(
            query=query,
            allowed_operation_types=allowed_operation_types,
            variable_values=variable_values,
            context_value=context_value,
            root_value=root_value,
            operation_name=operation_name,
            operation_extensions=operation_extensions,
        )
        extensions = self.get_extensions()
        # TODO (#3571): remove this when we implement execution context as parameter.
        for extension in extensions:
            extension.execution_context = execution_context

        extensions_runner = self.create_extensions_runner(execution_context, extensions)
        middleware_manager = self._get_middleware_manager(extensions)

        execute_function = self._get_execute_function()

        custom_context_kwargs = self._get_custom_context_kwargs(operation_extensions)

        try:
            async with extensions_runner.operation():
                # Note: In graphql-core the schema would be validated here but in
                # Strawberry we are validating it at initialisation time instead

                if errors := await self._prepare_operation_async(
                    execution_context, extensions_runner
                ):
                    return await self._handle_execution_result(
                        execution_context,
                        errors,
                        extensions_runner,
                    )

                assert execution_context.graphql_document
                result = await self._execute_operation(
                    execution_context,
                    extensions_runner,
                    middleware_manager,
                    execute_function,
                    custom_context_kwargs,
                )

        except (
            MissingQueryError,
            CannotGetOperationTypeError,
            InvalidOperationTypeError,
        ):
            raise
        except Exception as exc:  # noqa: BLE001
            return await self._handle_execution_result(
                execution_context,
                PreExecutionError(data=None, errors=[_coerce_error(exc)]),
                extensions_runner,
            )
        # return results after all the operation completed.
        return await self._handle_execution_result(
            execution_context, result, extensions_runner, skip_process_errors=True
        )

    def execute_sync(  # noqa: PLR0917
        self,
        query: str | None,
        variable_values: dict[str, Any] | None = None,
        context_value: Any | None = None,
        root_value: Any | None = None,
        operation_name: str | None = None,
        allowed_operation_types: Iterable[OperationType] | None = None,
        operation_extensions: dict[str, Any] | None = None,
    ) -> ExecutionResult:
        if allowed_operation_types is None:
            allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES

        execution_context = self._create_execution_context(
            query=query,
            allowed_operation_types=allowed_operation_types,
            variable_values=variable_values,
            context_value=context_value,
            root_value=root_value,
            operation_name=operation_name,
            operation_extensions=operation_extensions,
        )
        extensions = self.get_extensions(sync=True)
        # TODO (#3571): remove this when we implement execution context as parameter.
        for extension in extensions:
            extension.execution_context = execution_context

        extensions_runner = self.create_extensions_runner(execution_context, extensions)
        middleware_manager = self._get_middleware_manager(extensions)

        execute_function = self._get_execute_function()
        custom_context_kwargs = self._get_custom_context_kwargs(
            operation_extensions, sync=True
        )

        try:
            with extensions_runner.operation():
                # Note: In graphql-core the schema would be validated here but in
                # Strawberry we are validating it at initialisation time instead
                if (
                    pre_execution_result := self._prepare_operation_sync(
                        execution_context, extensions_runner
                    )
                ) is not None:
                    # Match the async path by exposing pre-execution results to
                    # operation extensions before their hooks unwind.
                    execution_context.result = pre_execution_result
                    return pre_execution_result

                assert execution_context.graphql_document is not None
                with extensions_runner.executing():
                    if not execution_context.result:
                        result = execute_function(
                            self._schema,
                            execution_context.graphql_document,
                            root_value=execution_context.root_value,
                            middleware=middleware_manager,
                            variable_values=execution_context.variables,
                            operation_name=execution_context.operation_name,
                            context_value=execution_context.context,
                            is_awaitable=optimized_is_awaitable,
                            **execution_context_class_kwargs(
                                self.execution_context_class
                            ),
                            **custom_context_kwargs,
                        )

                        if isawaitable(result):
                            result = cast("Awaitable[GraphQLExecutionResult]", result)
                            ensure_future(result).cancel()
                            raise RuntimeError(  # noqa: TRY301
                                "GraphQL execution failed to complete synchronously."
                            )

                        result = cast("GraphQLExecutionResult", result)
                        execution_context.result = result
                        # Also set errors on the context so that it's easier
                        # to access in extensions
                        if result.errors:
                            execution_context.pre_execution_errors = result.errors

                            # Run the `Schema.process_errors` function here before
                            # extensions have a chance to modify them (see the MaskErrors
                            # extension). That way we can log the original errors but
                            # only return a sanitised version to the client.
                            self._process_errors(result.errors, execution_context)
        except (
            MissingQueryError,
            CannotGetOperationTypeError,
            InvalidOperationTypeError,
        ):
            raise
        except Exception as exc:  # noqa: BLE001
            errors = [_coerce_error(exc)]
            execution_context.pre_execution_errors = errors
            self._process_errors(errors, execution_context)
            return ExecutionResult(
                data=None,
                errors=errors,
                extensions=extensions_runner.get_extensions_results_sync(),
            )
        return ExecutionResult(
            data=execution_context.result.data,
            errors=execution_context.result.errors,
            extensions=extensions_runner.get_extensions_results_sync(),
        )

    async def _stream(
        self,
        execution_context: ExecutionContext,
        extensions_runner: SchemaExtensionsRunner,
        middleware_manager: MiddlewareManager,
        execution_context_class: type[GraphQLExecutionContext] | None = None,
        operation_extensions: dict[str, Any] | None = None,
    ) -> StreamResult:
        async with extensions_runner.operation():
            try:
                initial_error = await self._prepare_operation_async(
                    context=execution_context,
                    extensions_runner=extensions_runner,
                )
            except (
                MissingQueryError,
                CannotGetOperationTypeError,
                InvalidOperationTypeError,
            ) as error:
                initial_error = PreExecutionError(
                    data=None, errors=[_coerce_pre_execution_error(error)]
                )

            if initial_error:
                initial_error.extensions = (
                    await extensions_runner.get_extensions_results(execution_context)
                )
                execution_result = await self._handle_execution_result(
                    execution_context, initial_error, extensions_runner
                )
                async with extensions_runner.on_stream_result(execution_result):
                    yield execution_result
                return

            assert execution_context.graphql_document is not None

            # Queries and mutations executed over a streaming transport yield a
            # single result and then the stream completes. Only subscriptions
            # produce an async generator of multiple results.
            if execution_context.operation_type is not OperationType.SUBSCRIPTION:
                result_source = self._stream_non_subscription(
                    execution_context,
                    extensions_runner,
                    middleware_manager,
                    operation_extensions,
                )
                async with aclosing(result_source):
                    async for result in result_source:
                        async with extensions_runner.on_stream_result(result):
                            yield result
                return

            try:
                async with extensions_runner.executing():
                    gql_33_kwargs = {
                        "middleware": middleware_manager,
                        "operation_extensions": operation_extensions,
                        **execution_context_class_kwargs(execution_context_class),
                    }
                    try:
                        # Might not be awaitable for pre-execution errors.
                        aiter_or_result: OriginSubscriptionResult = await await_maybe(
                            subscribe(
                                self._schema,
                                execution_context.graphql_document,
                                root_value=execution_context.root_value,
                                variable_values=execution_context.variables,
                                operation_name=execution_context.operation_name,
                                context_value=execution_context.context,
                                **{} if IS_GQL_32 else gql_33_kwargs,
                            )
                        )
                    # graphql-core 3.2 doesn't handle some of the pre-execution errors.
                    # see `test_subscription_immediate_error`
                    except Exception as exc:  # noqa: BLE001
                        aiter_or_result = OriginalExecutionResult(
                            data=None, errors=[_coerce_error(exc)]
                        )

                # Handle pre-execution errors.
                if isinstance(aiter_or_result, OriginalExecutionResult):
                    execution_result = await self._handle_execution_result(
                        execution_context,
                        PreExecutionError(data=None, errors=aiter_or_result.errors),
                        extensions_runner,
                    )
                    async with extensions_runner.on_stream_result(execution_result):
                        yield execution_result
                else:
                    try:
                        async with aclosing(aiter_or_result):
                            async for result in aiter_or_result:
                                extension_result = await self._handle_execution_result(
                                    execution_context,
                                    result,
                                    extensions_runner,
                                )

                                async with extensions_runner.on_stream_result(
                                    extension_result
                                ):
                                    yield extension_result
                    # graphql-core doesn't handle exceptions raised while executing.
                    except Exception as exc:  # noqa: BLE001
                        execution_result = await self._handle_execution_result(
                            execution_context,
                            OriginalExecutionResult(
                                data=None, errors=[_coerce_error(exc)]
                            ),
                            extensions_runner,
                        )
                        async with extensions_runner.on_stream_result(execution_result):
                            yield execution_result
            # catch exceptions raised in `on_execute` hook.
            except Exception as exc:  # noqa: BLE001
                origin_result = OriginalExecutionResult(
                    data=None, errors=[_coerce_error(exc)]
                )
                execution_result = await self._handle_execution_result(
                    execution_context,
                    origin_result,
                    extensions_runner,
                )
                async with extensions_runner.on_stream_result(execution_result):
                    yield execution_result

    async def _stream_non_subscription(
        self,
        execution_context: ExecutionContext,
        extensions_runner: SchemaExtensionsRunner,
        middleware_manager: MiddlewareManager,
        operation_extensions: dict[str, Any] | None = None,
    ) -> StreamResult:
        """Run a query/mutation over a streaming transport.

        Yields a single ``ExecutionResult`` for an ordinary operation. For
        incremental delivery (``@defer``/``@stream``) the result is a container,
        which is expanded into its initial frame followed by each subsequent
        patch so callers see one flat sequence of frames.
        """
        execute_function = self._get_execute_function()

        custom_context_kwargs = self._get_custom_context_kwargs(operation_extensions)

        assert execution_context.graphql_document is not None

        try:
            result = await self._execute_operation(
                execution_context,
                extensions_runner,
                middleware_manager,
                execute_function,
                custom_context_kwargs,
            )
        except asyncio.CancelledError:
            raise
        except Exception as exc:  # noqa: BLE001
            yield await self._handle_execution_result(
                execution_context,
                PreExecutionError(data=None, errors=[_coerce_error(exc)]),
                extensions_runner,
            )
            return

        # Incremental delivery (`@defer`/`@stream`) produces a container rather
        # than a single result. Normalize the initial result through Strawberry's
        # result handling, then yield subsequent graphql-core patch frames as-is:
        # their errors are nested inside incremental payloads and transport
        # formatting is responsible for those frame shapes.
        if isinstance(result, GraphQLIncrementalExecutionResults):
            yield await self._handle_execution_result(
                execution_context,
                result.initial_result,
                extensions_runner,
            )
            async with aclosing(result.subsequent_results) as subsequent_results:
                async for incremental_result in subsequent_results:
                    yield incremental_result
            return

        yield await self._handle_execution_result(
            execution_context, result, extensions_runner, skip_process_errors=True
        )

    async def subscribe(  # noqa: PLR0917
        self,
        query: str | None,
        variable_values: dict[str, Any] | None = None,
        context_value: Any | None = None,
        root_value: Any | None = None,
        operation_name: str | None = None,
        operation_extensions: dict[str, Any] | None = None,
    ) -> SubscriptionResult:
        """Execute a subscription and stream its results.

        Thin wrapper around :meth:`stream` restricted to subscriptions. New
        streaming transports should prefer :meth:`stream`, which can also run
        queries and mutations.
        """
        # Subscriptions never produce incremental-delivery frames, so the result
        # is always a ``SubscriptionResult`` even though ``stream`` is wider.
        return cast(
            "SubscriptionResult",
            await self.stream(
                query=query,
                variable_values=variable_values,
                context_value=context_value,
                root_value=root_value,
                operation_name=operation_name,
                operation_extensions=operation_extensions,
                allowed_operation_types=(OperationType.SUBSCRIPTION,),
            ),
        )

    async def stream(  # noqa: PLR0917
        self,
        query: str | None,
        variable_values: dict[str, Any] | None = None,
        context_value: Any | None = None,
        root_value: Any | None = None,
        operation_name: str | None = None,
        operation_extensions: dict[str, Any] | None = None,
        allowed_operation_types: Iterable[OperationType] | None = None,
    ) -> StreamResult:
        """Execute an operation and stream its result(s).

        Parses the document once, inside the extension lifecycle, and dispatches
        on the operation type: subscriptions stream their results, while queries
        and mutations yield a single result and then complete. This lets a
        streaming transport (SSE, multipart, WebSocket) run any operation without
        having to parse and route on the operation type itself.

        The yielded element is a strawberry :class:`ExecutionResult` (or
        :class:`PreExecutionError`) for queries, mutations and subscriptions. For
        incremental delivery (``@defer``/``@stream``) the raw graphql-core frames
        are yielded instead, which the caller must format for its transport.

        Custom parsing or parser caching should be implemented with an
        ``on_parse`` extension that populates ``execution_context.graphql_document``.
        """
        if allowed_operation_types is None:
            allowed_operation_types = DEFAULT_ALLOWED_OPERATION_TYPES

        execution_context = self._create_execution_context(
            query=query,
            allowed_operation_types=allowed_operation_types,
            variable_values=variable_values,
            context_value=context_value,
            root_value=root_value,
            operation_name=operation_name,
            operation_extensions=operation_extensions,
        )
        extensions = self.get_extensions()
        # TODO (#3571): remove this when we implement execution context as parameter.
        for extension in extensions:
            extension.execution_context = execution_context

        return self._stream(
            execution_context,
            extensions_runner=self.create_extensions_runner(
                execution_context, extensions
            ),
            middleware_manager=self._get_middleware_manager(extensions),
            execution_context_class=self.execution_context_class,
            operation_extensions=operation_extensions,
        )

    def _resolve_node_ids(self) -> None:
        for concrete_type in self.schema_converter.type_map.values():
            type_def = concrete_type.definition

            # This can be a TypeDefinition, StrawberryEnumDefinition, ScalarDefinition
            # or UnionDefinition
            if not isinstance(type_def, StrawberryObjectDefinition):
                continue

            # Do not validate id_attr for interfaces. relay.Node itself and
            # any other interfdace that implements it are not required to
            # provide a NodeID annotation, only the concrete type implementing
            # them needs to do that.
            if type_def.is_interface:
                continue

            # Call resolve_id_attr in here to make sure we raise provide
            # early feedback for missing NodeID annotations
            origin = type_def.origin
            if issubclass(origin, relay.Node):
                has_custom_resolve_id = False
                for base in origin.__mro__:
                    if base is relay.Node:
                        break
                    if "resolve_id" in base.__dict__:
                        has_custom_resolve_id = True
                        break

                if not has_custom_resolve_id:
                    origin.resolve_id_attr()

    def _prepare_schema_directives(self) -> None:
        """Hook for subclasses to append generated ``schema_directives``.

        Runs after the attached directives have been collected (so
        ``_schema_directives_in_use`` is populated) and before the GraphQLSchema
        is built, so anything appended here is part of the served schema.
        """

    def _warn_for_federation_directives(self) -> None:
        """Raises a warning if the schema has any federation directives."""
        from strawberry.federation.schema_directives import FederationDirective

        all_types = self.schema_converter.type_map.values()
        all_type_defs = (type_.definition for type_ in all_types)

        all_directives = (
            directive
            for type_def in all_type_defs
            for directive in (type_def.directives or [])
        )

        if any(
            isinstance(directive, FederationDirective) for directive in all_directives
        ):
            warnings.warn(
                "Federation directive found in schema. "
                "Use `strawberry.federation.Schema` instead of `strawberry.Schema`.",
                UserWarning,
                stacklevel=3,
            )

    def _extend_introspection(self) -> None:
        def _resolve_is_one_of(obj: Any, info: Any) -> bool:
            if "strawberry-definition" not in obj.extensions:
                return False

            return obj.extensions["strawberry-definition"].is_one_of

        instrospection_type = self._schema.type_map["__Type"]
        instrospection_type.fields["isOneOf"] = GraphQLField(GraphQLBoolean)  # type: ignore[attr-defined]
        instrospection_type.fields["isOneOf"].resolve = _resolve_is_one_of  # type: ignore[attr-defined]

    def as_str(self) -> str:
        return print_schema(self)

    __str__ = as_str

    def introspect(self) -> dict[str, Any]:
        """Return the introspection query result for the current schema.

        Raises:
            ValueError: If the introspection query fails due to an invalid schema
        """
        introspection = self.execute_sync(get_introspection_query())
        if introspection.errors or not introspection.data:
            raise ValueError(f"Invalid Schema. Errors {introspection.errors!r}")

        return introspection.data


__all__ = ["Schema"]
