<?php
/**
 * Class for handling deprecated functionality.
 *
 * Entirely deprecated classes can be relocated to the `deprecated/` directory, but things still need to be hooked into WordPress.
 *
 * @package  WPGraphQL
 */

namespace WPGraphQL;

use GraphQL\Error\UserError;
use GraphQLRelay\Relay;
use WPGraphQL\Data\Connection\PostObjectConnectionResolver;
use WPGraphQL\Model\Post;
use WPGraphQL\Type\Union\MenuItemObjectUnion;
use WPGraphQL\Type\Union\PostObjectUnion;
use WPGraphQL\Type\Union\TermObjectUnion;
use WPGraphQL\Type\WPObjectType;

/**
 * Class - Deprecated
 */
final class Deprecated {
	/**
	 * The class constructor.
	 */
	public function __construct() {}

	/**
	 * Register the deprecated functionality.
	 */
	public function register(): void {
		$this->filters();
		// We want to defer the action until after the schema is registered.
		add_action( 'graphql_register_types', [ $this, 'register_deprecated_types' ] );
	}

	/**
	 * Handles deprecated filters.
	 */
	private function filters(): void {
		/**
		 * The `graphql_object_type_interfaces` filter
		 *
		 * @deprecated 1.4.1
		 * @todo Remove in 3.0.0
		 */
		add_filter(
			'graphql_type_interfaces',
			static function ( $interfaces, $config, $type ) {
				if ( ! $type instanceof WPObjectType || ! has_filter( 'graphql_object_type_interfaces' ) ) {
					return $interfaces;
				}

				/**
				 * @deprecated
				 *
				 * @param string[]                                                     $interfaces List of interfaces applied to the Object Type
				 * @param array<string,mixed>                                          $config     The config for the Object Type
				 * @param \WPGraphQL\Type\WPInterfaceType|\WPGraphQL\Type\WPObjectType $type       The Type instance
				 */
				return apply_filters_deprecated( 'graphql_object_type_interfaces', [ $interfaces, $config, $type ], '1.4.1', 'graphql_type_interfaces', __( 'This will be removed in the next major release of WPGraphQL.', 'wp-graphql' ) );
			},
			10,
			3
		);

		/**
		 * The `graphql_return_modeled_data` filter.
		 *
		 * @deprecated 1.7.0
		 * @todo Remove in 3.0.0
		 */
		add_filter(
			'graphql_model_prepare_fields',
			static function ( $fields, $model_name, $data, $visibility, $owner, $current_user ) {
				if ( ! has_filter( 'graphql_return_modeled_data' ) ) {
					return $fields;
				}

				/**
				 * @param array<string,mixed>    $fields       The array of fields for the model
				 * @param string                 $model_name   Name of the model the filter is currently being executed in
				 * @param string                 $visibility   The visibility setting for this piece of data
				 * @param ?int                   $owner        The user ID for the owner of this piece of data
				 * @param \WP_User               $current_user The current user for the session
				 *
				 * @deprecated 1.7.0 use "graphql_model_prepare_fields" filter instead, which passes additional context to the filter
				 */
				return apply_filters_deprecated(
					'graphql_return_modeled_data',
					[ $fields, $model_name, $visibility, $owner, $current_user ],
					'1.7.0',
					'graphql_model_prepare_fields',
					__( 'This will be removed in the next major release of WPGraphQL.', 'wp-graphql' )
				);
			},
			10,
			6
		);
	}

	/**
	 * Registers deprecated graphql types.
	 */
	public function register_deprecated_types(): void {
		MenuItemObjectUnion::register_type(); /* @phpstan-ignore staticMethod.deprecatedClass */
		PostObjectUnion::register_type(); /* @phpstan-ignore staticMethod.deprecatedClass */
		TermObjectUnion::register_type(); /* @phpstan-ignore staticMethod.deprecatedClass */

		$this->graphql_post_types();
		$this->menu_item_connected_object();
		$this->send_password_reset_email_user();
	}

