<?php

namespace WPGraphQL\Registry;

use GraphQL\Error\Error;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use WPGraphQL;
use WPGraphQL\AppContext;
use WPGraphQL\Data\Connection\PostObjectConnectionResolver;
use WPGraphQL\Data\DataSource;
use WPGraphQL\Mutation\CommentCreate;
use WPGraphQL\Mutation\CommentDelete;
use WPGraphQL\Mutation\CommentRestore;
use WPGraphQL\Mutation\CommentUpdate;
use WPGraphQL\Mutation\MediaItemCreate;
use WPGraphQL\Mutation\MediaItemDelete;
use WPGraphQL\Mutation\MediaItemUpdate;
use WPGraphQL\Mutation\PostObjectCreate;
use WPGraphQL\Mutation\PostObjectDelete;
use WPGraphQL\Mutation\PostObjectUpdate;
use WPGraphQL\Mutation\ResetUserPassword;
use WPGraphQL\Mutation\SendPasswordResetEmail;
use WPGraphQL\Mutation\TermObjectCreate;
use WPGraphQL\Mutation\TermObjectDelete;
use WPGraphQL\Mutation\TermObjectUpdate;
use WPGraphQL\Mutation\UpdateSettings;
use WPGraphQL\Mutation\UserCreate;
use WPGraphQL\Mutation\UserDelete;
use WPGraphQL\Mutation\UserRegister;
use WPGraphQL\Mutation\UserUpdate;
use WPGraphQL\Registry\Utils\PostObject;
use WPGraphQL\Registry\Utils\TermObject;
use WPGraphQL\Type\Connection\Comments;
use WPGraphQL\Type\Connection\MenuItems;
use WPGraphQL\Type\Connection\PostObjects;
use WPGraphQL\Type\Connection\Taxonomies;
use WPGraphQL\Type\Connection\TermObjects;
use WPGraphQL\Type\Connection\Users;
use WPGraphQL\Type\Enum\AvatarRatingEnum;
use WPGraphQL\Type\Enum\CommentNodeIdTypeEnum;
use WPGraphQL\Type\Enum\CommentStatusEnum;
use WPGraphQL\Type\Enum\CommentsConnectionOrderbyEnum;
use WPGraphQL\Type\Enum\ContentNodeIdTypeEnum;
use WPGraphQL\Type\Enum\ContentTypeEnum;
use WPGraphQL\Type\Enum\ContentTypeIdTypeEnum;
use WPGraphQL\Type\Enum\MediaItemSizeEnum;
use WPGraphQL\Type\Enum\MediaItemStatusEnum;
use WPGraphQL\Type\Enum\MenuItemNodeIdTypeEnum;
use WPGraphQL\Type\Enum\MenuLocationEnum;
use WPGraphQL\Type\Enum\MenuNodeIdTypeEnum;
use WPGraphQL\Type\Enum\MimeTypeEnum;
use WPGraphQL\Type\Enum\OrderEnum;
use WPGraphQL\Type\Enum\PluginStatusEnum;
use WPGraphQL\Type\Enum\PostObjectFieldFormatEnum;
use WPGraphQL\Type\Enum\PostObjectsConnectionDateColumnEnum;
use WPGraphQL\Type\Enum\PostObjectsConnectionOrderbyEnum;
use WPGraphQL\Type\Enum\PostStatusEnum;
use WPGraphQL\Type\Enum\RelationEnum;
use WPGraphQL\Type\Enum\ScriptLoadingGroupLocationEnum;
use WPGraphQL\Type\Enum\ScriptLoadingStrategyEnum;
use WPGraphQL\Type\Enum\TaxonomyEnum;
use WPGraphQL\Type\Enum\TaxonomyIdTypeEnum;
use WPGraphQL\Type\Enum\TermNodeIdTypeEnum;
use WPGraphQL\Type\Enum\TermObjectsConnectionOrderbyEnum;
use WPGraphQL\Type\Enum\TimezoneEnum;
use WPGraphQL\Type\Enum\UserNodeIdTypeEnum;
use WPGraphQL\Type\Enum\UserRoleEnum;
use WPGraphQL\Type\Enum\UsersConnectionOrderbyEnum;
use WPGraphQL\Type\Enum\UsersConnectionSearchColumnEnum;
use WPGraphQL\Type\Input\DateInput;
use WPGraphQL\Type\Input\DateQueryInput;
use WPGraphQL\Type\Input\PostObjectsConnectionOrderbyInput;
use WPGraphQL\Type\Input\UsersConnectionOrderbyInput;
use WPGraphQL\Type\InterfaceType\Commenter;
use WPGraphQL\Type\InterfaceType\Connection;
use WPGraphQL\Type\InterfaceType\ContentNode;
use WPGraphQL\Type\InterfaceType\ContentTemplate;
use WPGraphQL\Type\InterfaceType\DatabaseIdentifier;
use WPGraphQL\Type\InterfaceType\Edge;
use WPGraphQL\Type\InterfaceType\EnqueuedAsset;
use WPGraphQL\Type\InterfaceType\HierarchicalContentNode;
use WPGraphQL\Type\InterfaceType\HierarchicalNode;
use WPGraphQL\Type\InterfaceType\HierarchicalTermNode;
use WPGraphQL\Type\InterfaceType\MenuItemLinkable;
use WPGraphQL\Type\InterfaceType\Node;
use WPGraphQL\Type\InterfaceType\NodeWithAuthor;
use WPGraphQL\Type\InterfaceType\NodeWithComments;
use WPGraphQL\Type\InterfaceType\NodeWithContentEditor;
use WPGraphQL\Type\InterfaceType\NodeWithExcerpt;
use WPGraphQL\Type\InterfaceType\NodeWithFeaturedImage;
use WPGraphQL\Type\InterfaceType\NodeWithPageAttributes;
use WPGraphQL\Type\InterfaceType\NodeWithRevisions;
use WPGraphQL\Type\InterfaceType\NodeWithTemplate;
use WPGraphQL\Type\InterfaceType\NodeWithTitle;
use WPGraphQL\Type\InterfaceType\NodeWithTrackbacks;
use WPGraphQL\Type\InterfaceType\OneToOneConnection;
use WPGraphQL\Type\InterfaceType\PageInfo;
use WPGraphQL\Type\InterfaceType\Previewable;
use WPGraphQL\Type\InterfaceType\TermNode;
use WPGraphQL\Type\InterfaceType\UniformResourceIdentifiable;
use WPGraphQL\Type\ObjectType\Avatar;
use WPGraphQL\Type\ObjectType\Comment;
use WPGraphQL\Type\ObjectType\CommentAuthor;
use WPGraphQL\Type\ObjectType\ContentType;
use WPGraphQL\Type\ObjectType\EnqueuedScript;
use WPGraphQL\Type\ObjectType\EnqueuedStylesheet;
use WPGraphQL\Type\ObjectType\MediaDetails;
use WPGraphQL\Type\ObjectType\MediaItemMeta;
use WPGraphQL\Type\ObjectType\MediaSize;
use WPGraphQL\Type\ObjectType\Menu;
use WPGraphQL\Type\ObjectType\MenuItem;
use WPGraphQL\Type\ObjectType\Plugin;
use WPGraphQL\Type\ObjectType\PostTypeLabelDetails;
use WPGraphQL\Type\ObjectType\RootMutation;
use WPGraphQL\Type\ObjectType\RootQuery;
use WPGraphQL\Type\ObjectType\SettingGroup;
use WPGraphQL\Type\ObjectType\Settings;
use WPGraphQL\Type\ObjectType\Taxonomy;
use WPGraphQL\Type\ObjectType\Theme;
use WPGraphQL\Type\ObjectType\User;
use WPGraphQL\Type\ObjectType\UserRole;
use WPGraphQL\Type\WPConnectionType;
use WPGraphQL\Type\WPEnumType;
use WPGraphQL\Type\WPInputObjectType;
use WPGraphQL\Type\WPInterfaceType;
use WPGraphQL\Type\WPMutationType;
use WPGraphQL\Type\WPObjectType;
use WPGraphQL\Type\WPScalar;
use WPGraphQL\Type\WPUnionType;
use WPGraphQL\Utils\Utils;

