<?php

namespace WPGraphQL;

use GraphQL\Error\DebugFlag;
use GraphQL\Error\Error;
use GraphQL\GraphQL;
use GraphQL\Server\OperationParams;
use GraphQL\Server\ServerConfig;
use GraphQL\Server\StandardServer;
use WPGraphQL\Server\ValidationRules\DisableIntrospection;
use WPGraphQL\Server\ValidationRules\QueryDepth;
use WPGraphQL\Server\ValidationRules\RequireAuthentication;
use WPGraphQL\Server\WPHelper;
use WPGraphQL\Utils\DebugLog;
use WPGraphQL\Utils\QueryAnalyzer;

/**
 * Class Request
 *
 * Proxies a request to graphql-php, applying filters and transforming request
 * data as needed.
 *
 * @package WPGraphQL
 *
 * phpcs:disable -- PHPStan annotation.
 * @phpstan-import-type RootValueResolver from \GraphQL\Server\ServerConfig
 * @phpstan-import-type SerializableResult from \GraphQL\Executor\ExecutionResult
 * phpcs:enable
 */
class Request {

	/**
	 * App context for this request.
	 *
	 * @var \WPGraphQL\AppContext
	 */
	public $app_context;

	/**
	 * Request data.
	 *
	 * @var array<string,mixed>|\GraphQL\Server\OperationParams
	 */
	public $data;

	/**
	 * Cached global post.
	 *
	 * @var ?\WP_Post
	 */
	public $global_post;

	/**
	 * Cached global wp_the_query.
	 *
	 * @var ?\WP_Query
	 */
	private $global_wp_the_query;

	/**
	 * GraphQL operation parameters for this request.
	 * Will be an array of OperationParams if this is a batch request.
	 *
	 * @var \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[]
	 */
	public $params;

	/**
	 * Schema for this request.
	 *
	 * @var \WPGraphQL\WPSchema
	 */
	public $schema;

	/**
	 * Debug log for WPGraphQL Requests
	 *
	 * @var \WPGraphQL\Utils\DebugLog
	 */
	public $debug_log;

	/**
	 * The Type Registry the Schema is built with
	 *
	 * @var \WPGraphQL\Registry\TypeRegistry
	 */
	public $type_registry;

	/**
	 * Validation rules for execution.
	 *
	 * @var array<string,\GraphQL\Validator\Rules\ValidationRule>
	 */
	protected $validation_rules;

	/**
	 * The default field resolver function. Default null
	 *
	 * @var callable|null
	 */
	protected $field_resolver;

	/**
	 * The root value of the request. Default null;
	 *
	 * @var mixed|RootValueResolver
	 */
	protected $root_value;

	/**
	 * @var \WPGraphQL\Utils\QueryAnalyzer
	 */
	protected $query_analyzer;

	/**
	 * Authentication error stored during before_execute().
	 * If set, the request should return this error instead of executing the query.
	 *
	 * @var \WP_Error|bool|null
	 */
	protected $authentication_error = null;

	/**
	 * Constructor
	 *
	 * @param array<string,mixed> $data The request data (for Non-HTTP requests).
	 *
	 * @return void
	 *
	 * @throws \Exception
	 */
	public function __construct( array $data = [] ) {

		/**
		 * Whether it's a GraphQL Request (http or internal)
		 *
		 * @since 0.0.5
		 */
		if ( ! defined( 'GRAPHQL_REQUEST' ) ) {
			define( 'GRAPHQL_REQUEST', true );
		}

		/**
		 * Filter "is_graphql_request" to return true
		 */
		\WPGraphQL::set_is_graphql_request( true );

		/**
		 * Action – intentionally with no context – to indicate a GraphQL Request has started.
		 * This is a great place for plugins to hook in and modify things that should only
		 * occur in the context of a GraphQL Request. The base class hooks into this action to
		 * kick off the schema creation, so types are not set up until this action has run!
		 */
		/**
		 * Action – intentionally with no context – to indicate a GraphQL Request has started.
		 *
		 * @hookGroup request-lifecycle
		 * @since 0.0.32
		 */
		do_action( 'init_graphql_request' );

		// Start tracking debug log messages
		$this->debug_log = new DebugLog();

		// Set request data for passed-in (non-HTTP) requests.
		$this->data = $data;

		// Get the Type Registry
		$this->type_registry = \WPGraphQL::get_type_registry();

		// Get the App Context
		$this->app_context = \WPGraphQL::get_app_context();

		$this->validation_rules = $this->get_validation_rules();
		$this->field_resolver   = $this->get_field_resolver();

		// Inject the type registry into the app context.
		$this->app_context->type_registry = $this->type_registry;

		// The query analyzer tracks nodes, models, list types and more
		// to return in headers and debug messages to help developers understand
		// what was resolved, how to cache it, etc.
		$this->query_analyzer = new QueryAnalyzer( $this );
		$this->query_analyzer->init();
	}