	/**
	 * The `MenuItem` connectedObject field.
	 *
	 * @todo remove in 3.0.0
	 */
	private function menu_item_connected_object(): void {
		register_graphql_field(
			'MenuItem',
			'connectedObject',
			[
				'type'              => 'MenuItemObjectUnion',
				'deprecationReason' => static function () {
					return __( 'Deprecated in favor of the connectedNode field', 'wp-graphql' );
				},
				'description'       => static function () {
					return __( 'The object connected to this menu item.', 'wp-graphql' );
				},
				'resolve'           => static function ( $menu_item, array $args, AppContext $context, $info ) {
					$object_id   = intval( get_post_meta( $menu_item->menuItemId, '_menu_item_object_id', true ) );
					$object_type = get_post_meta( $menu_item->menuItemId, '_menu_item_type', true );

					switch ( $object_type ) {
						// Post object
						case 'post_type':
							$resolved_object = $context->get_loader( 'post' )->load_deferred( $object_id );
							break;

						// Taxonomy term
						case 'taxonomy':
							$resolved_object = $context->get_loader( 'term' )->load_deferred( $object_id );
							break;
						default:
							$resolved_object = null;
							break;
					}

					/**
					 * @todo Remove in 3.0.0.
					 *
					 * @param \WP_Post|\WP_Term                    $resolved_object Post or term connected to MenuItem
					 * @param array<string,mixed>                  $args            Array of arguments input in the field as part of the GraphQL query
					 * @param \WPGraphQL\AppContext                $context         Object containing app context that gets passed down the resolve tree
					 * @param \GraphQL\Type\Definition\ResolveInfo $info            Info about fields passed down the resolve tree
					 * @param int                                  $object_id       Post or term ID of connected object
					 * @param string                               $object_type     Type of connected object ("post_type" or "taxonomy")
					 *
					 * @since 0.0.30
					 */
					return apply_filters_deprecated(
						'graphql_resolve_menu_item',
						[
							$resolved_object,
							$args,
							$context,
							$info,
							$object_id,
							$object_type,
						],
						'1.22.0',
						'graphql_pre_resolve_menu_item_connected_node',
						__( 'This will be removed in the next version of WPGraphQL. Use the `graphql_pre_resolve_menu_item_connected_node` filter on `connectedNode` instead.', 'wp-graphql' )
					);
				},
			],
		);
	}

	/**
	 * Registers deprecated Post Type data to the schema
	 */
	private function graphql_post_types(): void {
		$allowed_post_types = \WPGraphQL::get_allowed_post_types( 'objects', [ 'graphql_register_root_field' => true ] );

		foreach ( $allowed_post_types as $post_type_object ) {
			$this->post_type_by_field( $post_type_object );
			$this->register_deprecated_post_type_parents( $post_type_object );
			$this->register_deprecated_post_type_previews( $post_type_object );
		}
	}