/**
 * Class TypeRegistry
 *
 * This class maintains the registry of Types used in the GraphQL Schema
 *
 * @phpstan-import-type InputObjectConfig from \GraphQL\Type\Definition\InputObjectType
 * @phpstan-import-type InterfaceConfig from \GraphQL\Type\Definition\InterfaceType
 * @phpstan-import-type ObjectConfig from \GraphQL\Type\Definition\ObjectType
 * @phpstan-import-type WPEnumTypeConfig from \WPGraphQL\Type\WPEnumType
 * @phpstan-import-type WPScalarConfig from \WPGraphQL\Type\WPScalar
 *
 * @phpstan-type TypeDef \GraphQL\Type\Definition\Type&\GraphQL\Type\Definition\NamedType
 *
 * @package WPGraphQL\Registry
 */
class TypeRegistry {

	/**
	 * The registered Types
	 *
	 * @var array<string,?TypeDef>
	 */
	protected $types;

	/**
	 * The keys that are prepared for introspection.
	 *
	 * @var array<string>|null
	 */
	protected static ?array $introspection_keys = null;

	/**
	 * The loaders needed to register types
	 *
	 * @var array<string,callable(): ?TypeDef>
	 */
	protected $type_loaders;

	/**
	 * Stores a list of Types that need to be eagerly loaded instead of lazy loaded.
	 *
	 * Types that exist in the Schema but are only part of a Union/Interface ResolveType but not
	 * referenced directly need to be eagerly loaded.
	 *
	 * @var array<string,string>
	 */
	protected $eager_type_map;

	/**
	 * Stores a list of Types that should be excluded from the schema.
	 *
	 * Type names are filtered by `graphql_excluded_types` and normalized using strtolower(), to avoid case sensitivity issues.
	 *
	 * @var string[]
	 */
	protected $excluded_types = null;

	/**
	 * Stores a list of mutation names that should be excluded from the schema, along with their generated input and payload types.
	 *
	 * Type names are filtered by `graphql_excluded_mutations` and normalized using strtolower(), to avoid case sensitivity issues.
	 *
	 * @var string[]
	 */
	protected $excluded_mutations = null;

	/**
	 * Stores a list of connection Type names that should be excluded from the schema, along with their generated types.
	 *
	 * Type names are filtered by `graphql_excluded_connections` and normalized using strtolower(), to avoid case sensitivity issues.
	 *
	 * Type name
	 *
	 * @var string[]
	 */
	protected $excluded_connections = null;

	/**
	 * Flag to prevent infinite recursion when checking field type compatibility during interface field overrides.
	 *
	 * @var bool
	 */
	protected $checking_compatibility = false;

	/**
	 * TypeRegistry constructor.
	 */
	public function __construct() {
		$this->types          = [];
		$this->type_loaders   = [];
		$this->eager_type_map = [];
	}

	/**
	 * Formats the array key to a more friendly format
	 *
	 * @param string $key Name of the array key to format
	 *
	 * @return string
	 */
	protected function format_key( string $key ) {
		return strtolower( $key );
	}

	/**
	 * Returns the eager type map, an array of Type definitions for Types that
	 * are not directly referenced in the schema.
	 *
	 * Types can add "eagerlyLoadType => true" when being registered to be included
	 * in the eager_type_map.
	 *
	 * @return array<string,?TypeDef>
	 */
	protected function get_eager_type_map() {
		if ( empty( $this->eager_type_map ) ) {
			return [];
		}

		$resolved_types = [];
		foreach ( $this->eager_type_map as $type_name ) {
			$resolved_types[ $type_name ] = $this->get_type( $type_name );
		}

		return $resolved_types;
	}

	/**
	 * Initialize the TypeRegistry
	 *
	 * @throws \Exception
	 *
	 * @return void
	 */
	public function init() {
		$this->register_type( 'Bool', Type::boolean() );
		$this->register_type( 'Boolean', Type::boolean() );
		$this->register_type( 'Float', Type::float() );
		$this->register_type( 'Number', Type::float() );
		$this->register_type( 'Id', Type::id() );
		$this->register_type( 'Int', Type::int() );
		$this->register_type( 'Integer', Type::int() );
		$this->register_type( 'String', Type::string() );

		/**
		 * When the Type Registry is initialized execute these files
		 */
		add_action( 'init_graphql_type_registry', [ $this, 'init_type_registry' ], 5, 1 );

		/**
		 * Fire an action as the Type registry is being initiated
		 *
		 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
		 */
		do_action( 'init_graphql_type_registry', $this );
	}

