<?php

use Imagify\Traits\InstanceGetterTrait;

/**
 * Class that handles admin ajax/post callbacks.
 *
 * @since  1.6.11
 */
class Imagify_Admin_Ajax_Post extends Imagify_Admin_Ajax_Post_Deprecated {
	use InstanceGetterTrait;

	/**
	 * Class version.
	 *
	 * @var string
	 */
	const VERSION = '1.1';

	/**
	 * Actions to be triggered on admin ajax and admin post.
	 *
	 * @var array
	 */
	protected $ajax_post_actions = [
		// WP optimization.
		'imagify_manual_optimize',
		'imagify_manual_reoptimize',
		'imagify_optimize_missing_sizes',
		'imagify_generate_nextgen_versions',
		'imagify_delete_nextgen_versions',
		'imagify_restore',
		// Custom folders optimization.
		'imagify_optimize_file',
		'imagify_reoptimize_file',
		'imagify_restore_file',
		'imagify_refresh_file_modified',
	];

	/**
	 * Actions to be triggered only on admin ajax.
	 *
	 * @var array
	 */
	protected $ajax_only_actions = [
		// Settings page.
		'imagify_check_backup_dir_is_writable',
		'imagify_get_files_tree',
		// Account.
		'imagify_signup',
		'imagify_check_api_key_validity',
		'imagify_get_prices',
		'imagify_check_coupon',
		'imagify_get_discount',
		'imagify_get_images_counts',
		'imagify_update_estimate_sizes',
		'imagify_get_user_data',
		'imagify_delete_user_data_cache',
		// Various.
		'nopriv_imagify_rpc',
	];

	/**
	 * Actions to be triggered only on admin post.
	 *
	 * @var array
	 */
	protected $post_only_actions = [
		// Custom folders optimization.
		'imagify_scan_custom_folders',
		// Various.
		'imagify_dismiss_ad',
	];

	/**
	 * Filesystem object.
	 *
	 * @var Imagify_Filesystem
	 */
	protected $filesystem;


	/** ----------------------------------------------------------------------------------------- */
	/** INIT ==================================================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * The constructor.
	 */
	public function __construct() {
		$this->filesystem = Imagify_Filesystem::get_instance();
	}

	/**
	 * Launch the hooks.
	 *
	 * @since 1.6.11
	 */
	public function init() {
		$doing_ajax = wp_doing_ajax();

		foreach ( $this->ajax_post_actions as $action ) {
			$action_callback = "{$action}_callback";
			if ( $doing_ajax ) {
				add_action( 'wp_ajax_' . $action, [ $this, $action_callback ] );
			}
			add_action( 'admin_post_' . $action, [ $this, $action_callback ] );
		}

		// Actions triggered only on admin ajax.
		if ( $doing_ajax ) {
			foreach ( $this->ajax_only_actions as $action ) {
				add_action( 'wp_ajax_' . $action, [ $this, $action . '_callback' ] );
			}
		}

		// Actions triggered on admin post.
		foreach ( $this->post_only_actions as $action ) {
			add_action( 'admin_post_' . $action, [ $this, $action . '_callback' ] );
		}
	}


	/** ----------------------------------------------------------------------------------------- */
	/** OPTIMIZATION PROCESSES ================================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Optimize one media.
	 *
	 * @since 1.9
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @return bool|WP_Error    True if successfully launched. A \WP_Error instance on failure.
	 */
	protected function optimize_media( $media_id, $context ) {
		// A manual click is an explicit, single-image user action: flag it as priority so it
		// jumps ahead of any bulk optimization queue.
		return imagify_get_optimization_process( $media_id, $context )->optimize( null, [ 'priority' => true ] );
	}

	/**
	 * Re-optimize a media to a different optimization level.
	 *
	 * @since 1.9
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @param  int    $level    The optimization level.
	 * @return bool|WP_Error    True if successfully launched. A \WP_Error instance on failure.
	 */
	protected function reoptimize_media( $media_id, $context, $level ) {
		// A manual click is an explicit, single-image user action: flag it as priority so it
		// jumps ahead of any bulk optimization queue.
		return imagify_get_optimization_process( $media_id, $context )->reoptimize( $level, [ 'priority' => true ] );
	}

	/**
	 * Optimize all files from a media, whatever this media’s previous optimization status (will be restored if needed).
	 * This is used by the bulk optimization page.
	 *
	 * @since 1.9
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @param  int    $level    The optimization level.
	 * @return bool|WP_Error    True if successfully launched. A \WP_Error instance on failure.
	 */
	protected function force_optimize( $media_id, $context, $level ) {
		$process = imagify_get_optimization_process( $media_id, $context );
		$data    = $process->get_data();

		// Restore before re-optimizing.
		if ( $data->is_optimized() ) {
			$result = $process->restore();

			if ( is_wp_error( $result ) ) {
				// Return an error message.
				return $result;
			}
		}

		return $process->optimize( $level );
	}