	/**
	 * Register deprecated {PostType}By fields
	 *
	 * @todo remove in 3.0.0
	 *
	 * @param \WP_Post_Type $post_type_object The post type object to register the field for.
	 */
	private function post_type_by_field( $post_type_object ): void {
		$post_by_args = [
			'id'                                          => [
				'type'        => 'ID',
				'description' => static function () use ( $post_type_object ) {
					return sprintf(
						// translators: %s is the post type's GraphQL name.
						__( 'Get the %s object by its global ID', 'wp-graphql' ),
						$post_type_object->graphql_single_name
					);
				},
			],
			$post_type_object->graphql_single_name . 'Id' => [
				'type'        => 'Int',
				'description' => static function () use ( $post_type_object ) {
					return sprintf(
						// translators: %s is the post type's GraphQL name.
						__( 'Get the %s by its database ID', 'wp-graphql' ),
						$post_type_object->graphql_single_name
					);
				},
			],
			'uri'                                         => [
				'type'        => 'String',
				'description' => static function () use ( $post_type_object ) {
					return sprintf(
						// translators: %s is the post type's GraphQL name.
						__( 'Get the %s by its uri', 'wp-graphql' ),
						$post_type_object->graphql_single_name
					);
				},
			],
		];

		if ( false === $post_type_object->hierarchical ) {
			$post_by_args['slug'] = [
				'type'        => 'String',
				'description' => static function () use ( $post_type_object ) {
					return sprintf(
						// translators: %s is the post type's GraphQL name.
						__( 'Get the %s by its slug (only available for non-hierarchical types)', 'wp-graphql' ),
						$post_type_object->graphql_single_name
					);
				},
			];
		}

		/**
		 * @deprecated Deprecated in favor of single node entry points
		 */
		register_graphql_field(
			'RootQuery',
			$post_type_object->graphql_single_name . 'By',
			[
				'type'              => $post_type_object->graphql_single_name,
				'deprecationReason' => static function () {
					return __( 'Deprecated in favor of using the single entry point for this type with ID and IDType fields. For example, instead of postBy( id: "" ), use post(id: "" idType: "")', 'wp-graphql' );
				},
				'description'       => static function () use ( $post_type_object ) {
					return sprintf(
						// translators: %s is the post type's GraphQL name.
						__( 'A %s object', 'wp-graphql' ),
						$post_type_object->graphql_single_name
					);
				},
				'args'              => $post_by_args,
				'resolve'           => static function ( $source, array $args, $context ) use ( $post_type_object ) {
					$post_id = 0;

					if ( ! empty( $args['id'] ) ) {
						$id_components = Relay::fromGlobalId( $args['id'] );
						if ( empty( $id_components['id'] ) || empty( $id_components['type'] ) ) {
							throw new UserError( esc_html__( 'The "id" is invalid', 'wp-graphql' ) );
						}
						$post_id = absint( $id_components['id'] );
					} elseif ( ! empty( $args[ lcfirst( $post_type_object->graphql_single_name . 'Id' ) ] ) ) {
						$id      = $args[ lcfirst( $post_type_object->graphql_single_name . 'Id' ) ];
						$post_id = absint( $id );
					} elseif ( ! empty( $args['uri'] ) ) {
						return $context->node_resolver->resolve_uri(
							$args['uri'],
							[
								'post_type' => $post_type_object->name,
								'archive'   => false,
								'nodeType'  => 'ContentNode',
							]
						);
					} elseif ( ! empty( $args['slug'] ) ) {
						$slug = esc_html( $args['slug'] );

						return $context->node_resolver->resolve_uri(
							$slug,
							[
								'name'      => $slug,
								'post_type' => $post_type_object->name,
								'nodeType'  => 'ContentNode',
							]
						);
					}

					return $context->get_loader( 'post' )->load_deferred( $post_id )->then(
						static function ( $post ) use ( $post_type_object ) {

							// if the post type object isn't an instance of WP_Post_Type, return
							if ( ! $post_type_object instanceof \WP_Post_Type ) {
								return null;
							}

							// if the post isn't an instance of a Post model, return
							if ( ! $post instanceof Post ) {
								return null;
							}

							if ( ! isset( $post->post_type ) || ! in_array(
								$post->post_type,
								[
									'revision',
									$post_type_object->name,
								],
								true
							) ) {
								return null;
							}

							return $post;
						}
					);
				},
			]
		);
	}