	/**
	 * Initialize the Type Registry
	 *
	 * @param \WPGraphQL\Registry\TypeRegistry $type_registry
	 *
	 * @return void
	 * @throws \Exception
	 */
	public function init_type_registry( self $type_registry ) {

		/**
		 * Fire an action as the type registry is initialized. This executes
		 * before the `graphql_register_types` action to allow for earlier hooking
		 *
		 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
		 */
		do_action( 'graphql_register_initial_types', $type_registry );

		// Register Interfaces.
		Node::register_type();
		Commenter::register_type( $type_registry );
		Connection::register_type( $type_registry );
		ContentNode::register_type( $type_registry );
		ContentTemplate::register_type();
		DatabaseIdentifier::register_type();
		Edge::register_type( $type_registry );
		EnqueuedAsset::register_type( $type_registry );
		HierarchicalContentNode::register_type( $type_registry );
		HierarchicalNode::register_type( $type_registry );
		HierarchicalTermNode::register_type( $type_registry );
		MenuItemLinkable::register_type( $type_registry );
		NodeWithAuthor::register_type( $type_registry );
		NodeWithComments::register_type( $type_registry );
		NodeWithContentEditor::register_type( $type_registry );
		NodeWithExcerpt::register_type( $type_registry );
		NodeWithFeaturedImage::register_type( $type_registry );
		NodeWithRevisions::register_type( $type_registry );
		NodeWithTitle::register_type( $type_registry );
		NodeWithTemplate::register_type( $type_registry );
		NodeWithTrackbacks::register_type( $type_registry );
		NodeWithPageAttributes::register_type( $type_registry );
		PageInfo::register_type( $type_registry );
		Previewable::register_type( $type_registry );
		OneToOneConnection::register_type( $type_registry );
		TermNode::register_type( $type_registry );
		UniformResourceIdentifiable::register_type( $type_registry );

		// register types
		RootQuery::register_type();
		RootQuery::register_post_object_fields();
		RootQuery::register_term_object_fields();
		RootMutation::register_type();
		Avatar::register_type();
		Comment::register_type();
		CommentAuthor::register_type();
		ContentTemplate::register_content_template_types();
		EnqueuedStylesheet::register_type();
		EnqueuedScript::register_type();
		MediaDetails::register_type();
		MediaItemMeta::register_type();
		MediaSize::register_type();
		Menu::register_type();
		MenuItem::register_type();
		Plugin::register_type();
		ContentType::register_type();
		PostTypeLabelDetails::register_type();
		Settings::register_type( $this );
		Taxonomy::register_type();
		Theme::register_type();
		User::register_type();
		UserRole::register_type();

		AvatarRatingEnum::register_type();
		CommentNodeIdTypeEnum::register_type();
		CommentsConnectionOrderbyEnum::register_type();
		CommentStatusEnum::register_type();
		ContentNodeIdTypeEnum::register_type();
		ContentTypeEnum::register_type();
		ContentTypeIdTypeEnum::register_type();
		MediaItemSizeEnum::register_type();
		MediaItemStatusEnum::register_type();
		MenuLocationEnum::register_type();
		MenuItemNodeIdTypeEnum::register_type();
		MenuNodeIdTypeEnum::register_type();
		MimeTypeEnum::register_type();
		OrderEnum::register_type();
		PluginStatusEnum::register_type();
		PostObjectFieldFormatEnum::register_type();
		PostObjectsConnectionDateColumnEnum::register_type();
		PostObjectsConnectionOrderbyEnum::register_type();
		PostStatusEnum::register_type();
		RelationEnum::register_type();
		ScriptLoadingStrategyEnum::register_type();
		ScriptLoadingGroupLocationEnum::register_type();
		TaxonomyEnum::register_type();
		TaxonomyIdTypeEnum::register_type();
		TermNodeIdTypeEnum::register_type();
		TermObjectsConnectionOrderbyEnum::register_type();
		TimezoneEnum::register_type();
		UserNodeIdTypeEnum::register_type();
		UserRoleEnum::register_type();
		UsersConnectionOrderbyEnum::register_type();
		UsersConnectionSearchColumnEnum::register_type();

		DateInput::register_type();
		DateQueryInput::register_type();
		PostObjectsConnectionOrderbyInput::register_type();
		UsersConnectionOrderbyInput::register_type();

		/**
		 * Register core connections
		 */
		Comments::register_connections();
		MenuItems::register_connections();
		PostObjects::register_connections();
		Taxonomies::register_connections();
		TermObjects::register_connections();
		Users::register_connections();

		/**
		 * Register core mutations
		 */
		CommentCreate::register_mutation();
		CommentDelete::register_mutation();
		CommentRestore::register_mutation();
		CommentUpdate::register_mutation();
		MediaItemCreate::register_mutation();
		MediaItemDelete::register_mutation();
		MediaItemUpdate::register_mutation();
		ResetUserPassword::register_mutation();
		SendPasswordResetEmail::register_mutation();
		UserCreate::register_mutation();
		UserDelete::register_mutation();
		UserUpdate::register_mutation();
		UserRegister::register_mutation();
		UpdateSettings::register_mutation( $this );

		/**
		 * Register PostObject types based on post_types configured to show_in_graphql.
		 */
		$allowed_post_types = WPGraphQL::get_allowed_post_types( 'objects' );
		$allowed_taxonomies = WPGraphQL::get_allowed_taxonomies( 'objects' );

		foreach ( $allowed_post_types as $post_type_object ) {
			PostObject::register_types( $post_type_object );

			/**
			 * Mutations for attachments are handled differently
			 * because they require different inputs
			 */
			if ( 'attachment' !== $post_type_object->name ) {

				/**
				 * Revisions are created behind the scenes as a side effect of post updates,
				 * they aren't created manually.
				 */
				if ( 'revision' !== $post_type_object->name ) {
					if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'create', $post_type_object->graphql_exclude_mutations, true ) ) {
						PostObjectCreate::register_mutation( $post_type_object );
					}

					if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'update', $post_type_object->graphql_exclude_mutations, true ) ) {
						PostObjectUpdate::register_mutation( $post_type_object );
					}
				}

				if ( empty( $post_type_object->graphql_exclude_mutations ) || ! in_array( 'delete', $post_type_object->graphql_exclude_mutations, true ) ) {
					PostObjectDelete::register_mutation( $post_type_object );
				}
			}

			foreach ( $allowed_taxonomies as $tax_object ) {
				// If the taxonomy is in the array of taxonomies registered to the post_type
				if ( in_array( $tax_object->name, get_object_taxonomies( $post_type_object->name ), true ) ) {
					register_graphql_input_type(
						$post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
						[
							'description' => static function () use ( $tax_object, $post_type_object ) {
								return sprintf(
										// translators: %1$s is the GraphQL plural name of the taxonomy, %2$s is the GraphQL singular name of the post type.
									__( 'List of %1$s to connect the %2$s to. If an ID is set, it will be used to create the connection. If not, it will look for a slug. If neither are valid existing terms, and the site is configured to allow terms to be created during post mutations, a term will be created using the Name if it exists in the input, then fallback to the slug if it exists.', 'wp-graphql' ),
									$tax_object->graphql_plural_name,
									$post_type_object->graphql_single_name
								);
							},
							'fields'      => [
								'id'          => [
									'type'        => 'Id',
									'description' => static function () use ( $tax_object, $post_type_object ) {
										return sprintf(
												// translators: %1$s is the GraphQL name of the taxonomy, %2$s is the GraphQL name of the post type.
											__( 'The ID of the %1$s. If present, this will be used to connect to the %2$s. If no existing %1$s exists with this ID, no connection will be made.', 'wp-graphql' ),
											$tax_object->graphql_single_name,
											$post_type_object->graphql_single_name
										);
									},
								],
								'slug'        => [
									'type'        => 'String',
									'description' => static function () use ( $tax_object ) {
										return sprintf(
											// translators: %1$s is the GraphQL name of the taxonomy.
											__( 'The slug of the %1$s. If no ID is present, this field will be used to make a connection. If no existing term exists with this slug, this field will be used as a fallback to the Name field when creating a new term to connect to, if term creation is enabled as a nested mutation.', 'wp-graphql' ),
											$tax_object->graphql_single_name
										);
									},
								],
								'description' => [
									'type'        => 'String',
									'description' => static function () use ( $tax_object ) {
										return sprintf(
											// translators: %1$s is the GraphQL name of the taxonomy.
											__( 'The description of the %1$s. This field is used to set a description of the %1$s if a new one is created during the mutation.', 'wp-graphql' ),
											$tax_object->graphql_single_name
										);
									},
								],
								'name'        => [
									'type'        => 'String',
									'description' => static function () use ( $tax_object ) {
										return sprintf(
											// translators: %1$s is the GraphQL name of the taxonomy.
											__( 'The name of the %1$s. This field is used to create a new term, if term creation is enabled in nested mutations, and if one does not already exist with the provided slug or ID or if a slug or ID is not provided. If no name is included and a term is created, the creation will fallback to the slug field.', 'wp-graphql' ),
											$tax_object->graphql_single_name
										);
									},
								],
							],
						]
					);

					register_graphql_input_type(
						ucfirst( $post_type_object->graphql_single_name ) . ucfirst( $tax_object->graphql_plural_name ) . 'Input',
						[
							'description' => static function () use ( $tax_object, $post_type_object ) {
								return sprintf(
									// translators: %1$s is the GraphQL name of the post type, %2$s is the plural GraphQL name of the taxonomy.
									__( 'Set relationships between the %1$s to %2$s', 'wp-graphql' ),
									$post_type_object->graphql_single_name,
									$tax_object->graphql_plural_name
								);
							},
							'fields'      => [
								'append' => [
									'type'        => 'Boolean',
									'description' => static function () use ( $tax_object ) {
										return sprintf(
											// translators: %1$s is the GraphQL name of the taxonomy, %2$s is the plural GraphQL name of the taxonomy.
											__( 'If true, this will append the %1$s to existing related %2$s. If false, this will replace existing relationships. Default true.', 'wp-graphql' ),
											$tax_object->graphql_single_name,
											$tax_object->graphql_plural_name
										);
									},
								],
								'nodes'  => [
									'type'        => [
										'list_of' => $post_type_object->graphql_single_name . ucfirst( $tax_object->graphql_plural_name ) . 'NodeInput',
									],
									'description' => static function () {
										return __( 'The input list of items to set.', 'wp-graphql' );
									},
								],
							],
						]
					);
				}
			}
		}

		/**
		 * Register TermObject types based on taxonomies configured to show_in_graphql
		 */
		foreach ( $allowed_taxonomies as $tax_object ) {
			TermObject::register_types( $tax_object );

			if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'create', $tax_object->graphql_exclude_mutations, true ) ) {
				TermObjectCreate::register_mutation( $tax_object );
			}

			if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'update', $tax_object->graphql_exclude_mutations, true ) ) {
				TermObjectUpdate::register_mutation( $tax_object );
			}

			if ( empty( $tax_object->graphql_exclude_mutations ) || ! in_array( 'delete', $tax_object->graphql_exclude_mutations, true ) ) {
				TermObjectDelete::register_mutation( $tax_object );
			}
		}

		/**
		 * Create the root query fields for any setting type in
		 * the $allowed_setting_types array.
		 */
		$allowed_setting_types = DataSource::get_allowed_settings_by_group( $this );

		/**
		 * The url is not a registered setting for multisite, so this is a polyfill
		 * to expose the URL to the Schema for multisite sites
		 */
		if ( is_multisite() ) {
			$this->register_field(
				'GeneralSettings',
				'url',
				[
					'type'        => 'String',
					'description' => static function () {
						return __( 'Site URL.', 'wp-graphql' );
					},
					'resolve'     => static function () {
						return get_site_url();
					},
				]
			);
		}

		/**
		 * Register the siteIconUrl field on GeneralSettings.
		 *
		 * WordPress Site Icon is a feature that allows users to set a favicon/icon
		 * for their site. This exposes that icon URL to the GraphQL Schema.
		 *
		 * @see https://developer.wordpress.org/reference/functions/get_site_icon_url/
		 */
		$this->register_field(
			'GeneralSettings',
			'siteIconUrl',
			[
				'type'        => 'String',
				'description' => static function () {
					return __( 'Site icon URL configured in site settings, used as the site\'s favicon and app icon.', 'wp-graphql' );
				},
				'args'        => [
					'size' => [
						'type'        => 'Int',
						'description' => static function () {
							return __( 'Size of the site icon in pixels. Defaults to 512. Max 512.', 'wp-graphql' );
						},
					],
				],
				'resolve'     => static function ( $source, array $args ) {
					$size = ! empty( $args['size'] ) ? absint( $args['size'] ) : 512;
					// WordPress site icons are uploaded at 512x512 max, clamp the size.
					$size = min( $size, 512 );
					$url  = get_site_icon_url( $size );

					return ! empty( $url ) ? $url : null;
				},
			]
		);

		/**
		 * Register the siteIcon connection on GeneralSettings.
		 *
		 * This provides access to the MediaItem that is set as the site icon,
		 * allowing clients to access all media properties (alt text, sizes, etc).
		 *
		 * @see https://developer.wordpress.org/reference/functions/get_option/ (site_icon option)
		 */
		register_graphql_connection(
			[
				'fromType'      => 'GeneralSettings',
				'toType'        => 'MediaItem',
				'fromFieldName' => 'siteIcon',
				'oneToOne'      => true,
				'description'   => static function () {
					return __( 'The media item representing the site icon configured in site settings, used as the site\'s favicon and app icon.', 'wp-graphql' );
				},
				'resolve'       => static function ( $source, array $args, AppContext $context, ResolveInfo $info ) {
					$site_icon_id = get_option( 'site_icon' );

					if ( empty( $site_icon_id ) ) {
						return null;
					}

					$resolver = new PostObjectConnectionResolver( $source, $args, $context, $info, 'attachment' );
					$resolver->set_query_arg( 'p', absint( $site_icon_id ) );

					return $resolver->one_to_one()->get_connection();
				},
			]
		);

		if ( ! empty( $allowed_setting_types ) && is_array( $allowed_setting_types ) ) {
			foreach ( $allowed_setting_types as $group_name => $setting_type ) {
				$group_name = DataSource::format_group_name( $group_name );
				$type_name  = SettingGroup::register_settings_group( $group_name, $group_name, $this );

				if ( ! $type_name ) {
					continue;
				}

				register_graphql_field(
					'RootQuery',
					Utils::format_field_name( $type_name ),
					[
						'type'        => $type_name,
						'description' => static function () use ( $group_name ) {
							return sprintf(
								// translators: %s is the GraphQL name of the settings group.
								__( "Fields of the '%s' settings group", 'wp-graphql' ),
								ucfirst( $group_name ) . 'Settings'
							);
						},
						'resolve'     => static function () use ( $setting_type ) {
							return $setting_type;
						},
					]
				);
			}
		}

		/**
		 * Fire an action as the type registry is initialized. This executes
		 * before the `graphql_register_types` action to allow for earlier hooking
		 *
		 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
		 */
		do_action( 'graphql_register_types', $type_registry );

		/**
		 * Fire an action as the type registry is initialized. This executes
		 * during the `graphql_register_types` action to allow for earlier hooking
		 *
		 * @param \WPGraphQL\Registry\TypeRegistry $registry Instance of the TypeRegistry
		 */
		do_action( 'graphql_register_types_late', $type_registry );
	}

	/**
	 * Given a config for a custom Scalar, this adds the Scalar for use in the Schema.
	 *
	 * @param string              $type_name The name of the Type to register
	 * @param array<string,mixed> $config    The config for the scalar type to register
	 *
	 * @phpstan-param WPScalarConfig $config
	 *
	 * @throws \Exception
	 *
	 * @return void
	 */
	public function register_scalar( string $type_name, array $config ) {
		$config['kind'] = 'scalar';
		$this->register_type( $type_name, $config );
	}

	/**
	 * Registers connections that were passed through the Type registration config
	 *
	 * @param array<string,mixed> $config Type config
	 *
	 * @return void
	 *
	 * @throws \Exception
	 */
	protected function register_connections_from_config( array $config ) {
		$connections = $config['connections'] ?? null;

		if ( ! is_array( $connections ) ) {
			return;
		}

		foreach ( $connections as $field_name => $connection_config ) {
			if ( ! is_array( $connection_config ) ) {
				continue;
			}

			$connection_config['fromType']      = $config['name'];
			$connection_config['fromFieldName'] = $field_name;
			register_graphql_connection( $connection_config );
		}
	}

	/**
	 * Add a Type to the Registry
	 *
	 * @param string                      $type_name The name of the type to register
	 * @param array<string,mixed>|TypeDef $config The config for the type
	 *
	 * @throws \Exception
	 */
	public function register_type( string $type_name, $config ): void {
		/**
		 * If the type should be excluded from the schema, skip it.
		 */
		if ( in_array( strtolower( $type_name ), $this->get_excluded_types(), true ) ) {
			return;
		}
		/**
		 * If the Type Name starts with a number, skip it.
		 */
		if ( ! is_valid_graphql_name( $type_name ) ) {
			graphql_debug(
				sprintf(
					// translators: %s is the name of the type.
					__( 'The Type name \'%1$s\' is invalid and has not been added to the GraphQL Schema.', 'wp-graphql' ),
					$type_name
				),
				[
					'type'      => 'INVALID_TYPE_NAME',
					'type_name' => $type_name,
				]
			);
			return;
		}

		$type_key = $this->format_key( $type_name );

		// If the Type Name is already registered, skip it.
		if ( isset( $this->types[ $type_key ] ) || isset( $this->type_loaders[ $type_key ] ) ) {
			graphql_debug(
				sprintf(
					// translators: %s is the name of the type.
					__( 'You cannot register duplicate Types to the Schema. The Type \'%1$s\' already exists in the Schema. Make sure to give new Types a unique name.', 'wp-graphql' ),
					$type_name
				),
				[
					'type'      => 'DUPLICATE_TYPE',
					'type_name' => $type_name,
				]
			);
			return;
		}

		// Register the type loader.
		$this->type_loaders[ $type_key ] = function () use ( $type_name, $config ) {
			return $this->prepare_type( $type_name, $config );
		};

		// If the config isn't an array, there's nothing left to do.
		if ( ! is_array( $config ) ) {
			return;
		}

		// Register any connections that were passed through the Type config
		if ( isset( $config['connections'] ) ) {
			$config['name'] = ucfirst( $type_name ); // Other types are capitalized in the prepare_type method.
			$this->register_connections_from_config( $config );
		}

		// Load eager types if this is an introspection query.
		$should_load_eagerly = WPGraphQL::is_introspection_query() && ! empty( $config['eagerlyLoadType'] );
		if ( ! $should_load_eagerly || isset( $this->eager_type_map[ $type_key ] ) ) {
			return;
		}

		$this->eager_type_map[ $type_key ] = $type_key;
	}

	/**
	 * Add an Object Type to the Registry
	 *
	 * @param string              $type_name The name of the type to register
	 * @param array<string,mixed> $config The configuration of the type
	 *
	 * @throws \Exception
	 */
	public function register_object_type( string $type_name, array $config ): void {
		$config['kind'] = 'object';
		$this->register_type( $type_name, $config );
	}

	/**
	 * Add an Interface Type to the registry
	 *
	 * @param string              $type_name The name of the type to register
	 * @param array<string,mixed> $config The configuration of the type
	 *
	 * @throws \Exception
	 */
	public function register_interface_type( string $type_name, $config ): void {
		$config['kind'] = 'interface';
		$this->register_type( $type_name, $config );
	}

	/**
	 * Add an Enum Type to the registry
	 *
	 * @param string              $type_name The name of the type to register
	 * @param array<string,mixed> $config he configuration of the type
	 *
	 * @phpstan-param WPEnumTypeConfig $config
	 *
	 * @throws \Exception
	 */
	public function register_enum_type( string $type_name, array $config ): void {
		$config['kind'] = 'enum';
		$this->register_type( $type_name, $config );
	}

	/**
	 * Add an Input Type to the Registry
	 *
	 * @param string              $type_name The name of the type to register
	 * @param array<string,mixed> $config he configuration of the type
	 *
	 * @throws \Exception
	 */
	public function register_input_type( string $type_name, array $config ): void {
		$config['kind'] = 'input';
		$this->register_type( $type_name, $config );
	}

	/**
	 * Add a Union Type to the Registry
	 *
	 * @param string              $type_name The name of the type to register
	 * @param array<string,mixed> $config he configuration of the type
	 *
	 * @throws \Exception
	 */
	public function register_union_type( string $type_name, array $config ): void {
		$config['kind'] = 'union';
		$this->register_type( $type_name, $config );
	}

	/**
	 * Get the keys that are prepared for introspection.
	 *
	 * @return array<string>
	 */
	protected static function get_introspection_keys(): array {

		if ( null === self::$introspection_keys ) {
			/**
			 * Filter the keys that are prepared for introspection.
			 *
			 * @param array<string> $introspection_keys The keys to prepare for introspection.
			 */
			$introspection_keys       = \apply_filters( 'graphql_introspection_keys', [ 'description', 'deprecationReason' ] );
			self::$introspection_keys = $introspection_keys;
		}

		return self::$introspection_keys;
	}

	/**
	 * Prepare the config for introspection. This is used to resolve callable values for description and deprecationReason for
	 * introspection queries.
	 *
	 * @template T of array<string,mixed>
	 * @param T $config The config to prepare.
	 *
	 * @return array<string,mixed> The prepared config.
	 * @phpstan-return T|array{description?: string|null, deprecationReason?: string|null}
	 *
	 * @internal
	 */
	public static function prepare_config_for_introspection( array $config ): array {

		// Get the keys that are prepared for introspection.
		$introspection_keys = self::get_introspection_keys();

		foreach ( $introspection_keys as $key ) {
			if ( ! isset( $config[ $key ] ) || ! is_callable( $config[ $key ] ) ) {
				continue;
			}

			if ( ! WPGraphQL::is_introspection_query() ) {
				// If not introspection, set to null.
				$config[ $key ] = null;
				continue;
			}

			$config[ $key ] = is_callable( $config[ $key ] ) ? $config[ $key ]() : '';
		}

		return $config;
	}

	/**
	 * Prepare the type for registration.
	 *
	 * @template T of WPEnumTypeConfig|WPScalarConfig|array<string,mixed>
	 *
	 * @param string    $type_name The name of the type to prepare
	 * @param T|TypeDef $config    The config for the type
	 *
	 * @return ?TypeDef The prepared type
	 */
	protected function prepare_type( string $type_name, $config ) {
		if ( ! is_array( $config ) ) {
			return $config;
		}

		if ( empty( $config ) ) {
			return null;
		}

		$config         = self::prepare_config_for_introspection( $config );
		$config['name'] = ucfirst( $type_name );

		$kind = isset( $config['kind'] ) ? $config['kind'] : null;
		switch ( $kind ) {
			case 'enum':
				/** @var WPEnumTypeConfig $config */
				$prepared_type = new WPEnumType( $config );
				break;
			case 'input':
				/** @var InputObjectConfig $config */
				$prepared_type = new WPInputObjectType( $config, $this );
				break;
			case 'scalar':
				$prepared_type = new WPScalar( $config, $this );
				break;
			case 'union':
				$prepared_type = new WPUnionType( $config, $this );
				break;
			case 'interface':
				/** @var InterfaceConfig $config */
				$prepared_type = new WPInterfaceType( $config, $this );
				break;
			case 'object':
			default:
				/** @var ObjectConfig $config */
				$prepared_type = new WPObjectType( $config, $this );
		}

		return $prepared_type;
	}

	/**
	 * Given a type name, returns the type or null if not found
	 *
	 * @param string $type_name The name of the Type to get from the registry
	 *
	 * @return ?TypeDef
	 */
	public function get_type( string $type_name ) {
		$key = $this->format_key( $type_name );

		if ( isset( $this->type_loaders[ $key ] ) ) {
			$type = $this->type_loaders[ $key ]();
			/**
			 * Filter the type before it is loaded into the registry.
			 *
			 * @param ?TypeDef $type The type to load.
			 * @param string   $type_name The name of the type.
			 */
			$this->types[ $key ] = apply_filters( 'graphql_get_type', $type, $type_name );
			unset( $this->type_loaders[ $key ] );
		}

		return $this->types[ $key ] ?? null;
	}

	/**
	 * Given a type name, determines if the type is already present in the Type Loader
	 *
	 * @param string $type_name The name of the type to check the registry for
	 */
	public function has_type( string $type_name ): bool {
		return isset( $this->type_loaders[ $this->format_key( $type_name ) ] );
	}

	/**
	 * Return the Types in the registry
	 *
	 * @return TypeDef[]
	 */
	public function get_types(): array {
		// The full map of types is merged with eager types to support the
		// rename_graphql_type API.
		//
		// All of the types are closures, but eager Types are the full
		// Type definitions up front
		return array_filter( array_merge( $this->types, $this->get_eager_type_map() ) );
	}

	/**
	 * Wrapper for prepare_field to prepare multiple fields for registration at once
	 *
	 * @param array<string,mixed> $fields    Array of fields and their settings to register on a Type
	 * @param string              $type_name Name of the Type to register the fields to
	 *
	 * @return array<string,mixed>
	 * @throws \Exception
	 */
	public function prepare_fields( array $fields, string $type_name ): array {
		$prepared_fields = [];
		foreach ( $fields as $field_name => $field_config ) {
			if ( is_array( $field_config ) && isset( $field_config['type'] ) ) {
				$prepared_field = $this->prepare_field( $field_name, $field_config, $type_name );
				if ( ! empty( $prepared_field ) ) {
					$prepared_fields[ $this->format_key( $field_name ) ] = $prepared_field;
				}
			}
		}

		return $prepared_fields;
	}

	/**
	 * Prepare the field to be registered on the type
	 *
	 * @param string              $field_name   Friendly name of the field
	 * @param array<string,mixed> $field_config Config data about the field to prepare
	 * @param string              $type_name    Name of the type to prepare the field for
	 *
	 * @return ?array<string,mixed>
	 * @throws \Exception
	 */
	protected function prepare_field( string $field_name, array $field_config, string $type_name ): ?array {
		if ( ! isset( $field_config['name'] ) ) {
			$field_config['name'] = lcfirst( $field_name );
		}

		if ( ! isset( $field_config['type'] ) ) {
			graphql_debug(
				sprintf(
					/* translators: %s is the Field name. */
					__( 'The registered field \'%s\' does not have a Type defined. Make sure to define a type for all fields.', 'wp-graphql' ),
					$field_name
				),
				[
					'type'       => 'INVALID_FIELD_TYPE',
					'type_name'  => $type_name,
					'field_name' => $field_name,
				]
			);
			return null;
		}

		/**
		 * If the type is a string, create a callable wrapper to get the type from
		 * type registry. This preserves lazy-loading and prevents a bug where a type
		 * has the same name as a function in the global scope (e.g., `header()`) and
		 * is called since it passes `is_callable`.
		 */
		if ( is_string( $field_config['type'] ) ) {
			// Bail if the type is excluded from the Schema.
			if ( in_array( strtolower( $field_config['type'] ), $this->get_excluded_types(), true ) ) {
				return null;
			}

			$field_config['type'] = function () use ( $field_config, $type_name ) {
				$type = $this->get_type( $field_config['type'] );
				if ( ! $type ) {
					$message = sprintf(
					/* translators: %1$s is the Field name, %2$s is the type name the field belongs to. %3$s is the non-existent type name being referenced. */
						__( 'The field \'%1$s\' on Type \'%2$s\' is configured to return \'%3$s\' which is a non-existent Type in the Schema. Make sure to define a valid type for all fields. This might occur if there was a typo with \'%3$s\', or it needs to be registered to the Schema.', 'wp-graphql' ),
						$field_config['name'],
						$type_name,
						$field_config['type']
					);
					// We throw an error here instead of graphql_debug message, as an error would already be thrown if a type didn't exist at this point,
					// but now it will have a more helpful error message.
					throw new Error( esc_html( $message ) );
				}
				return $type;
			};
		}

		/**
		 * If the type is an array, it contains type modifiers (e.g., "non_null").
		 * Create a callable wrapper to preserve lazy-loading.
		 */
		if ( is_array( $field_config['type'] ) ) {
			// Bail if the type is excluded from the Schema.
			$unmodified_type_name = $this->get_unmodified_type_name( $field_config['type'] );

			if ( empty( $unmodified_type_name ) || in_array( strtolower( $unmodified_type_name ), $this->get_excluded_types(), true ) ) {
				return null;
			}

			$field_config['type'] = function () use ( $field_config ) {
				return $this->setup_type_modifiers( $field_config['type'] );
			};
		}

		/**
		 * If the field has arguments, each one must be prepared.
		 */
		if ( isset( $field_config['args'] ) && is_array( $field_config['args'] ) ) {
			foreach ( $field_config['args'] as $arg_name => $arg_config ) {
				$arg = $this->prepare_field( $arg_name, $arg_config, $type_name );

				// Remove the arg if the field could not be prepared.
				if ( empty( $arg ) ) {
					unset( $field_config['args'][ $arg_name ] );
					continue;
				}

				$field_config['args'][ $arg_name ] = $arg;
			}
		}

		/**
		 * If the field has no (remaining) valid arguments, unset the key.
		 */
		if ( empty( $field_config['args'] ) ) {
			unset( $field_config['args'] );
		}

		$field_config = self::prepare_config_for_introspection( $field_config );

		return $field_config;
	}

	/**
	 * Processes type modifiers (e.g., "non-null"). Loads types immediately, so do
	 * not call before types are ready to be loaded.
	 *
	 * @template WrappedType of array{non_null:mixed}|array{list_of:mixed}
	 * @param WrappedType|array<string,mixed>|string|\GraphQL\Type\Definition\Type $type The type to process.
	 *
	 * @return ($type is WrappedType ? \GraphQL\Type\Definition\Type : (array<string,mixed>|string|\GraphQL\Type\Definition\Type))
	 * @throws \Exception
	 */
	public function setup_type_modifiers( $type ) {
		if ( ! is_array( $type ) ) {
			return $type;
		}

		if ( isset( $type['non_null'] ) ) {
			/** @var TypeDef inner_type */
			$inner_type = $this->setup_type_modifiers( $type['non_null'] );
			return $this->non_null( $inner_type );
		}

		if ( isset( $type['list_of'] ) ) {
			/** @var TypeDef $inner_type */
			$inner_type = $this->setup_type_modifiers( $type['list_of'] );
			return $this->list_of( $inner_type );
		}

		return $type;
	}

	/**
	 * Wrapper for the register_field method to register multiple fields at once
	 *
	 * @param string                            $type_name Name of the type in the Type Registry to add the fields to
	 * @param array<string,array<string,mixed>> $fields    Fields to register
	 *
	 * @throws \Exception
	 */
	public function register_fields( string $type_name, array $fields = [] ): void {
		if ( ! empty( $fields ) ) {
			foreach ( $fields as $field_name => $config ) {
				if ( is_string( $field_name ) && ! empty( $config ) && is_array( $config ) ) {
					$this->register_field( $type_name, $field_name, $config );
				}
			}
		}
	}

	/**
	 * Add a field to a Type in the Type Registry
	 *
	 * @param string              $type_name  Name of the type in the Type Registry to add the fields to
	 * @param string              $field_name Name of the field to add to the type
	 * @param array<string,mixed> $config     Info about the field to register to the type
	 *
	 * @throws \Exception
	 */
	public function register_field( string $type_name, string $field_name, array $config ): void {
		add_filter(
			'graphql_' . $type_name . '_fields',
			function ( $fields, $wp_type = null ) use ( $type_name, $field_name, $config ) {

				// Whether the field should be allowed to have underscores in the field name
				$allow_field_underscores = isset( $config['allowFieldUnderscores'] ) && true === $config['allowFieldUnderscores'];

				$field_name = Utils::format_field_name( $field_name, $allow_field_underscores );

				if ( preg_match( '/^\d/', $field_name ) ) {
					graphql_debug(
						sprintf(
							// translators: %1$s is the field name, %2$s is the type name.
							__( 'The field \'%1$s\' on Type \'%2$s\' is invalid. Field names cannot start with a number.', 'wp-graphql' ),
							$field_name,
							$type_name
						),
						[
							'type'       => 'INVALID_FIELD_NAME',
							'field_name' => $field_name,
							'type_name'  => $type_name,
						]
					);
					return $fields;
				}

				// if a field has already been registered with the same name output a debug message
				if ( isset( $fields[ $field_name ] ) ) {
					// If we're already inside a compatibility check (an outer call to
					// is_compatible_interface_field_override resolved a type via get_type
					// and re-entered this filter chain), the outer pass has already decided
					// the override outcome. Re-running the check here cannot reach a different
					// answer, so silently no-op to avoid spurious DUPLICATE_FIELD debug noise.
					if ( $this->checking_compatibility ) {
						return $fields;
					}

					$existing_field_type = $fields[ $field_name ]['type'] ?? null;
					$new_field_type      = $config['type'] ?? null;

					// if the existing field is a connection type
					// and the new field is also a connection type
					// and the toType is the same for both
					// then we can allow the duplicate field
					if (
						isset(
							$fields[ $field_name ]['isConnectionField'],
							$config['isConnectionField'],
							$fields[ $field_name ]['toType'],
							$config['toType'],
							$fields[ $field_name ]['connectionTypeName'],
							$config['connectionTypeName']
						) &&
						$fields[ $field_name ]['toType'] === $config['toType'] &&
						$fields[ $field_name ]['connectionTypeName'] === $config['connectionTypeName']
					) {
						return $fields;
					}

					// Check if this is an interface field override scenario and if the new type is compatible.
					$is_compatible_override = $this->is_compatible_interface_field_override( $existing_field_type, $new_field_type, $type_name, $field_name, $wp_type );

					// If the override is compatible, allow it by returning early
					if ( $is_compatible_override ) {
						// Prepare and add the new field, overriding the interface field
						$field = $this->prepare_field( $field_name, $config, $type_name );
						if ( ! empty( $field ) ) {
							$fields[ $field_name ] = $field;
						}
						return $fields;
					}

					graphql_debug(
						sprintf(
							// translators: %1$s is the field name, %2$s is the type name.
							__( 'You cannot register duplicate fields on the same Type. The field \'%1$s\' already exists on the type \'%2$s\'. Make sure to give the field a unique name.', 'wp-graphql' ),
							$field_name,
							$type_name
						),
						[
							'type'            => 'DUPLICATE_FIELD',
							'field_name'      => $field_name,
							'type_name'       => $type_name,
							'existing_field'  => $fields[ $field_name ],
							'duplicate_field' => $config,
						]
					);
					return $fields;
				}

				/**
				 * If the field returns a properly prepared field, add it the the field registry
				 */
				$field = $this->prepare_field( $field_name, $config, $type_name );

				if ( ! empty( $field ) ) {
					$fields[ $field_name ] = $field;
				}

				return $fields;
			},
			10,
			2
		);
	}

	/**
	 * Determines if a duplicate field is a compatible interface-field override.
	 *
	 * Two paths are checked:
	 *  1. The existing field's declared type is (or wraps) an interface, and the
	 *     override's type implements that interface.
	 *  2. The existing field was inherited from one of the type's implemented
	 *     interfaces, and the override's type is structurally compatible with the
	 *     interface's declared field type (same wrappers, same base type, or base
	 *     implementing the interface field's base when that base is an interface).
	 *
	 * @param mixed                                    $existing_field_type Existing field type definition.
	 * @param mixed                                    $new_field_type      New field type definition.
	 * @param string                                   $type_name           Name of the type the field belongs to.
	 * @param string                                   $field_name          Name of the field being registered (used for Path 2).
	 * @param \GraphQL\Type\Definition\ObjectType|null $wp_type             The implementing object type instance (used for Path 2).
	 */
	private function is_compatible_interface_field_override( $existing_field_type, $new_field_type, string $type_name, string $field_name = '', $wp_type = null ): bool {
		if ( ! ( is_callable( $existing_field_type ) || is_string( $existing_field_type ) || is_array( $existing_field_type ) ) ) {
			return false;
		}

		if ( ! ( is_string( $new_field_type ) || ( is_array( $new_field_type ) && ! empty( $new_field_type ) ) ) ) {
			return false;
		}

		$unmodified_new_type_name = is_string( $new_field_type ) ? $new_field_type : $this->get_unmodified_type_name( $new_field_type );
		if ( empty( $unmodified_new_type_name ) || $this->checking_compatibility ) {
			return false;
		}

		$current_type_key       = $this->format_key( $type_name );
		$new_type_key           = $this->format_key( $unmodified_new_type_name );
		$is_same_type           = $new_type_key === $current_type_key;
		$current_type_is_loaded = isset( $this->types[ $current_type_key ] );
		$new_type_is_loaded     = isset( $this->types[ $new_type_key ] );
		$new_type_is_registered = $new_type_is_loaded || isset( $this->type_loaders[ $new_type_key ] );

		// For a different type we need a resolvable type.
		if ( ! $is_same_type && ! $new_type_is_registered ) {
			return false;
		}

		$this->checking_compatibility = true;

		try {
			// Path 1: the existing field's TYPE is (or wraps) an interface.
			$interface_name = $this->resolve_interface_name_from_existing_field_type( $existing_field_type, $current_type_key );
			if ( ! empty( $interface_name ) ) {
				// Same-type overrides can happen while the object is still being constructed.
				// If we positively identified an interface source, allow to avoid recursive loads.
				if ( $is_same_type && ! $current_type_is_loaded ) {
					return true;
				}

				return $this->type_implements_interface( $unmodified_new_type_name, $interface_name );
			}

			// Path 2: the field was inherited from one of the type's implemented interfaces.
			// This covers scenarios where the field's *type* is a concrete/scalar (e.g. inheriting
			// `tag: String` or `related: Post`) but the field itself came from an interface contract.
			if ( $wp_type instanceof \GraphQL\Type\Definition\ObjectType && '' !== $field_name ) {
				$interface_field_type = $this->find_inherited_interface_field_type( $wp_type, $field_name );
				if ( null !== $interface_field_type ) {
					return $this->is_override_compatible_with_interface_field_type( $new_field_type, $interface_field_type );
				}
			}

			return false;
		} catch ( \Throwable $e ) {
			unset( $e );
			return false;
		} finally {
			$this->checking_compatibility = false;
		}
	}

	/**
	 * Walk the type's implemented interfaces and return the interface's declared field
	 * type for the first interface that declares a field with the given name. Returns
	 * null if no interface declares the field.
	 *
	 * @param \GraphQL\Type\Definition\ObjectType $wp_type    The implementing object type.
	 * @param string                              $field_name The field name to look up.
	 */
	private function find_inherited_interface_field_type( \GraphQL\Type\Definition\ObjectType $wp_type, string $field_name ): ?\GraphQL\Type\Definition\Type {
		foreach ( $wp_type->getInterfaces() as $interface ) {
			if ( ! $interface instanceof \GraphQL\Type\Definition\InterfaceType ) {
				continue;
			}

			try {
				if ( ! $interface->hasField( $field_name ) ) {
					continue;
				}

				return $interface->getField( $field_name )->getType();
			} catch ( \Throwable $e ) {
				unset( $e );
				continue;
			}
		}

		return null;
	}

	/**
	 * Compare an override field type (config-array syntax) with an interface's declared
	 * field type (live Type instance) for structural compatibility.
	 *
	 * Wrapping (list_of, non_null) must match exactly. The base type must either be the
	 * same named type, or — when the interface's base is itself an interface — be a
	 * concrete type that implements that interface.
	 *
	 * @param mixed                         $override_type        Override field type in config-array syntax.
	 * @param \GraphQL\Type\Definition\Type $interface_field_type Interface's declared field type as a live Type instance.
	 */
	private function is_override_compatible_with_interface_field_type( $override_type, \GraphQL\Type\Definition\Type $interface_field_type ): bool {
		$override = $this->unwrap_field_type_config( $override_type );
		if ( null === $override ) {
			return false;
		}

		$interface = $this->unwrap_field_type_instance( $interface_field_type );

		if ( $override['wrappers'] !== $interface['wrappers'] ) {
			return false;
		}

		$interface_base = $interface['base'];

		// After unwrapping all WrappingType layers, the base should be a NamedType, but
		// guard explicitly so static analysis can narrow the type before reading ->name.
		if ( ! $interface_base instanceof \GraphQL\Type\Definition\NamedType ) {
			return false;
		}

		if ( strtolower( $override['base'] ) === strtolower( $interface_base->name ) ) {
			return true;
		}

		if ( $interface_base instanceof \GraphQL\Type\Definition\InterfaceType ) {
			return $this->type_implements_interface( $override['base'], $interface_base->name );
		}

		return false;
	}

	/**
	 * Normalize a field-type definition in config-array syntax to `{wrappers, base}`.
	 *
	 * Wrappers are returned outer-to-inner (e.g. `['non_null','list_of']`). Returns null
	 * for malformed input.
	 *
	 * @param mixed $type Field type in WPGraphQL config syntax (string, or nested array
	 *                    using 'non_null'/'list_of' keys).
	 *
	 * @return array{wrappers:string[],base:string}|null
	 */
	private function unwrap_field_type_config( $type ): ?array {
		$wrappers = [];

		while ( is_array( $type ) ) {
			if ( isset( $type['non_null'] ) ) {
				$wrappers[] = 'non_null';
				$type       = $type['non_null'];
				continue;
			}

			if ( isset( $type['list_of'] ) ) {
				$wrappers[] = 'list_of';
				$type       = $type['list_of'];
				continue;
			}

			return null;
		}

		if ( ! is_string( $type ) || '' === $type ) {
			return null;
		}

		return [
			'wrappers' => $wrappers,
			'base'     => $type,
		];
	}

	/**
	 * Normalize a live Type instance to `{wrappers, base}`.
	 *
	 * @param \GraphQL\Type\Definition\Type $type Live Type instance, possibly wrapped.
	 *
	 * @return array{wrappers:string[],base:\GraphQL\Type\Definition\Type}
	 */
	private function unwrap_field_type_instance( \GraphQL\Type\Definition\Type $type ): array {
		$wrappers = [];

		while ( $type instanceof \GraphQL\Type\Definition\WrappingType ) {
			if ( $type instanceof \GraphQL\Type\Definition\NonNull ) {
				$wrappers[] = 'non_null';
			} elseif ( $type instanceof \GraphQL\Type\Definition\ListOfType ) {
				$wrappers[] = 'list_of';
			}

			$type = $type->getWrappedType();
		}

		return [
			'wrappers' => $wrappers,
			'base'     => $type,
		];
	}

	/**
	 * Resolve the interface name from an existing field type definition.
	 *
	 * @param mixed  $existing_field_type Existing field type definition.
	 * @param string $current_type_key    Type key currently being resolved.
	 */
	private function resolve_interface_name_from_existing_field_type( $existing_field_type, string $current_type_key ): ?string {
		if ( is_callable( $existing_field_type ) ) {
			$resolved_existing_type = $existing_field_type();

			while ( $resolved_existing_type instanceof \GraphQL\Type\Definition\WrappingType ) {
				$resolved_existing_type = $resolved_existing_type->getWrappedType();
			}

			if ( $resolved_existing_type instanceof \GraphQL\Type\Definition\InterfaceType ) {
				return $resolved_existing_type->name;
			}
		}

		if ( is_string( $existing_field_type ) || is_array( $existing_field_type ) ) {
			$existing_type_name = $this->get_unmodified_type_name( $existing_field_type );
			return $this->resolve_interface_name_from_type_name( $existing_type_name, $current_type_key );
		}

		return null;
	}

	/**
	 * Resolves a type name to an interface name when possible.
	 *
	 * @param string $type_name        Type name to resolve.
	 * @param string $current_type_key Type key currently being resolved.
	 */
	private function resolve_interface_name_from_type_name( string $type_name, string $current_type_key ): ?string {
		if ( empty( $type_name ) ) {
			return null;
		}

		$type_key = $this->format_key( $type_name );

		if ( isset( $this->types[ $type_key ] ) ) {
			$loaded_type = $this->types[ $type_key ];
			if ( $loaded_type instanceof \GraphQL\Type\Definition\InterfaceType ) {
				return $loaded_type->name;
			}

			return null;
		}

		// Avoid recursively resolving the type currently being constructed.
		if ( $type_key === $current_type_key || ! isset( $this->type_loaders[ $type_key ] ) ) {
			return null;
		}

		$maybe_interface = $this->get_type( $type_name );
		if ( $maybe_interface instanceof \GraphQL\Type\Definition\InterfaceType ) {
			return $maybe_interface->name;
		}

		return null;
	}

	/**
	 * Determines if a type implements a given interface.
	 *
	 * @param string $type_name      Type name to inspect.
	 * @param string $interface_name Interface name to verify.
	 */
	private function type_implements_interface( string $type_name, string $interface_name ): bool {
		$type_key = $this->format_key( $type_name );
		$type     = $this->types[ $type_key ] ?? $this->get_type( $type_name );

		if ( ! $type instanceof \GraphQL\Type\Definition\ObjectType ) {
			return false;
		}

		foreach ( $type->getInterfaces() as $interface ) {
			if ( $interface->name === $interface_name ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Remove a field from a type
	 *
	 * @param string $type_name  Name of the Type the field is registered to
	 * @param string $field_name Name of the field you would like to remove from the type
	 *
	 * @return void
	 */
	public function deregister_field( string $type_name, string $field_name ) {
		add_filter(
			'graphql_' . $type_name . '_fields',
			static function ( $fields ) use ( $field_name ) {
				if ( isset( $fields[ $field_name ] ) ) {
					unset( $fields[ $field_name ] );
				}

				return $fields;
			}
		);
	}

	/**
	 * Method to register a new connection in the Type registry
	 *
	 * @param array<string,mixed> $config The info about the connection being registered
	 *
	 * @throws \InvalidArgumentException
	 * @throws \Exception
	 */
	public function register_connection( array $config ): void {
		new WPConnectionType( $config, $this );
	}

	/**
	 * Handles registration of a mutation to the Type registry
	 *
	 * @param string              $mutation_name Name of the mutation being registered
	 * @param array<string,mixed> $config        Info about the mutation being registered
	 *
	 * @throws \Exception
	 */
	public function register_mutation( string $mutation_name, array $config ): void {
		// Bail if the mutation has been excluded from the schema.
		if ( in_array( strtolower( $mutation_name ), $this->get_excluded_mutations(), true ) ) {
			return;
		}

		$config['name'] = $mutation_name;
		new WPMutationType( $config, $this );
	}

	/**
	 * Removes a GraphQL mutation from the schema.
	 *
	 * This works by preventing the mutation from being registered in the first place.
	 *
	 * @uses 'graphql_excluded_mutations' filter.
	 *
	 * @param string $mutation_name Name of the mutation to remove from the schema.
	 *
	 * @since 1.14.0
	 */
	public function deregister_mutation( string $mutation_name ): void {
		// Prevent the mutation from being registered to the scheme directly.
		add_filter(
			'graphql_excluded_mutations',
			static function ( $excluded_mutations ) use ( $mutation_name ): array {
				// Normalize the types to prevent case sensitivity issues.
				$mutation_name = strtolower( $mutation_name );
				// If the type isn't already excluded, add it to the array.
				if ( ! in_array( $mutation_name, $excluded_mutations, true ) ) {
					$excluded_mutations[] = $mutation_name;
				}

				return $excluded_mutations;
			},
			10
		);
	}

	/**
	 * Removes a GraphQL connection from the schema.
	 *
	 * This works by preventing the connection from being registered in the first place.
	 *
	 * @uses 'graphql_excluded_connections' filter.
	 *
	 * @param string $connection_name The GraphQL connection name.
	 */
	public function deregister_connection( string $connection_name ): void {
		add_filter(
			'graphql_excluded_connections',
			static function ( $excluded_connections ) use ( $connection_name ) {
				$connection_name = strtolower( $connection_name );

				if ( ! in_array( $connection_name, $excluded_connections, true ) ) {
					$excluded_connections[] = $connection_name;
				}

				return $excluded_connections;
			}
		);
	}

	/**
	 * Given a Type, this returns an instance of a NonNull of that type.
	 *
	 * @template T of \GraphQL\Type\Definition\NullableType&\GraphQL\Type\Definition\Type
	 * @param T|string $type The Type being wrapped.
	 */
	public function non_null( $type ): \GraphQL\Type\Definition\NonNull {
		if ( is_string( $type ) ) {
			$type_def = $this->get_type( $type );

			/** @phpstan-var T&TypeDef $type_def */
			return Type::nonNull( $type_def );
		}

		return Type::nonNull( $type );
	}

	/**
	 * Given a Type, this returns an instance of a listOf of that type.
	 *
	 * @template T of \GraphQL\Type\Definition\Type
	 * @param T|string $type The Type being wrapped.
	 *
	 * @return \GraphQL\Type\Definition\ListOfType<\GraphQL\Type\Definition\Type>
	 */
	public function list_of( $type ): \GraphQL\Type\Definition\ListOfType {
		if ( is_string( $type ) ) {
			$resolved_type = $this->get_type( $type );

			if ( is_null( $resolved_type ) ) {
				$resolved_type = Type::string();
			}

			$type = $resolved_type;
		}

		return Type::listOf( $type );
	}

	/**
	 * Get the list of GraphQL type names to exclude from the schema.
	 *
	 * Type names are normalized using `strtolower()`, to avoid case sensitivity issues.
	 *
	 * @since 1.13.0
	 *
	 * @return string[]
	 */
	public function get_excluded_types(): array {
		if ( null === $this->excluded_types ) {
			/**
			 * Filter the list of GraphQL types to exclude from the schema.
			 *
			 * Note: using this filter directly will NOT remove the type from being referenced as a possible interface or a union type.
			 * To remove a GraphQL from the schema **entirely**, please use deregister_graphql_type();
			 *
			 * @param string[] $excluded_types The names of the GraphQL Types to exclude.
			 *
			 * @since 1.13.0
			 */
			$excluded_types = apply_filters( 'graphql_excluded_types', [] );

			// Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
			$this->excluded_types = ! empty( $excluded_types ) ? array_map( 'strtolower', $excluded_types ) : [];
		}

		return $this->excluded_types;
	}

	/**
	 * Get the list of GraphQL connections to exclude from the schema.
	 *
	 * Type names are normalized using `strtolower()`, to avoid case sensitivity issues.
	 *
	 * @return string[]
	 *
	 * @since 1.14.0
	 */
	public function get_excluded_connections(): array {
		if ( null === $this->excluded_connections ) {
			/**
			 * Filter the list of GraphQL connections to excluded from the registry.
			 *
			 * @param string[] $excluded_connections The names of the GraphQL connections to exclude.
			 *
			 * @since 1.14.0
			 */
			$excluded_connections = apply_filters( 'graphql_excluded_connections', [] );

			// Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
			$this->excluded_connections = ! empty( $excluded_connections ) ? array_map( 'strtolower', $excluded_connections ) : [];
		}

		return $this->excluded_connections;
	}

	/**
	 * Get the list of GraphQL mutation names to exclude from the schema.
	 *
	 * Mutation names are normalized using `strtolower()`, to avoid case sensitivity issues.
	 *
	 * @return string[]
	 * @since 1.14.0
	 */
	public function get_excluded_mutations(): array {
		if ( null === $this->excluded_mutations ) {
			/**
			 * Filter the list of GraphQL mutations to excluded from the registry.
			 *
			 * @param string[] $excluded_mutations The names of the GraphQL mutations to exclude.
			 *
			 * @since 1.14.0
			 */
			$excluded_mutations = apply_filters( 'graphql_excluded_mutations', [] );

			// Normalize the types to be lowercase, to avoid case-sensitivity issue when comparing.
			$this->excluded_mutations = ! empty( $excluded_mutations ) ? array_map( 'strtolower', $excluded_mutations ) : [];
		}

		return $this->excluded_mutations;
	}

	/**
	 * Gets the actual type name, stripped of possible NonNull and ListOf wrappers.
	 *
	 * Returns an empty string if the type modifiers are malformed.
	 *
	 * @param string|array<string|int,mixed> $type The (possibly-wrapped) type name.
	 */
	protected function get_unmodified_type_name( $type ): string {
		if ( ! is_array( $type ) ) {
			return $type;
		}

		$type = array_values( $type )[0] ?? '';

		return $this->get_unmodified_type_name( $type );
	}
}
