<?php

namespace WPGraphQL\Mutation;

use GraphQL\Error\UserError;
use GraphQL\Type\Definition\ResolveInfo;
use WPGraphQL\AppContext;
use WPGraphQL\Data\MediaItemMutation;
use WPGraphQL\Utils\Utils;

class MediaItemCreate {
	/**
	 * Registers the MediaItemCreate mutation.
	 *
	 * @return void
	 * @throws \Exception
	 */
	public static function register_mutation() {
		register_graphql_mutation(
			'createMediaItem',
			[
				'inputFields'         => self::get_input_fields(),
				'outputFields'        => self::get_output_fields(),
				'mutateAndGetPayload' => self::mutate_and_get_payload(),
			]
		);
	}

	/**
	 * Defines the mutation input field configuration.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public static function get_input_fields() {
		return [
			'altText'       => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'Alternative text to display when mediaItem is not displayed', 'wp-graphql' );
				},
			],
			'authorId'      => [
				'type'        => 'ID',
				'description' => static function () {
					return __( 'The userId to assign as the author of the mediaItem', 'wp-graphql' );
				},
			],
			'caption'       => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The caption for the mediaItem', 'wp-graphql' );
				},
			],
			'commentStatus' => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The comment status for the mediaItem', 'wp-graphql' );
				},
			],
			'date'          => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The date of the mediaItem', 'wp-graphql' );
				},
			],
			'dateGmt'       => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The date (in GMT zone) of the mediaItem', 'wp-graphql' );
				},
			],
			'description'   => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'Description of the mediaItem', 'wp-graphql' );
				},
			],
			'filePath'      => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The file name of the mediaItem', 'wp-graphql' );
				},
			],
			'fileType'      => [
				'type'        => 'MimeTypeEnum',
				'description' => static function () {
					return __( 'The file type of the mediaItem', 'wp-graphql' );
				},
			],
			'slug'          => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The slug of the mediaItem', 'wp-graphql' );
				},
			],
			'status'        => [
				'type'        => 'MediaItemStatusEnum',
				'description' => static function () {
					return __( 'The status of the mediaItem', 'wp-graphql' );
				},
			],
			'title'         => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The title of the mediaItem', 'wp-graphql' );
				},
			],
			'pingStatus'    => [
				'type'        => 'String',
				'description' => static function () {
					return __( 'The ping status for the mediaItem', 'wp-graphql' );
				},
			],
			'parentId'      => [
				'type'        => 'ID',
				'description' => static function () {
					return __( 'The ID of the parent object', 'wp-graphql' );
				},
			],
		];
	}

	/**
	 * Defines the mutation output field configuration.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public static function get_output_fields() {
		return [
			'mediaItem' => [
				'type'        => 'MediaItem',
				'description' => static function () {
					return __( 'The MediaItem object mutation type.', 'wp-graphql' );
				},
				'resolve'     => static function ( $payload, $args, AppContext $context ) {
					if ( empty( $payload['postObjectId'] ) || ! absint( $payload['postObjectId'] ) ) {
						return null;
					}

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

	/**
	 * Defines the mutation data modification closure.
	 *
	 * @return callable(array<string,mixed>$input,\WPGraphQL\AppContext $context,\GraphQL\Type\Definition\ResolveInfo $info):array<string,mixed>
	 */
	public static function mutate_and_get_payload() {
		return static function ( $input, AppContext $context, ResolveInfo $info ) {
			/**
			 * Stop now if a user isn't allowed to upload a mediaItem
			 */
			if ( ! current_user_can( 'upload_files' ) ) {
				throw new UserError( esc_html__( 'Sorry, you are not allowed to upload mediaItems', 'wp-graphql' ) );
			}

			$post_type_object = get_post_type_object( 'attachment' );
			if ( empty( $post_type_object ) ) {
				throw new UserError( esc_html__( 'The Media Item could not be created', 'wp-graphql' ) );
			}

			/**
			 * If the mediaItem being created is being assigned to another user that's not the current user, make sure
			 * the current user has permission to edit others mediaItems
			 */
			if ( ! empty( $input['authorId'] ) ) {
				// Ensure authorId is a valid databaseId.
				$input['authorId'] = Utils::get_database_id_from_id( $input['authorId'] );

				// Bail if can't edit other users' attachments.
				if ( get_current_user_id() !== $input['authorId'] && ( ! isset( $post_type_object->cap->edit_others_posts ) || ! current_user_can( $post_type_object->cap->edit_others_posts ) ) ) {
					throw new UserError( esc_html__( 'Sorry, you are not allowed to create mediaItems as this user', 'wp-graphql' ) );
				}
			}

			// REST parity: reject `revision` and `attachment` as parent types.
			// WP_REST_Attachments_Controller::create_item() rejects these at the
			// top of the handler. Mirroring that here so the failure happens
			// before any file is downloaded.
			if ( ! empty( $input['parentId'] ) ) {
				$parent_database_id = Utils::get_database_id_from_id( $input['parentId'] );
				if ( $parent_database_id ) {
					$parent_post_type = get_post_type( (int) $parent_database_id );
					if ( in_array( $parent_post_type, [ 'revision', 'attachment' ], true ) ) {
						throw new UserError( esc_html__( 'Invalid parent type.', 'wp-graphql' ) );
					}
				}
			}

			/**
			 * Set the file name, whether it's a local file or from a URL.
			 * Then set the url for the uploaded file
			 */
			$file_name           = basename( $input['filePath'] );
			$uploaded_file_url   = (string) $input['filePath'];
			$sanitized_file_path = sanitize_file_name( $input['filePath'] );

			// Check that the filetype is allowed
			$check_file = wp_check_filetype( $sanitized_file_path );

			// wp_http_validate_url() rejects 127/10/0/172.16-31/192.168 but not
			// RFC 3927 link-local (169.254/16), which exposes cloud instance
			// metadata (169.254.169.254). Reject those explicitly.
			$host          = wp_parse_url( $uploaded_file_url, PHP_URL_HOST );
			$is_link_local = is_string( $host ) && 0 === strpos( $host, '169.254.' );

			// if the file doesn't pass the check, throw an error
			if ( ! $check_file['ext'] || ! $check_file['type'] || ! wp_http_validate_url( $uploaded_file_url ) || $is_link_local ) {
				// translators: %s is the file path.
				throw new UserError( esc_html( sprintf( __( 'Invalid filePath "%s"', 'wp-graphql' ), $input['filePath'] ) ) );
			}

			$protocol = wp_parse_url( $input['filePath'], PHP_URL_SCHEME );

			// prevent the filePath from being submitted with a non-allowed protocols
			$allowed_protocols = [ 'https', 'http' ];

			/**
			 * Filter the allowed protocols for the mutation
			 *
			 * @param string[]                             $allowed_protocols The allowed protocols for filePaths to be submitted
			 * @param mixed                                $protocol          The current protocol of the filePath
			 * @param array<string,mixed>                  $input             The input of the current mutation
			 * @param \WPGraphQL\AppContext                $context           The context of the current request
			 * @param \GraphQL\Type\Definition\ResolveInfo $info              The ResolveInfo of the current field
			 *
			 * @hookGroup models
			 * @since 0.0.5
			 */
			$allowed_protocols = apply_filters( 'graphql_media_item_create_allowed_protocols', $allowed_protocols, $protocol, $input, $context, $info );

			if ( ! in_array( $protocol, $allowed_protocols, true ) ) {
				throw new UserError(
					esc_html(
						sprintf(
							// translators: %1$s is the protocol, %2$s is the list of allowed protocols.
							__( 'Invalid protocol. "%1$s". Only "%2$s" allowed.', 'wp-graphql' ),
							$protocol,
							implode( '", "', $allowed_protocols )
						)
					)
				);
			}

			/**
			 * Require the file.php file from wp-admin. This file includes the
			 * download_url and wp_handle_sideload methods.
			 */
			require_once ABSPATH . 'wp-admin/includes/file.php';

			/**
			 * Ensure we have a valid URL before attempting download
			 */
			if ( empty( $uploaded_file_url ) ) {
				throw new UserError( esc_html__( 'Sorry, the file could not be uploaded', 'wp-graphql' ) );
			}

			/**
			 * URL data for the mediaItem, timeout value is the default, see:
			 * https://developer.wordpress.org/reference/functions/download_url/
			 */
			$timeout_seconds = 300;

			$temp_file = download_url( $uploaded_file_url, $timeout_seconds );

			/**
			 * Handle the error from download_url if it occurs
			 */
			if ( is_wp_error( $temp_file ) ) {
				throw new UserError( esc_html__( 'Sorry, the URL for this file is invalid, it must be a valid URL', 'wp-graphql' ) );
			}

			// REST parity: enforce multisite file-size and quota limits.
			// Mirrors WP_REST_Attachments_Controller::check_upload_size().
			$size_error = self::check_multisite_upload_size( $temp_file );
			if ( null !== $size_error ) {
				wp_delete_file( $temp_file );
				throw new UserError( esc_html( $size_error ) );
			}

			/**
			 * Build the file data for side loading
			 */
			$file_data = [
				'name'     => $file_name,
				'type'     => ! empty( $input['fileType'] ) ? $input['fileType'] : wp_check_filetype( $temp_file ),
				'tmp_name' => $temp_file,
				'error'    => 0,
				'size'     => (int) filesize( $temp_file ),
			];

			/**
			 * Tells WordPress to not look for the POST form fields that would normally be present as
			 * we downloaded the file from a remote server, so there will be no form fields
			 * The default is true
			 */
			$overrides = [
				'test_form' => false,
			];

			/**
			 * Insert the mediaItem and retrieve it's data
			 */
			$file = wp_handle_sideload( $file_data, $overrides );

			/**
			 * Handle the error from wp_handle_sideload if it occurs
			 */
			if ( ! empty( $file['error'] ) ) {
				throw new UserError( esc_html__( 'Sorry, the URL for this file is invalid, it must be a path to the mediaItem file', 'wp-graphql' ) );
			}

			// REST parity: reject image types the server cannot generate
			// sub-sizes for. Mirrors the wp_image_editor_supports() check in
			// WP_REST_Attachments_Controller::create_item_permissions_check()
			// (added in WP 6.8). The wp_prevent_unsupported_mime_type_uploads
			// filter mirrors core, so site owners can opt out the same way.
			$detected_type = isset( $file['type'] ) && is_string( $file['type'] ) ? $file['type'] : '';
			if (
				apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, $detected_type )
				&& 0 === strpos( $detected_type, 'image/' )
				&& 'image/svg+xml' !== $detected_type
				&& ! wp_image_editor_supports( [ 'mime_type' => $detected_type ] )
			) {
				if ( ! empty( $file['file'] ) ) {
					wp_delete_file( $file['file'] );
				}
				throw new UserError( esc_html__( 'The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading.', 'wp-graphql' ) );
			}

			/**
			 * Insert the mediaItem object and get the ID
			 */
			$media_item_args = MediaItemMutation::prepare_media_item( $input, $post_type_object, 'createMediaItem', $file );

			/**
			 * Get the post parent and if it's not set, set it to 0
			 */
			$attachment_parent_id = ! empty( $media_item_args['post_parent'] ) ? $media_item_args['post_parent'] : 0;

			/**
			 * Stop now if a user isn't allowed to edit the parent post
			 */
			$parent = get_post( $attachment_parent_id );

			if ( null !== $parent ) {
				$post_parent_type = get_post_type_object( $parent->post_type );

				if ( empty( $post_parent_type ) ) {
					throw new UserError( esc_html__( 'The parent of the Media Item is of an invalid type', 'wp-graphql' ) );
				}

				if ( 'attachment' !== $post_parent_type->name && ( ! isset( $post_parent_type->cap->edit_post ) || ! current_user_can( $post_parent_type->cap->edit_post, $attachment_parent_id ) ) ) {
					throw new UserError( esc_html__( 'Sorry, you are not allowed to upload mediaItems assigned to this parent node', 'wp-graphql' ) );
				}
			}

			/**
			 * Insert the mediaItem
			 *
			 * Required Argument defaults are set in the main MediaItemMutation.php if they aren't set
			 * by the user during input, they are:
			 * post_title (pulled from file if not entered)
			 * post_content (empty string if not entered)
			 * post_status (inherit if not entered)
			 * post_mime_type (pulled from the file if not entered in the mutation)
			 */
			$attachment_id = wp_insert_attachment( $media_item_args, $file['file'], $attachment_parent_id, true );

			if ( is_wp_error( $attachment_id ) ) {
				$error_message = $attachment_id->get_error_message();
				if ( ! empty( $error_message ) ) {
					throw new UserError( esc_html( $error_message ) );
				}

				throw new UserError( esc_html__( 'The media item failed to create but no error was provided', 'wp-graphql' ) );
			}

			/**
			 * Check if the wp_generate_attachment_metadata method exists and include it if not.
			 */
			require_once ABSPATH . 'wp-admin/includes/image.php';

			/**
			 * Generate and update the mediaItem's metadata.
			 * If we make it this far the file and attachment
			 * have been validated and we will not receive any errors
			 */
			$attachment_data = wp_generate_attachment_metadata( $attachment_id, $file['file'] );
			wp_update_attachment_metadata( $attachment_id, $attachment_data );

			/**
			 * Update alt text postmeta for mediaItem
			 */
			MediaItemMutation::update_additional_media_item_data( $attachment_id, $input, $post_type_object, 'createMediaItem', $context, $info );

			return [
				'postObjectId' => $attachment_id,
			];
		};
	}

	/**
	 * Mirrors WP_REST_Attachments_Controller::check_upload_size() for the
	 * multisite quota and per-file size limit checks. Returns null when the
	 * file is within all limits, or a translated error message string when
	 * one of the limits is exceeded.
	 *
	 * No-ops on single-site installs, where these site-option-driven limits
	 * do not apply.
	 *
	 * @param string $file_path Path to the downloaded temp file.
	 * @return string|null Error message if size exceeds limits, null otherwise.
	 */
	private static function check_multisite_upload_size( $file_path ) {
		if ( ! is_multisite() || get_site_option( 'upload_space_check_disabled' ) ) {
			return null;
		}

		$file_size  = (int) filesize( $file_path );
		$space_left = (int) get_upload_space_available();

		if ( $space_left < $file_size ) {
			return sprintf(
				// translators: %s is the required disk space in kilobytes.
				__( 'Not enough space to upload. %s KB needed.', 'wp-graphql' ),
				number_format( ( $file_size - $space_left ) / KB_IN_BYTES )
			);
		}

		$max_kb = (int) get_site_option( 'fileupload_maxk', 1500 );
		if ( $file_size > KB_IN_BYTES * $max_kb ) {
			return sprintf(
				// translators: %s is the maximum allowed file size in kilobytes.
				__( 'This file is too big. Files must be less than %s KB in size.', 'wp-graphql' ),
				$max_kb
			);
		}

		require_once ABSPATH . 'wp-admin/includes/ms.php';
		if ( upload_is_user_over_quota( false ) ) {
			return __( 'You have used your space quota. Please delete files before uploading.', 'wp-graphql' );
		}

		return null;
	}
}