	/**
	 * Optimize one or some thumbnails that are not optimized yet.
	 *
	 * @since 1.9
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @return bool|WP_Error    True if successfully launched. A \WP_Error instance on failure.
	 */
	protected function optimize_missing_sizes( $media_id, $context ) {
		return imagify_get_optimization_process( $media_id, $context )->optimize_missing_thumbnails();
	}

	/**
	 * Generate next-gen images if they are missing.
	 *
	 * @since 1.9
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @return bool|WP_Error    True if successfully launched. A \WP_Error instance on failure.
	 */
	protected function generate_nextgen_versions( $media_id, $context ) {
		return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions();
	}

	/**
	 * Delete Next gen images for media that are "already_optimize".
	 *
	 * @since 1.9.6
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @return bool|WP_Error    True if successfully launched. A \WP_Error instance on failure.
	 */
	protected function delete_nextgen_versions( $media_id, $context ) {
		$process = imagify_get_optimization_process( $media_id, $context );

		if ( ! $process->is_valid() ) {
			return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) );
		}

		$data = $process->get_data();

		if ( ! $data->is_already_optimized() ) {
			return new WP_Error( 'not_already_optimized', __( 'This media does not have the right optimization status.', 'imagify' ) );
		}

		if ( ! $process->has_next_gen() ) {
			return true;
		}

		$data->delete_optimization_data();
		$deleted = $process->delete_nextgen_files( false, true );

		if ( is_wp_error( $deleted ) ) {
			return new WP_Error( 'nextgen_not_deleted', __( 'Previous next-gen files could not be deleted.', 'imagify' ) );
		}

		return true;
	}

	/**
	 * Restore a media.
	 *
	 * @since 1.9
	 *
	 * @param  int    $media_id The media ID.
	 * @param  string $context  The context.
	 * @return bool|WP_Error    True on success. A \WP_Error instance on failure.
	 */
	protected function restore_media( $media_id, $context ) {
		return imagify_get_optimization_process( $media_id, $context )->restore();
	}

	/** ----------------------------------------------------------------------------------------- */
	/** WP OPTIMIZATION CALLBACKS =============================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Optimize all thumbnails of a specific image with the manual method.
	 *
	 * @since 1.6.11
	 */
	public function imagify_manual_optimize_callback() {
		$context  = $this->get_context();
		$media_id = $this->get_media_id();

		if ( ! $media_id || ! $context ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		imagify_check_nonce( 'imagify-optimize-' . $media_id . '-' . $context );

		if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$result = $this->optimize_media( $media_id, $context );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			$output = $result->get_error_message();

			wp_send_json_error( [ 'html' => $output ] );
		}

		wp_send_json_success();
	}

	/**
	 * Optimize all thumbnails of a specific image with a different optimization level.
	 *
	 * @since 1.6.11
	 */
	public function imagify_manual_reoptimize_callback() {
		$context  = $this->get_context();
		$media_id = $this->get_media_id();

		if ( ! $media_id || ! $context ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		imagify_check_nonce( 'imagify-manual-reoptimize-' . $media_id . '-' . $context );

		if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$result = $this->reoptimize_media( $media_id, $context, $this->get_optimization_level() );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			$output = $result->get_error_message();

			wp_send_json_error( [ 'html' => $output ] );
		}

		wp_send_json_success();
	}

	/**
	 * Optimize one or some thumbnails that are not optimized yet.
	 *
	 * @since 1.6.11
	 */
	public function imagify_optimize_missing_sizes_callback() {
		$context  = $this->get_context();
		$media_id = $this->get_media_id();

		if ( ! $media_id || ! $context ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		imagify_check_nonce( 'imagify-optimize-missing-sizes-' . $media_id . '-' . $context );

		if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$result = $this->optimize_missing_sizes( $media_id, $context );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			$output = $result->get_error_message();

			wp_send_json_error( [ 'html' => $output ] );
		}

		wp_send_json_success();
	}

	/**
	 * Generate next-gen images if they are missing.
	 *
	 * @since 1.9
	 */
	public function imagify_generate_nextgen_versions_callback() {
		$context  = $this->get_context();
		$media_id = $this->get_media_id();

		if ( ! $media_id || ! $context ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		imagify_check_nonce( 'imagify-generate-nextgen-versions-' . $media_id . '-' . $context );

		if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$result = $this->generate_nextgen_versions( $media_id, $context );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			$output = $result->get_error_message();

			wp_send_json_error( [ 'html' => $output ] );
		}

		wp_send_json_success();
	}

	/**
	 * Generate next-gen images if they are missing.
	 *
	 * @since 1.9.6
	 */
	public function imagify_delete_nextgen_versions_callback() {
		$context  = $this->get_context();
		$media_id = $this->get_media_id();

		if ( ! $media_id || ! $context ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		imagify_check_nonce( 'imagify-delete-nextgen-versions-' . $media_id . '-' . $context );

		if ( ! imagify_get_context( $context )->current_user_can( 'manual-restore', $media_id ) ) {
			imagify_die();
		}

		$result = $this->delete_nextgen_versions( $media_id, $context );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			$output = $result->get_error_message();

			wp_send_json_error( [ 'html' => $output ] );
		}

		wp_send_json_success();
	}

	/**
	 * Process a restoration to the original attachment.
	 *
	 * @since 1.6.11
	 */
	public function imagify_restore_callback() {
		$context  = $this->get_context();
		$media_id = $this->get_media_id();

		if ( ! $media_id || ! $context ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		imagify_check_nonce( 'imagify-restore-' . $media_id . '-' . $context );

		if ( ! imagify_get_context( $context )->current_user_can( 'manual-restore', $media_id ) ) {
			imagify_die();
		}

		$result = $this->restore_media( $media_id, $context );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			$output = $result->get_error_message();

			wp_send_json_error( [ 'html' => $output ] );
		}

		// Return the optimization button.
		$output = Imagify_Views::get_instance()->get_template(
			'button/optimize',
			[
				'url' => get_imagify_admin_url(
					'optimize',
					[
						'attachment_id' => $media_id,
						'context'       => $context,
					]
				),
			]
		);

		wp_send_json_success( [ 'html' => $output ] );
	}


	/** ----------------------------------------------------------------------------------------- */
	/** CUSTOM FOLDERS OPTIMIZATION CALLBACKS =================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Optimize a file.
	 *
	 * @since 1.7
	 */
	public function imagify_optimize_file_callback() {
		imagify_check_nonce( 'imagify_optimize_file' );

		$media_id = $this->get_media_id( 'GET', 'id' );

		if ( ! $media_id ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$result = $this->optimize_media( $media_id, 'custom-folders' );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			wp_send_json_error( $result->get_error_message() );
		}

		wp_send_json_success();
	}

	/**
	 * Re-optimize a file.
	 *
	 * @since 1.7
	 */
	public function imagify_reoptimize_file_callback() {
		imagify_check_nonce( 'imagify_reoptimize_file' );

		$media_id = $this->get_media_id( 'GET', 'id' );

		if ( ! $media_id ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$level = $this->get_optimization_level( 'GET', 'level' );

		$result = $this->reoptimize_media( $media_id, 'custom-folders', $level );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			wp_send_json_error( $result->get_error_message() );
		}

		wp_send_json_success();
	}

	/**
	 * Restore a file.
	 *
	 * @since 1.7
	 */
	public function imagify_restore_file_callback() {
		imagify_check_nonce( 'imagify_restore_file' );

		$media_id = $this->get_media_id( 'GET', 'id' );

		if ( ! $media_id ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-restore', $media_id ) ) {
			imagify_die();
		}

		$result = $this->restore_media( $media_id, 'custom-folders' );

		imagify_maybe_redirect( is_wp_error( $result ) ? $result : false );

		if ( is_wp_error( $result ) ) {
			// Return an error message.
			wp_send_json_error( $result->get_error_message() );
		}

		$process = imagify_get_optimization_process( $media_id, 'custom-folders' );
		$this->file_optimization_output( $process );
	}

	/**
	 * Check if a file has been modified, and update the database accordingly.
	 *
	 * @since 1.7
	 */
	public function imagify_refresh_file_modified_callback() {
		imagify_check_nonce( 'imagify_refresh_file_modified' );

		$media_id = $this->get_media_id( 'GET', 'id' );

		if ( ! $media_id ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manual-optimize', $media_id ) ) {
			imagify_die();
		}

		$process = imagify_get_optimization_process( $media_id, 'custom-folders' );
		$result  = Imagify_Custom_Folders::refresh_file( $process );

		if ( is_wp_error( $result ) ) {
			// The media is not valid or has been removed from the database.
			$message = $result->get_error_message();

			imagify_maybe_redirect( $message );

			wp_send_json_error(
				[
					'row' => $message,
				]
			);
		}

		imagify_maybe_redirect();

		// Return some HTML to the ajax call.
		$this->file_optimization_output( $process );
	}

	/**
	 * Look for new files in custom folders.
	 *
	 * @since 1.7
	 */
	public function imagify_scan_custom_folders_callback() {
		imagify_check_nonce( 'imagify_scan_custom_folders' );

		if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'optimize' ) ) {
			imagify_die();
		}

		$folder = (int) filter_input( INPUT_GET, 'folder', FILTER_VALIDATE_INT );

		if ( $folder > 0 ) {
			// A specific custom folder (selected or not).
			$folders_db  = Imagify_Folders_DB::get_instance();
			$folders_key = $folders_db->get_primary_key();
			$folder      = $folders_db->get( $folder );

			if ( ! $folder ) {
				// This should not happen.
				imagify_maybe_redirect( __( 'This folder is not in the database.', 'imagify' ) );
			}

			$folder['folder_path'] = Imagify_Files_Scan::remove_placeholder( $folder['path'] );

			$folders = [
				$folder[ $folders_key ] => $folder,
			];

			Imagify_Custom_Folders::get_files_from_folders(
				$folders,
				[
					'add_inactive_folder_files' => true,
				]
			);

			imagify_maybe_redirect();
		}

		// All selected custom folders.
		$folders = Imagify_Custom_Folders::get_folders(
			[
				'active' => true,
			]
		);
		Imagify_Custom_Folders::get_files_from_folders( $folders );

		imagify_maybe_redirect();
	}


	/** ----------------------------------------------------------------------------------------- */
	/** SETTINGS PAGE CALLBACKS ================================================================= */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Check if the backup directory is writable.
	 * This is used to display an error message in the plugin's settings page.
	 *
	 * @since 1.6.11
	 */
	public function imagify_check_backup_dir_is_writable_callback() {
		imagify_check_nonce( 'imagify_check_backup_dir_is_writable' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		wp_send_json_success(
			[
				'is_writable' => (int) Imagify_Requirements::attachments_backup_dir_is_writable(),
			]
		);
	}

	/**
	 * Get files and folders that are direct children of a given folder.
	 *
	 * @since 1.7
	 */
	public function imagify_get_files_tree_callback() {
		imagify_check_nonce( 'get-files-tree' );

		if ( ! imagify_get_context( 'custom-folders' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		if ( ! isset( $_POST['folder'] ) || '' === $_POST['folder'] ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		$folder = sanitize_text_field( wp_unslash( $_POST['folder'] ) );
		$folder = trailingslashit( $folder );
		$folder = realpath( $this->filesystem->get_site_root() . ltrim( $folder, '/' ) );

		if ( ! $folder ) {
			imagify_die( __( 'This folder doesn\'t exist.', 'imagify' ) );
		}

		if ( ! $this->filesystem->is_dir( $folder ) ) {
			imagify_die( __( 'This file is not a folder.', 'imagify' ) );
		}

		$folder = $this->filesystem->normalize_dir_path( $folder );

		if ( Imagify_Files_Scan::is_path_forbidden( $folder ) ) {
			imagify_die( __( 'This folder is not allowed.', 'imagify' ) );
		}

		// Finally we made all our validations.
		$selected = ! empty( $_POST['selected'] ) && is_array( $_POST['selected'] ) ? array_flip( array_map( 'sanitize_text_field', wp_unslash( $_POST['selected'] ) ) ) : [];
		$views    = Imagify_Views::get_instance();
		$output   = '';

		if ( $this->filesystem->is_site_root( $folder ) ) {
			$output .= $views->get_template(
				'part-settings-files-tree-row',
				[
					'relative_path'     => '/',
					// Value #///# Label.
					'checkbox_value'    => '{{ROOT}}/#///#' . esc_attr__( 'Site\'s root', 'imagify' ),
					'checkbox_id'       => 'ABSPATH',
					'checkbox_selected' => isset( $selected['{{ROOT}}/'] ),
					'label'             => __( 'Site\'s root', 'imagify' ),
					'no_button'         => true,
				]
			);
		}

		$dir    = new DirectoryIterator( $folder );
		$dir    = new Imagify_Files_Iterator( $dir );
		$images = 0;

		foreach ( new IteratorIterator( $dir ) as $file ) {
			if ( ! $file->isDir() ) {
				++$images;
				continue;
			}

			$folder_path   = trailingslashit( $file->getPathname() );
			$relative_path = $this->filesystem->make_path_relative( $folder_path );
			$placeholder   = Imagify_Files_Scan::add_placeholder( $folder_path );

			$output .= $views->get_template(
				'part-settings-files-tree-row',
				[
					'relative_path'     => esc_attr( $relative_path ),
					// Value #///# Label.
					'checkbox_value'    => esc_attr( $placeholder ) . '#///#' . esc_attr( $relative_path ),
					'checkbox_id'       => sanitize_html_class( $placeholder ),
					'checkbox_selected' => isset( $selected[ $placeholder ] ),
					'label'             => $this->filesystem->file_name( $folder_path ),
				]
			);
		}

		if ( $images ) {
			/* translators: %s is a formatted number, dont use %d. */
			$output .= '<li class="imagify-number-of-images-in-folder"><em><span class="dashicons dashicons-images-alt"></span> ' . sprintf( _n( '%s Media File', '%s Media Files', $images, 'imagify' ), number_format_i18n( $images ) ) . '</em></li>';
		}

		if ( ! $output ) {
			$output .= '<li class="imagify-empty-folder"><em>' . __( 'No optimizable files', 'imagify' ) . '</em></li>';
		}

		wp_send_json_success( $output );
	}


	/** ----------------------------------------------------------------------------------------- */
	/** IMAGIFY ACCOUNT CALLBACKS =============================================================== */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Create a new Imagify account.
	 *
	 * @since 1.6.11
	 */
	public function imagify_signup_callback() {
		imagify_check_nonce( 'imagify-signup', 'imagifysignupnonce' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		if ( empty( $_GET['email'] ) ) {
			imagify_die( __( 'Empty email address.', 'imagify' ) );
		}

		$email = sanitize_email( wp_unslash( $_GET['email'] ) );

		if ( ! is_email( $email ) ) {
			imagify_die( __( 'Not a valid email address.', 'imagify' ) );
		}

		$data = [
			'email'    => $email,
			'password' => wp_generate_password( 12, false ),
			'lang'     => imagify_get_locale(),
		];

		$response = add_imagify_user( $data );

		if ( is_wp_error( $response ) ) {
			imagify_die( $response );
		}

		wp_send_json_success();
	}

	/**
	 * Check the API key validity.
	 *
	 * @since 1.6.11
	 * @since 2.3.1 Ignore user abort while saving the key, so the option and its
	 *              related validity cache/notice are never left in a partial state
	 *              when the browser cancels the request (e.g. the user clicks away
	 *              or navigates while the check is still running).
	 */
	public function imagify_check_api_key_validity_callback() {
		imagify_check_nonce( 'imagify-check-api-key', 'imagifycheckapikeynonce' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		if ( empty( $_GET['api_key'] ) ) {
			imagify_die( __( 'Empty API key.', 'imagify' ) );
		}

		$api_key  = sanitize_key( wp_unslash( $_GET['api_key'] ) );
		$response = get_imagify_status( $api_key );

		if ( is_wp_error( $response ) ) {
			imagify_die( $response );
		}

		/*
		 * From this point, the key is known to be valid. Saving the option triggers
		 * `after_save_options()`, which performs its own request to the Imagify API
		 * to (re)set the "valid key" cache and dismiss/renew the "wrong-api-key" notice.
		 * If the client disconnects (e.g. the user clicks elsewhere or navigates away
		 * while the spinner is still showing), PHP would otherwise abort mid-way,
		 * leaving the key saved but the validity cache/notice stale or missing.
		 * Ignoring user abort for this short, bounded sequence guarantees the option
		 * and its validity state are always saved consistently together.
		 */
		$ignore_user_abort = ignore_user_abort( true );

		update_imagify_option( 'api_key', $api_key );

		delete_transient( 'imagify_user_cache' );

		ignore_user_abort( $ignore_user_abort );

		wp_send_json_success();
	}

	/**
	 * Get pricings from API for Onetime and Plans at the same time.
	 *
	 * @since 1.6.11
	 */
	public function imagify_get_prices_callback() {
		imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		$prices_all = get_imagify_all_prices();

		if ( is_wp_error( $prices_all ) ) {
			imagify_die( $prices_all );
		}

		if ( ! is_object( $prices_all ) ) {
			imagify_die( __( 'Wrongly formatted response from our server.', 'imagify' ) );
		}

		wp_send_json_success(
			[
				'monthlies' => $prices_all->Plans,
			]
		);
	}

	/**
	 * Check Coupon code on modal popin.
	 *
	 * @since 1.6.11
	 */
	public function imagify_check_coupon_callback() {
		imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		if ( empty( $_POST['coupon'] ) ) {
			wp_send_json_success(
				[
					'success' => false,
					'detail'  => __( 'Coupon is empty.', 'imagify' ),
				]
			);
		}

		$coupon = sanitize_text_field( wp_unslash( $_POST['coupon'] ) );
		$coupon = check_imagify_coupon_code( $coupon );

		if ( is_wp_error( $coupon ) ) {
			imagify_die( $coupon );
		}

		wp_send_json_success( imagify_translate_api_message( $coupon ) );
	}

	/**
	 * Get current discount promotion to display information on payment modal.
	 *
	 * @since 1.6.11
	 */
	public function imagify_get_discount_callback() {
		imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		wp_send_json_success( imagify_translate_api_message( check_imagify_discount() ) );
	}

	/**
	 * Get estimated sizes from the WordPress library.
	 *
	 * @since 1.6.11
	 */
	public function imagify_get_images_counts_callback() {
		imagify_check_nonce( 'imagify_get_pricing_' . get_current_user_id(), 'imagifynonce' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		$raw_total_size_in_library = imagify_calculate_total_size_images_library() + Imagify_Files_Stats::get_overall_original_size();
		$raw_average_per_month     = imagify_calculate_average_size_images_per_month() + Imagify_Files_Stats::calculate_average_size_per_month();

		Imagify_Data::get_instance()->set(
			[
				'total_size_images_library'     => $raw_total_size_in_library,
				'average_size_images_per_month' => $raw_average_per_month,
			]
		);

		wp_send_json_success(
			[
				'total_library_size' => [
					'raw'   => $raw_total_size_in_library,
					'human' => imagify_size_format( $raw_total_size_in_library ),
				],
				'average_month_size' => [
					'raw'   => $raw_average_per_month,
					'human' => imagify_size_format( $raw_average_per_month ),
				],
			]
		);
	}

	/**
	 * Estimate sizes and update the options values for them.
	 *
	 * @since 1.6.11
	 */
	public function imagify_update_estimate_sizes_callback() {
		imagify_check_nonce( 'update_estimate_sizes' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		$raw_total_size_in_library = imagify_calculate_total_size_images_library() + Imagify_Files_Stats::get_overall_original_size();
		$raw_average_per_month     = imagify_calculate_average_size_images_per_month() + Imagify_Files_Stats::calculate_average_size_per_month();

		Imagify_Data::get_instance()->set(
			[
				'total_size_images_library'     => $raw_total_size_in_library,
				'average_size_images_per_month' => $raw_average_per_month,
			]
		);

		die( 1 );
	}

	/**
	 * Get the Imagify User data.
	 *
	 * @since 1.7
	 */
	public function imagify_get_user_data_callback() {
		imagify_check_nonce( 'imagify_get_user_data' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		$user = imagify_cache_user();

		if ( ! $user || ! $user->id ) {
			imagify_die( __( 'Couldn\'t get user data.', 'imagify' ) );
		}

		// Remove useless sensitive data.
		unset( $user->email );

		if ( ! $user->get_percent_unconsumed_quota ) {
			$user->best_plan_title = __( 'Oops, It\'s Over!', 'imagify' );
		} elseif ( $user->get_percent_unconsumed_quota <= 20 ) {
			$user->best_plan_title = __( 'Oops, It\'s almost over!', 'imagify' );
		} else {
			$user->best_plan_title = __( 'Unlock Imagify\'s full potential', 'imagify' );
		}

		wp_send_json_success( $user );
	}

	/**
	 * Delete the Imagify User data cache.
	 *
	 * @since 1.9.5
	 */
	public function imagify_delete_user_data_cache_callback() {
		imagify_check_nonce( 'imagify_delete_user_data_cache' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		imagify_delete_cached_user();

		wp_send_json_success();
	}


	/** ----------------------------------------------------------------------------------------- */
	/** VARIOUS CALLBACKS ======================================================================= */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Bridge between XML-RPC and actions triggered by imagify_do_async_job().
	 * When XML-RPC is used, a current user is set, but no cookies are set, so they cannot be sent with the request. Instead we stored the user ID in a transient.
	 *
	 * @since 1.6.11
	 * @see imagify_do_async_job()
	 */
	public function nopriv_imagify_rpc_callback() {
		if ( empty( $_POST['imagify_rpc_action'] ) || empty( $_POST['imagify_rpc_id'] ) ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		$action = sanitize_text_field( wp_unslash( $_POST['imagify_rpc_action'] ) );

		if ( 32 !== strlen( $action ) ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		// Not necessary but just in case, whitelist the original action.
		$actions = array_flip( $this->ajax_only_actions );
		unset( $actions['nopriv_imagify_rpc'] );

		if ( ! isset( $actions[ $action ] ) ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		// Get the user ID.
		$rpc_id  = sanitize_key( $_POST['imagify_rpc_id'] );
		$user_id = absint( get_transient( 'imagify_rpc_' . $rpc_id ) );
		$user    = $user_id ? get_userdata( $user_id ) : false;

		delete_transient( 'imagify_rpc_' . $rpc_id );

		if ( ! $user || ! $user->exists() ) {
			imagify_die( __( 'Invalid request', 'imagify' ) );
		}

		// The current user must be set before verifying the nonce.
		wp_set_current_user( $user_id );

		imagify_check_nonce( 'imagify_rpc_' . $rpc_id, 'imagify_rpc_nonce' );

		// Trigger the action we originally wanted.
		$_POST['action'] = $action;
		unset( $_POST['imagify_rpc_action'], $_POST['imagify_rpc_id'], $_POST['imagify_rpc_nonce'] );

		/** This hook is documented in wp-admin/admin-ajax.php. */
		do_action( 'wp_ajax_' . $action );
	}

	/**
	 * Store the "closed" status of the ads.
	 *
	 * @since 1.7
	 */
	public function imagify_dismiss_ad_callback() {
		imagify_check_nonce( 'imagify-dismiss-ad' );

		if ( ! imagify_get_context( 'wp' )->current_user_can( 'manage' ) ) {
			imagify_die();
		}

		if ( empty( $_GET['ad'] ) ) {
			imagify_maybe_redirect();
			wp_send_json_error();
		}

		$notice = sanitize_text_field( wp_unslash( $_GET['ad'] ) );

		if ( ! $notice ) {
			imagify_maybe_redirect();
			wp_send_json_error();
		}

		$user_id = get_current_user_id();
		$notices = get_user_meta( $user_id, '_imagify_ignore_ads', true );
		$notices = $notices && is_array( $notices ) ? array_flip( $notices ) : [];

		if ( isset( $notices[ $notice ] ) ) {
			imagify_maybe_redirect();
			wp_send_json_success();
		}

		$notices   = array_flip( $notices );
		$notices[] = $notice;
		$notices   = array_filter( $notices );
		$notices   = array_values( $notices );

		update_user_meta( $user_id, '_imagify_ignore_ads', $notices );

		imagify_maybe_redirect();
		wp_send_json_success();
	}


	/** ----------------------------------------------------------------------------------------- */
	/** VARIOUS HELPERS ========================================================================= */
	/** ----------------------------------------------------------------------------------------- */

	/**
	 * Get the submitted optimization level.
	 *
	 * @since 1.7
	 * @since 1.9 Added $method and $parameter parameters.
	 *
	 * @param  string $method The method used: 'GET' (default), or 'POST'.
	 * @param  string $parameter The name of the parameter to look for.
	 * @return int
	 */
	public function get_optimization_level( $method = 'GET', $parameter = 'optimization_level' ) {
		$method = 'POST' === $method ? INPUT_POST : INPUT_GET;
		$level  = filter_input( $method, $parameter );

		if ( ! is_numeric( $level ) || $level < 0 || $level > 2 ) {
			if ( get_imagify_option( 'lossless' ) ) {
				return 0;
			}

			return get_imagify_option( 'optimization_level' );
		}

		return (int) $level;
	}

	/**
	 * Get the submitted context.
	 *
	 * @since 1.9
	 *
	 * @param  string $method The method used: 'GET' (default), or 'POST'.
	 * @param  string $parameter The name of the parameter to look for.
	 * @return string
	 */
	public function get_context( $method = 'GET', $parameter = 'context' ) {
		if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) {
			// No context.
			return 'noop';
		}

		$context = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); //phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended

		return imagify_sanitize_context( $context );
	}

	/**
	 * Get the submitted media ID.
	 *
	 * @since 1.9
	 *
	 * @param  string $method    The method used: 'GET' (default), or 'POST'.
	 * @param  string $parameter The name of the parameter to look for.
	 * @return int
	 */
	public function get_media_id( $method = 'GET', $parameter = 'attachment_id' ) {
		$method   = 'POST' === $method ? INPUT_POST : INPUT_GET;
		$media_id = filter_input( $method, $parameter );

		if ( ! is_numeric( $media_id ) || $media_id < 0 ) {
			return 0;
		}

		return (int) $media_id;
	}

	/**
	 * Get the submitted folder_type.
	 *
	 * @since 1.9
	 *
	 * @param string $method    The method used: 'GET' (default), or 'POST'.
	 * @param string $parameter The name of the parameter to look for.
	 *
	 * @return string
	 */
	public function get_folder_type( $method = 'GET', $parameter = 'folder_type' ) {
		if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) {
			// No folder type.
			return 'noop';
		}

		$folder_type = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended

		return $folder_type;
	}

	/**
	 * Get the submitted imagify action.
	 *
	 * @since 1.9
	 *
	 * @param string $method    The method used: 'GET' (default), or 'POST'.
	 * @param string $parameter The name of the parameter to look for.
	 *
	 * @return string
	 */
	public function get_imagify_action( $method = 'GET', $parameter = 'imagify_action' ) {
		if ( empty( $_POST[ $parameter ] ) && empty( $_GET[ $parameter ] ) ) {
			// No action.
			return 'optimize';
		}

		$action = 'POST' === $method ? sanitize_text_field( wp_unslash( $_POST[ $parameter ] ) ) : sanitize_text_field( wp_unslash( $_GET[ $parameter ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended

		return $action ? $action : 'optimize';
	}

	/**
	 * Get the Bulk class name depending on a context.
	 *
	 * @since 1.9
	 *
	 * @param  string $context The context name. Default values are 'wp' and 'custom-folders'.
	 * @return string          The Bulk class name.
	 */
	public function get_bulk_class_name( $context ) {
		switch ( $context ) {
			case 'wp':
				$class_name = '\\Imagify\\Bulk\\WP';
				break;

			case 'custom-folders':
				$class_name = '\\Imagify\\Bulk\\CustomFolders';
				break;

			default:
				$class_name = '\\Imagify\\Bulk\\Noop';
		}

		/**
		* Filter the name of the class to use for bulk process.
		*
		* @since 1.9
		*
		* @param int    $class_name The class name.
		* @param string $context    The context name.
		*/
		$class_name = apply_filters( 'imagify_bulk_class_name', $class_name, $context );

		return '\\' . ltrim( $class_name, '\\' );
	}

	/**
	 * Get the Bulk instance depending on a context.
	 *
	 * @since 1.9
	 *
	 * @param  string $context The context name. Default values are 'wp' and 'custom-folders'.
	 * @return BulkInterface   The optimization process instance.
	 */
	public function get_bulk_instance( $context ) {
		$class_name = $this->get_bulk_class_name( $context );
		return new $class_name();
	}

	/**
	 * Check if the user has a valid account and has quota. Die on failure.
	 *
	 * @since 1.7
	 */
	public function check_can_optimize() {
		if ( ! Imagify_Requirements::is_api_key_valid() ) {
			if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
				wp_send_json_error( [ 'message' => 'invalid-api-key' ] );
			}

			imagify_die( __( 'Your API key is not valid!', 'imagify' ) );
		}

		if ( Imagify_Requirements::is_over_quota() ) {
			if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
				wp_send_json_error( [ 'message' => 'over-quota' ] );
			}

			imagify_die( __( 'You have used all your credits!', 'imagify' ) );
		}
	}

	/**
	 * Get a media columns for the "Other Media" page.
	 *
	 * @since 1.9
	 *
	 * @param  object $process    A \Imagify\Optimization\Process\CustomFolders object.
	 * @param  object $list_table A Imagify_Files_List_Table object.
	 * @return array              An array of HTML, keyed by column name.
	 */
	public function get_media_columns( $process, $list_table ) {
		$item = (object) [ 'process' => $process ];

		return [
			'folder'             => $list_table->get_column( 'folder', $item ),
			'optimization'       => $list_table->get_column( 'optimization', $item ),
			'status'             => $list_table->get_column( 'status', $item ),
			'optimization_level' => $list_table->get_column( 'optimization_level', $item ),
			'actions'            => $list_table->get_column( 'actions', $item ),
			'title'              => $list_table->get_column( 'title', $item ), // This one must remain after the "optimization" column, otherwize the data for the comparison tool won't be up-to-date.
		];
	}

	/**
	 * After a file optimization, restore, or whatever, redirect the user or output HTML for ajax.
	 *
	 * @since 1.7
	 * @since 1.9 Removed parameter $result.
	 * @since 1.9 Added $folder in the returned JSON.
	 *
	 * @param object $process A \Imagify\Optimization\Process\CustomFolders object.
	 */
	protected function file_optimization_output( $process ) {
		$list_table = new Imagify_Files_List_Table(
			[
				'screen' => 'imagify-files',
			]
		);

		wp_send_json_success(
			[
				'columns' => $this->get_media_columns( $process, $list_table ),
			]
		);
	}
}