	/**
	 * Get the instance of the Query Analyzer
	 */
	public function get_query_analyzer(): QueryAnalyzer {
		return $this->query_analyzer;
	}

	/**
	 * @return callable|null
	 */
	protected function get_field_resolver() {
		return $this->field_resolver;
	}

	/**
	 * Return the validation rules to use in the request
	 *
	 * @return array<string,\GraphQL\Validator\Rules\ValidationRule>
	 */
	protected function get_validation_rules(): array {
		$validation_rules = GraphQL::getStandardValidationRules();

		$validation_rules['require_authentication'] = new RequireAuthentication();
		$validation_rules['disable_introspection']  = new DisableIntrospection();
		$validation_rules['query_depth']            = new QueryDepth();

		/**
		 * Return the validation rules to use in the request
		 *
		 * @param array<string,\GraphQL\Validator\Rules\ValidationRule> $validation_rules The validation rules to use in the request
		 * @param \WPGraphQL\Request                                        $request          The Request instance
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		return apply_filters( 'graphql_validation_rules', $validation_rules, $this );
	}

	/**
	 * Returns the root value to use in the request.
	 *
	 * @return mixed|RootValueResolver|null
	 */
	protected function get_root_value() {
		/**
		 * Set the root value based on what was passed to the request
		 */
		$root_value = is_array( $this->data ) && ! empty( $this->data['root_value'] ) ? $this->data['root_value'] : null;

		/**
		 * Return the filtered root value
		 *
		 * @param mixed|RootValueResolver $root_value The root value the Schema should use to resolve with. Default null.
		 * @param \WPGraphQL\Request      $request    The Request instance
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		return apply_filters( 'graphql_root_value', $root_value, $this );
	}

	/**
	 * Apply filters and do actions before GraphQL execution
	 *
	 * @throws \GraphQL\Error\Error
	 */
	private function before_execute(): void {

		/**
		 * Store the global post so that it can be reset after GraphQL execution
		 *
		 * This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
		 * without disrupting the flow of the post as the global POST before and after GraphQL execution will be
		 * the same.
		 */
		if ( ! empty( $GLOBALS['post'] ) ) {
			$this->global_post = $GLOBALS['post'];
		}

		if ( ! empty( $GLOBALS['wp_query'] ) && $GLOBALS['wp_the_query'] instanceof \WP_Query ) {
			$this->global_wp_the_query = clone $GLOBALS['wp_the_query'];
		}

		/**
		 * Reset authentication error state for this execution.
		 *
		 * This ensures each batch item starts with clean auth state, preventing
		 * errors from one batch item incorrectly persisting to subsequent items.
		 *
		 * @since 2.5.4
		 */
		$this->authentication_error = null;

		/**
		 * Check for authentication errors via the graphql_authentication_errors filter.
		 *
		 * Note: For HTTP requests, all CSRF protection and nonce validation is
		 * handled by Router::validate_http_request_authentication() before this
		 * code runs. This call allows plugins to hook in and indicate auth errors.
		 *
		 * @since 2.5.4 CSRF protection and nonce validation moved to Router.
		 */
		$auth_error = $this->has_authentication_errors();

		if ( false !== $auth_error ) {
			// Store the authentication error for later use in execute methods
			$this->authentication_error = $auth_error;
		}

		/**
		 * Update AppContext->viewer to reflect the current user after auth check.
		 *
		 * If the user was downgraded due to missing nonce (CSRF protection),
		 * the viewer should reflect the guest user, not the originally authenticated user.
		 *
		 * @since 2.6.0
		 */
		$this->app_context->viewer = wp_get_current_user();

		/**
		 * If the request is a batch request it will come back as an array
		 */
		if ( is_array( $this->params ) ) {

			// If the request is a batch request, but batch requests are disabled,
			// bail early
			if ( ! $this->is_batch_queries_enabled() ) {
				throw new Error( esc_html__( 'Batch Queries are not supported', 'wp-graphql' ) );
			}

			$batch_limit = get_graphql_setting( 'batch_limit', 10 );
			$batch_limit = absint( $batch_limit ) ? absint( $batch_limit ) : 10;

			// If batch requests are enabled, but a limit is set and the request exceeds the limit
			// fail now
			if ( $batch_limit < count( $this->params ) ) {
				// translators: First placeholder is the max number of batch operations allowed in a GraphQL request. The 2nd placeholder is the number of operations requested in the current request.
				throw new Error( sprintf( esc_html__( 'Batch requests are limited to %1$d operations. This request contained %2$d', 'wp-graphql' ), absint( $batch_limit ), count( $this->params ) ) );
			}

			/**
			 * Execute batch queries
			 *
			 * @param \GraphQL\Server\OperationParams[] $params The operation params of the batch request
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
			 */
			do_action( 'graphql_execute_batch_queries', $this->params );

			// Process the batched requests
			array_walk( $this->params, [ $this, 'do_action' ] );
		} else {
			$this->do_action( $this->params );
		}

		// Get the Schema
		$this->schema = \WPGraphQL::get_schema();

		/**
		 * This action runs before execution of a GraphQL request (regardless if it's a single or batch request)
		 *
		 * @param \WPGraphQL\Request $request The instance of the Request being executed
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		do_action( 'graphql_before_execute', $this );
	}

	/**
	 * Checks authentication errors via the graphql_authentication_errors filter.
	 *
	 * As of 2.6.0, all CSRF protection and nonce validation for HTTP requests is
	 * handled by Router::validate_http_request_authentication() BEFORE any GraphQL
	 * hooks fire. This method now only provides:
	 * - Plugin integration via the graphql_authentication_errors filter
	 *
	 * False means no errors and execution continues.
	 * True or WP_Error prevents execution of the GraphQL request.
	 *
	 * @since 0.0.5
	 * @since 2.6.0 CSRF protection and nonce validation moved to Router.
	 *
	 * @return bool|\WP_Error False if no errors, true or WP_Error if there are errors.
	 *
	 * @see Router::validate_http_request_authentication()
	 */
	protected function has_authentication_errors() {
		return $this->filtered_authentication_errors( false );
	}

	/**
	 * Filter Authentication errors. Allows plugins that authenticate to hook in and prevent
	 * execution if Authentication errors exist.
	 *
	 * @param bool $authentication_errors Whether there are authentication errors with the request.
	 *
	 * @return bool
	 */
	protected function filtered_authentication_errors( $authentication_errors = false ) {

		/**
		 * If false, there are no authentication errors. If true, execution of the
		 * GraphQL request will be prevented and an error will be thrown.
		 *
		 * @param bool $authentication_errors Whether there are authentication errors with the request
		 * @param \WPGraphQL\Request $request Instance of the Request
		 * @hookGroup authentication
		 * @since 0.0.5
		 */
		return apply_filters( 'graphql_authentication_errors', $authentication_errors, $this );
	}

	/**
	 * Performs actions and runs filters after execution completes
	 *
	 * @template T from (SerializableResult|SerializableResult[])|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
	 *
	 * @param T $response The response from execution.  Array for batch requests, single object for individual requests.
	 *
	 * @return T
	 */
	private function after_execute( $response ) {

		/**
		 * Authentication check has been moved to before_execute() as of 2.6.0.
		 * This ensures auth is validated BEFORE query execution, not after.
		 *
		 * @since 2.6.0 Auth check moved to before_execute()
		 * @see https://github.com/wp-graphql/wp-graphql/issues/3447
		 */

		/**
		 * If the params and the $response are both arrays
		 * treat this as a batch request and map over the array to apply the
		 * after_execute_actions, otherwise apply them to the current response
		 */
		if ( is_array( $this->params ) && is_array( $response ) ) {
			$filtered_response = [];
			foreach ( $response as $key => $resp ) {
				$filtered_response[] = $this->after_execute_actions( $resp, (int) $key );
			}
		} else {
			$filtered_response = $this->after_execute_actions( $response, null );
		}

		/**
		 * Reset the global post after execution
		 *
		 * This allows for a GraphQL query to be used in the middle of post content, such as in a Shortcode
		 * without disrupting the flow of the post as the global POST before and after GraphQL execution will be
		 * the same.
		 *
		 * We cannot use wp_reset_postdata here because it just resets the post from the global query which can
		 * be anything the because the resolvers themself can set it to whatever. So we just manually reset the
		 * post with setup_postdata we cached before this request.
		 */

		if ( ! empty( $this->global_wp_the_query ) ) {
			$GLOBALS['wp_the_query'] = $this->global_wp_the_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
			wp_reset_query(); // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query
		}

		if ( ! empty( $this->global_post ) ) {
			$GLOBALS['post'] = $this->global_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride
			setup_postdata( $this->global_post );
		}

		/**
		 * Run an action after GraphQL Execution
		 *
		 * @param mixed[]            $filtered_response The response of the entire operation. Could be a single operation or a batch operation
		 * @param \WPGraphQL\Request $request           Instance of the Request being executed
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		do_action( 'graphql_after_execute', $filtered_response, $this );

		/**
		 * Return the filtered response
		 */
		return $filtered_response;
	}

	/**
	 * Apply filters and do actions after GraphQL execution
	 *
	 * @param mixed|array<string,mixed>|object $response The response for your GraphQL request
	 * @param int|null                         $key      The array key of the params for batch requests
	 *
	 * @return mixed|array<string,mixed>|object
	 */
	private function after_execute_actions( $response, $key = null ) {

		/**
		 * Determine which params (batch or single request) to use when passing through to the actions
		 */
		$query     = null;
		$operation = null;
		$variables = null;
		$query_id  = null;

		if ( $this->params instanceof OperationParams ) {
			$operation = $this->params->operation;
			$query     = $this->params->query;
			$query_id  = $this->params->queryId;
			$variables = $this->params->variables;
		} elseif ( is_array( $this->params ) ) {
			$operation = $this->params[ $key ]->operation ?? '';
			$query     = $this->params[ $key ]->query ?? '';
			$query_id  = $this->params[ $key ]->queryId ?? null;
			$variables = $this->params[ $key ]->variables ?? null;
		}

		/**
		 * Run an action. This is a good place for debug tools to hook in to log things, etc.
		 *
		 * @param mixed|array<string,mixed>|object $response  The response your GraphQL request
		 * @param \WPGraphQL\WPSchema              $schema    The schema object for the root request
		 * @param ?string                          $operation The name of the operation
		 * @param ?string                          $query     The query that GraphQL executed
		 * @param ?array<string,mixed>             $variables Variables to passed to your GraphQL query
		 * @param \WPGraphQL\Request               $request   Instance of the Request
		 *
		 * @hookGroup request-lifecycle
		 * @since 0.0.6
		 */
		do_action( 'graphql_execute', $response, $this->schema, $operation, $query, $variables, $this );

		/**
		 * Add the debug log to the request
		 */
		if ( ! empty( $response ) ) {
			$logs = $this->debug_log->get_logs();
			if ( is_array( $response ) ) {
				$response['extensions']['debug'] = $logs;
			} else {
				$response->extensions['debug'] = $logs;
			}
		}

		/**
		 * Filter the $response of the GraphQL execution. This allows for the response to be filtered
		 * before it's returned, allowing granular control over the response at the latest point.
		 *
		 * POSSIBLE USAGE EXAMPLES:
		 * This could be used to ensure that certain fields never make it to the response if they match
		 * certain criteria, etc. For example, this filter could be used to check if a current user is
		 * allowed to see certain things, and if they are not, the $response could be filtered to remove
		 * the data they should not be allowed to see.
		 *
		 * Or, perhaps some systems want the response to always include some additional piece of data in
		 * every response, regardless of the request that was sent to it, this could allow for that
		 * to be hooked in and included in the $response.
		 *
		 * @param mixed|array<string,mixed>|object $response  The response for your GraphQL query
		 * @param \WPGraphQL\WPSchema              $schema    The schema object for the root request
		 * @param ?string                          $operation The name of the operation
		 * @param ?string                          $query     The query that GraphQL executed
		 * @param ?array<string,mixed>             $variables Variables to passed to your GraphQL query
		 * @param \WPGraphQL\Request               $request   Instance of the Request
		 * @param ?string                          $query_id  The query id that GraphQL executed
		 *
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		$filtered_response = apply_filters( 'graphql_request_results', $response, $this->schema, $operation, $query, $variables, $this, $query_id );

		/**
		 * Run an action after the response has been filtered, as the response is being returned.
		 * This is a good place for debug tools to hook in to log things, etc.
		 *
		 * @param mixed|array<string,mixed>|object $filtered_response The filtered response for the GraphQL request
		 * @param mixed|array<string,mixed>|object $response          The response for your GraphQL request
		 * @param \WPGraphQL\WPSchema              $schema            The schema object for the root request
		 * @param ?string                          $operation         The name of the operation
		 * @param ?string                          $query             The query that GraphQL executed
		 * @param ?array<string,mixed>             $variables         Variables to passed to your GraphQL query
		 * @param \WPGraphQL\Request               $request           Instance of the Request
		 * @param ?string                          $query_id          The query id that GraphQL executed
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		do_action( 'graphql_return_response', $filtered_response, $response, $this->schema, $operation, $query, $variables, $this, $query_id );

		/**
		 * Filter "is_graphql_request" back to false.
		 */
		\WPGraphQL::set_is_graphql_request( false );

		return $filtered_response;
	}

	/**
	 * Run action for a request.
	 *
	 * @param \GraphQL\Server\OperationParams $params OperationParams for the request.
	 */
	private function do_action( OperationParams $params ): void {

		/**
		 * Run an action for each request.
		 *
		 * @param ?string                         $query     The GraphQL query
		 * @param ?string                         $operation The name of the operation
		 * @param ?array<string,mixed>            $variables Variables to be passed to your GraphQL request
		 * @param \GraphQL\Server\OperationParams $params    The Operation Params. This includes any extra params,
		 *                                                   such as extensions or any other modifications to the request body
		 * @hookGroup request-lifecycle
		 * @since 0.0.6
		 */
		do_action( 'do_graphql_request', $params->query, $params->operation, $params->variables, $params );
	}

	/**
	 * Execute an internal request (graphql() function call).
	 *
	 * @return mixed[]
	 * @phpstan-return SerializableResult|SerializableResult[]|mixed[]
	 * @throws \Exception
	 */
	public function execute() {
		$helper = new WPHelper();

		if ( ! $this->data instanceof OperationParams ) {
			$this->params = $helper->parseRequestParams( 'POST', $this->data, [] );
		} else {
			$this->params = $this->data;
		}

		if ( is_array( $this->params ) ) {
			return array_map(
				function ( $data ) {
					$this->data = $data;
					return $this->execute();
				},
				$this->params
			);
		}

		// If $this->params isn't an array or an OperationParams instance, then something probably went wrong.
		if ( ! $this->params instanceof OperationParams ) {
			throw new \Exception( 'Invalid request params.' );
		}

		/**
		 * Initialize the GraphQL Request
		 */
		$this->before_execute();

		/**
		 * If there was an authentication error, return it as a GraphQL error response
		 * instead of executing the query.
		 *
		 * IMPORTANT: This intentionally happens BEFORE the `pre_graphql_execute_request` filter.
		 * Authentication failures should fail fast for security reasons:
		 * - Don't give plugins a chance to interfere with or "undo" auth failures
		 * - Avoid unnecessary filter processing for failed requests
		 * - Ensure consistent, predictable auth error handling
		 *
		 * Plugins that need to observe ALL requests (including auth failures) should use
		 * earlier hooks like `graphql_before_execute` or `do_graphql_request`.
		 */
		if ( null !== $this->authentication_error ) {
			$error_message = is_wp_error( $this->authentication_error )
				? $this->authentication_error->get_error_message()
				: __( 'Authentication Error', 'wp-graphql' );

			return $this->after_execute(
				[
					'errors' => [
						[
							'message' => esc_html( $error_message ),
						],
					],
				]
			);
		}

		/**
		 * Filter this to be anything other than null to short-circuit the request.
		 *
		 * @param ?SerializableResult $response The response to return early. Null continues execution.
		 * @param self               $request  The request instance being executed.
		 * @hookGroup request-lifecycle
		 * @since 1.6.6
		 */
		$response = apply_filters( 'pre_graphql_execute_request', null, $this );

		if ( null === $response ) {
			/**
			 * @var \GraphQL\Server\OperationParams $params
			 */
			$params = $this->params;

			/**
			 * Allow the query string to be determined by a filter. Ex, when params->queryId is present, query can be retrieved.
			 *
			 * @param string                          $query  The query string to execute.
			 * @param \GraphQL\Server\OperationParams $params Operation params for the request.
			 * @hookGroup request-lifecycle
			 * @since 0.0.5
			 */
			$query = apply_filters(
				'graphql_execute_query_params',
				$params->query ?? '',
				$params
			);

			$result = GraphQL::executeQuery(
				$this->schema,
				$query,
				$this->get_root_value(),
				$this->app_context,
				$params->variables ?? null,
				$params->operation ?? null,
				$this->field_resolver,
				$this->validation_rules
			);

			/**
			 * Return the result of the request
			 */
			$response = $result->toArray( $this->get_debug_flag() );
		}

		/**
		 * Ensure the response is returned as a proper, populated array. Otherwise add an error.
		 */
		if ( empty( $response ) || ! is_array( $response ) ) {
			$response = [
				'errors' => __( 'The GraphQL request returned an invalid response', 'wp-graphql' ),
			];
		}

		/**
		 * If the request is a batch request it will come back as an array
		 */
		return $this->after_execute( $response );
	}

	/**
	 * Execute an HTTP request.
	 *
	 * @return SerializableResult|(\GraphQL\Executor\ExecutionResult|array<int,\GraphQL\Executor\ExecutionResult>)
	 * @throws \Exception
	 */
	public function execute_http() {
		if ( ! $this->is_valid_http_content_type() ) {
			return $this->get_invalid_content_type_response();
		}

		/**
		 * Parse HTTP request.
		 */
		$helper       = new WPHelper();
		$this->params = $helper->parseHttpRequest();

		/**
		 * Initialize the GraphQL Request
		 */
		$this->before_execute();

		/**
		 * If there was an authentication error, return it as a GraphQL error response
		 * instead of executing the query. This ensures consistent error handling.
		 */
		if ( null !== $this->authentication_error ) {
			$error_message = is_wp_error( $this->authentication_error )
				? $this->authentication_error->get_error_message()
				: __( 'Authentication Error', 'wp-graphql' );

			return $this->after_execute(
				[
					'errors' => [
						[
							'message' => esc_html( $error_message ),
						],
					],
				]
			);
		}

		/**
		 * Get the response.
		 */
		/**
		 * Filter this to be anything other than null to short-circuit HTTP execution.
		 *
		 * @param mixed|null $response The response to return early. Null continues execution.
		 * @param self       $request  The request instance being executed.
		 * @hookGroup request-lifecycle
		 * @since 1.6.6
		 */
		$response = apply_filters( 'pre_graphql_execute_request', null, $this );

		/**
		 * If no cached response, execute the query
		 */
		if ( null === $response ) {
			$server   = $this->get_server();
			$response = $server->executeRequest( $this->params );
		}

		return $this->after_execute( $response );
	}

	/**
	 * Validates the content type for HTTP POST requests
	 */
	private function is_valid_http_content_type(): bool {
		if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
			return true;
		}

		$content_type = $this->get_content_type();
		if ( empty( $content_type ) ) {
			return false;
		}

		$is_valid = 0 === stripos( $content_type, 'application/json' );

		/**
		 * Allow graphql to validate custom content types for HTTP POST requests
		 *
		 * @param bool $is_valid Whether the content type is valid
		 * @param string $content_type The content type header value that was received
		 *
		 * @hookGroup request-lifecycle
		 * @since 2.1.0
		 */
		return (bool) apply_filters( 'graphql_is_valid_http_content_type', $is_valid, $content_type );
	}

	/**
	 * Gets the content type from the request headers
	 */
	private function get_content_type(): string {
		if ( isset( $_SERVER['CONTENT_TYPE'] ) ) {
			return sanitize_text_field( $_SERVER['CONTENT_TYPE'] );
		}

		if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
			return sanitize_text_field( $_SERVER['HTTP_CONTENT_TYPE'] );
		}

		return '';
	}

	/**
	 * Returns the error response for invalid content type
	 *
	 * @return array{errors: array{array{message: string}}}
	 */
	private function get_invalid_content_type_response(): array {
		$content_type = $this->get_content_type();

		/**
		 * Filter the status code to return when the content type is invalid
		 *
		 * @param int    $status_code The status code to return. Default 415.
		 * @param string $content_type The content type header value that was received.
		 * @hookGroup request-lifecycle
		 * @since 2.1.0
		 */
		$filtered_status_code = apply_filters( 'graphql_invalid_content_type_status_code', 415, $content_type );

		// Set the status code to the filtered value if it's a valid status code.
		if ( is_numeric( $filtered_status_code ) ) {
			$filtered_status_code = (int) $filtered_status_code;

			if ( $filtered_status_code > 100 && $filtered_status_code < 599 ) {
				Router::$http_status_code = $filtered_status_code;
			}
		}

		return [
			'errors' => [
				[
					// translators: %s is the content type header value that was received
					'message' => sprintf( esc_html__( 'HTTP POST requests must have Content-Type: application/json header. Received: %s', 'wp-graphql' ), $content_type ),
				],
			],
		];
	}

	/**
	 * Get the operation params for the request.
	 *
	 * @return \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[]
	 */
	public function get_params() {
		return $this->params;
	}

	/**
	 * Returns the debug flag value
	 *
	 * @return int
	 */
	public function get_debug_flag() {
		$flag = DebugFlag::INCLUDE_DEBUG_MESSAGE;
		if ( 0 !== get_current_user_id() ) {
			// Flag 2 shows the trace data, which should require user to be logged in to see by default
			$flag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE;
		}

		return true === \WPGraphQL::debug() ? $flag : DebugFlag::NONE;
	}

	/**
	 * Determines if batch queries are enabled for the server.
	 *
	 * Default is to have batch queries enabled.
	 */
	private function is_batch_queries_enabled(): bool {
		$batch_queries_enabled = true;

		$batch_queries_setting = get_graphql_setting( 'batch_queries_enabled', 'on' );
		if ( 'off' === $batch_queries_setting ) {
			$batch_queries_enabled = false;
		}

		/**
		 * Filter whether batch queries are supported or not
		 *
		 * @param bool                                                              $batch_queries_enabled Whether Batch Queries should be enabled
		 * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params Request operation params
		 * @hookGroup request-lifecycle
		 * @since 0.0.5
		 */
		return (bool) apply_filters( 'graphql_is_batch_queries_enabled', $batch_queries_enabled, $this->params );
	}

	/**
	 * Create the GraphQL server that will process the request.
	 */
	private function get_server(): StandardServer {
		$debug_flag = $this->get_debug_flag();

		$config = new ServerConfig();
		$config
			->setDebugFlag( $debug_flag )
			->setSchema( $this->schema )
			->setContext( $this->app_context )
			->setValidationRules( $this->validation_rules )
			->setQueryBatching( $this->is_batch_queries_enabled() );

		if ( ! empty( $this->get_root_value() ) ) {
			$config->setRootValue( $this->get_root_value() );
		}

		if ( ! empty( $this->field_resolver ) ) {
			$config->setFieldResolver( $this->field_resolver );
		}

		/**
		 * Run an action when the server config is created. The config can be acted
		 * upon directly to override default values or implement new features, e.g.,
		 * $config->setValidationRules().
		 *
		 * @param \GraphQL\Server\ServerConfig                                      $config Server config
		 * @param \GraphQL\Server\OperationParams|\GraphQL\Server\OperationParams[] $params Request operation params
		 *
		 * @hookGroup request-lifecycle
		 * @since 0.2.0
		 */
		do_action( 'graphql_server_config', $config, $this->params );

		return new StandardServer( $config );
	}
}