	/**
	 * Register deprecated Post Type connections.
	 *
	 * @todo remove in 3.0.0
	 *
	 * @param \WP_Post_Type $post_type_object The post type object to register the connection for.
	 */
	private function register_deprecated_post_type_parents( \WP_Post_Type $post_type_object ): void {
		if ( $post_type_object->hierarchical || in_array( $post_type_object->name, [ 'attachment', 'revision' ], true ) ) {
			return;
		}

		// Ancestors
		register_graphql_connection(
			[
				'fromType'          => $post_type_object->graphql_single_name,
				'toType'            => $post_type_object->graphql_single_name,
				'fromFieldName'     => 'ancestors',
				'description'       => static function () {
						return __( 'The ancestors of the content node.', 'wp-graphql' );
				},
				'deprecationReason' => static function () {
					return __( 'This content type is not hierarchical and typically will not have ancestors', 'wp-graphql' );
				},
				'resolve'           => static function () {
					return null;
				},
			]
		);

		// Parent
		register_graphql_connection(
			[
				'fromType'          => $post_type_object->graphql_single_name,
				'toType'            => $post_type_object->graphql_single_name,
				'fromFieldName'     => 'parent',
				'oneToOne'          => true,
				'description'       => static function () {
					return __( 'The parent of the content node.', 'wp-graphql' );
				},
				'deprecationReason' => static function () {
					return __( 'This content type is not hierarchical and typically will not have a parent', 'wp-graphql' );
				},
				'resolve'           => static function () {
					return null;
				},
			]
		);
	}

	/**
	 * Register deprecated Post Type previews.
	 *
	 * @todo remove in 3.0.0
	 * @param \WP_Post_Type $post_type_object The post type object to register the preview for.
	 */
	private function register_deprecated_post_type_previews( \WP_Post_Type $post_type_object ): void {
		if ( in_array( $post_type_object->name, [ 'attachment', 'revision' ], true ) ) {
			return;
		}

		register_graphql_connection(
			[
				'fromType'           => $post_type_object->graphql_single_name,
				'toType'             => $post_type_object->graphql_single_name,
				'fromFieldName'      => 'preview',
				'connectionTypeName' => ucfirst( $post_type_object->graphql_single_name ) . 'ToPreviewConnection',
				'oneToOne'           => true,
				'deprecationReason'  => ( true === $post_type_object->publicly_queryable || true === $post_type_object->public ) ? null
					: sprintf(
						// translators: %s is the post type's GraphQL name.
						__( 'The "%s" Type is not publicly queryable and does not support previews. This field will be removed in the future.', 'wp-graphql' ),
						\WPGraphQL\Utils\Utils::format_type_name( $post_type_object->graphql_single_name )
					),
				'resolve'            => static function ( Post $post, $args, $context, $info ) {
					if ( $post->isRevision ) {
						return null;
					}

					if ( empty( $post->previewRevisionDatabaseId ) ) {
						return null;
					}

					$resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'revision' );
					$resolver->set_query_arg( 'p', $post->previewRevisionDatabaseId );

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

	/**
	 * SendPasswordResetEmail.user output field\
	 *
	 * @todo remove in 3.0.0
	 */
	public function send_password_reset_email_user(): void {
		register_graphql_field(
			'SendPasswordResetEmailPayload',
			'user',
			[
				'type'              => 'User',
				'description'       => static function () {
					return __( 'The user that the password reset email was sent to', 'wp-graphql' );
				},
				'deprecationReason' => static function () {
					return __( 'This field will be removed in a future version of WPGraphQL', 'wp-graphql' );
				},
				'resolve'           => static function ( $payload, $args, AppContext $context ) {
					// The `sendPasswordResetEmail` mutation deliberately obfuscates whether the supplied
					// username/email matches a real account (it always returns `success: true`) to prevent
					// user enumeration. The internal `$payload['id']` is only populated on a real match, so
					// resolving it for everyone would turn this field into an enumeration oracle: an
					// unauthenticated caller could test arbitrary emails/logins and learn which ones exist
					// (and link them to a user identity). Only expose the user to requesters who already have
					// the capability to list users; everyone else gets null regardless of whether the account
					// exists, preserving the mutation's anti-enumeration guarantee.
					if ( empty( $payload['id'] ) || ! current_user_can( 'list_users' ) ) {
						return null;
					}

					return $context->get_loader( 'user' )->load_deferred( $payload['id'] );
				},
			],
		);
	}
}
