<?php
/**
 * Plugin update class
 *
 * @since      1.0
 * @package    RankMathPro
 * @subpackage RankMathPro\Admin
 * @author     Rank Math <support@rankmath.com>
 */

namespace RankMathPro\Plugin_Update;

use RankMath\KB;
use RankMath\Helper;
use RankMath\Helpers\Param;
use RankMath\Admin\Admin_Helper;
use RankMath\Traits\Hooker;

defined( 'ABSPATH' ) || exit;

/**
 * Plugin_Update class
 *
 * @method action()
 */
class Plugin_Update {

	use Hooker;

	/**
	 * Placeholder for opening tag inserted with JS.
	 *
	 * @var string
	 */
	const NOTICE_START_MARKER = '[[';

	/**
	 * Placeholder for closing tag inserted with JS.
	 *
	 * @var string
	 */
	const NOTICE_END_MARKER = ']]';

	/**
	 * Plugin slug.
	 *
	 * @var string
	 */
	private $slug = 'seo-by-rank-math-pro';

	/**
	 * Rank Math API URL.
	 *
	 * @var string
	 */
	private $api_url = 'https://rankmath.com/wp-json/rankmath/v1';

	/**
	 * Keep a log of external requests made in this thread so we can avoid
	 * running them multiple times.
	 *
	 * @var string
	 */
	private $requested = [];

	/**
	 * The Constructor.
	 *
	 * @return void
	 */
	public function __construct() {
		$this->action( 'admin_enqueue_scripts', 'enqueue' );
		$this->action( 'admin_notices', 'admin_license_notice', 20 );
		$this->action( 'in_plugin_update_message-seo-by-rank-math-pro/rank-math-pro.php', 'beta_update_message', 20 );
		$this->action( 'in_plugin_update_message-' . plugin_basename( RANK_MATH_PRO_FILE ), 'in_plugin_update_message', 30, 2 );
		$this->action( 'add_option_rank_math_connect_data', 'check_and_inject' );
		$this->action( 'update_option_rank_math_connect_data', 'check_and_inject' );
		$this->action( 'delete_option_rank_math_connect_data', 'check_and_inject' );
		$this->action( 'rank_math/settings/toggle_auto_update', 'toggle_auto_update', 10, 1 );
		$this->action( 'update_site_option_auto_update_plugins', 'connect_auto_update_toggles', 20, 3 );
		$this->action( 'admin_action_rank_math_pro_force_check', 'handle_force_check' );
		$this->action( 'admin_notices', 'force_check_notice' );

		// For multisite, trigger network update from sub-site if PRO isn’t network-active.
		if ( is_multisite() ) {
			$this->action( 'load-plugins.php', 'prime_network_update_from_subsite', 9 );
			$this->action( 'load-update-core.php', 'prime_network_update_from_subsite', 9 );
		}

		$this->filter( 'plugin_action_links_' . plugin_basename( RANK_MATH_PRO_FILE ), 'plugin_action_links', 50 );
		$this->filter( 'pre_set_site_transient_update_plugins', 'maybe_inject_update', 20, 1 );
		$this->filter( 'site_transient_update_plugins', 'maybe_disable_update', 90, 1 );
		$this->filter( 'pre_set_site_transient_update_plugins', 'maybe_add_upgrade_notice', 120, 1 );
		$this->filter( 'plugins_api', 'filter_info', 10, 3 );
		$this->filter( 'admin_print_footer_scripts-plugin-install.php', 'iframe_footer_scripts', 10, 3 );

		$this->filter( 'auto_update_plugin', 'auto_update_plugin', 20, 2 );
		$this->filter( 'plugin_auto_update_setting_html', 'plugin_auto_update_setting_html', 10, 3 );

		$this->filter( 'rank_math/admin/should_send_update_notification', 'should_send_update_notification', 10, 2 );
		$this->filter( 'rank_math/admin/update_notification_products', 'update_notification_products', 10, 2 );
	}

	/**
	 * Enqueue styles and scripts.
	 *
	 * @param  string $hook Page hook prefix.
	 *
	 * @return void
	 */
	public function enqueue( $hook ) {
		if ( 'update-core.php' !== $hook ) {
			return;
		}

		Helper::add_json( 'canUpdatePro', $this->can_update() );
		Helper::add_json( 'betaOptinEnabled', Helper::get_settings( 'general.beta_optin' ) );
		wp_enqueue_script( 'rank-math-pro-updates', RANK_MATH_PRO_URL . 'assets/admin/js/updates.js', [], RANK_MATH_PRO_VERSION, true );
	}

	/**
	 * Returns false if Rank Math Pro update object exists but its 'package'
	 * item is empty, which means that the user cannot update. Returns true in
	 * all other cases – even if update is not available ATM.
	 *
	 * @param mixed $transient Update object to check or null to load transient.
	 * @return boolean
	 */
	public function can_update( $transient = null ) {
		if ( is_null( $transient ) ) {
			$transient = get_site_transient( 'update_plugins' );
		}

		if ( ! is_object( $transient ) ) {
			return true;
		}

		if ( ! isset( $transient->response ) || ! isset( $transient->response['seo-by-rank-math-pro/rank-math-pro.php'] ) ) {
			return true;
		}

		return ( ! empty( $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->package ) );
	}

	/**
	 * Add connect/activation notice.
	 *
	 * @return void
	 */
	public function admin_license_notice() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		if ( $this->is_block_editor_page() ) {
			return;
		}

		if ( Helper::is_site_connected() || rank_math()->registration->invalid ) {
			return;
		}
		?>
			<div class="notice notice-success rank-math-notice rank-math-admin-license-notice">
				<p>
					<?php
					esc_html_e( 'Rank Math PRO is installed but it is not connected to your account, so you are missing out on important SEO features.', 'rank-math-pro' );
					echo '<br>';
					// translators: 1: opening HTML anchor tag, 2: closing HTML anchor tags.
					echo wp_kses_post( sprintf( __( '%1$sConnect now%2$s. It only takes 20 seconds!', 'rank-math-pro' ), '<a href="' . esc_url( Admin_Helper::get_activate_url() ) . '">', '</a>' ) );
					?>
				</p>
			</div>
		<?php
	}

	/**
	 * Check if we are on Block Editor page.
	 *
	 * @return bool
	 */
	private function is_block_editor_page() {
		$current_screen = get_current_screen();

		if ( $current_screen instanceof \WP_Screen && method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
			return true;
		}

		if ( function_exists( 'is_gutenberg_page' ) && is_gutenberg_page() ) {
			return true;
		}

		return false;
	}

	/**
	 * Add Connect/Activate action link
	 *
	 * @param  array $links Action links.
	 * @return array
	 */
	public function plugin_action_links( $links ) {
		if ( ! Helper::is_site_connected() ) {
			$links['activate_license'] = sprintf( '<a href="%s" class="rank-math-pro-activate-link" style="color:green">%s</a>', esc_url( Admin_Helper::get_activate_url( network_admin_url( 'plugins.php' ) ) ), __( 'Enable updates', 'rank-math-pro' ) );
		}

		// Add row action to check updates from sub-site when PRO isn’t network-active.
		if ( is_multisite() && ! is_network_admin() && current_user_can( 'update_plugins' ) && ! $this->is_network_activated() ) {
			$links['rank_math_pro_check'] = sprintf(
				'<a href="%s">%s</a>',
				esc_url( wp_nonce_url( admin_url( 'plugins.php?action=rank_math_pro_force_check' ), 'rank_math_pro_force_check_update' ) ),
				esc_html__( 'Check for updates', 'rank-math-pro' )
			);
		}

		return $links;
	}

	/**
	 * Handle the sub-site force check action to fetch & inject update data.
	 *
	 * @return void
	 */
	public function handle_force_check() {
		if ( ! current_user_can( 'update_plugins' ) ) {
			wp_die( esc_html__( 'Sorry, you are not allowed to update plugins on this site.', 'rank-math-pro' ) );
		}

		check_admin_referer( 'rank_math_pro_force_check_update' );

		// Force refetch and inject the update into the site-wide transient.
		$this->inject_update( $this->fetch_latest_version( true ) );

		// Redirect back to Plugins with a flag to show a notice.
		$redirect = remove_query_arg( [ 'action', '_wpnonce' ], wp_get_referer() ? wp_get_referer() : admin_url( 'plugins.php' ) );
		$redirect = add_query_arg( 'rank_math_pro_force_checked', '1', $redirect );
		wp_safe_redirect( $redirect );
		exit;
	}

	/**
	 * Show an admin notice after a manual force-check is performed.
	 *
	 * @return void
	 */
	public function force_check_notice() {
		if ( ! is_admin() || ! current_user_can( 'update_plugins' ) ) {
			return;
		}

		if ( ! Param::get( 'rank_math_pro_force_checked' ) ) {
			return;
		}

		$message = __( 'Checked for Rank Math SEO PRO updates.', 'rank-math-pro' );
		if ( is_multisite() && ! is_network_admin() ) {
			$updates_link = sprintf(
				'<a href="%s">%s</a>',
				esc_url( network_admin_url( 'update-core.php' ) ),
				esc_html__( 'Network Admin → Updates', 'rank-math-pro' )
			);
			/* translators: 1: linked text "Network Admin → Updates" */
			$message .= ' ' . sprintf( __( 'If an update is available, it will appear in %s.', 'rank-math-pro' ), $updates_link );
		}
		?>
		<div class="notice notice-success is-dismissible"><p><?php echo wp_kses_post( $message ); ?></p></div>
		<?php
	}

	/**
	 * Check if PRO is network-activated in multisite.
	 *
	 * @return bool
	 */
	private function is_network_activated() {
		if ( ! is_multisite() ) {
			return false;
		}

		if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php'; // @phpstan-ignore-line
		}

		return function_exists( 'is_plugin_active_for_network' ) && is_plugin_active_for_network( 'seo-by-rank-math-pro/rank-math-pro.php' );
	}

	/**
	 * If user requested check with force-check parameter.
	 *
	 * @return bool
	 */
	public function is_check_requested() {
		return (bool) Param::get( 'force-check' );
	}

	/**
	 * Check for updates & inject to update_plugins transient.
	 *
	 * @return void
	 */
	public function check_and_inject() {
		$this->inject_update( $this->fetch_latest_version( true ) );
	}

	/**
	 * Populate network 'update_plugins' transient with PRO data when active only on a sub-site.
	 * Runs from sub-site admin to make updates visible in Network Admin.
	 *
	 * @return void
	 */
	public function prime_network_update_from_subsite() {
		// Only act outside Network Admin and for users capable of updating plugins.
		if ( is_network_admin() || ! current_user_can( 'update_plugins' ) ) {
			return;
		}

		// Throttle priming to avoid repeated work across admin requests.
		if ( get_site_transient( 'rank_math_pro_prime_throttle' ) ) {
			return;
		}

		$plugin    = 'seo-by-rank-math-pro/rank-math-pro.php';
		$transient = get_site_transient( 'update_plugins' );

		// Skip if network transient already has plugin entry.
		if (
			is_object( $transient ) && (
				( isset( $transient->response[ $plugin ] ) && is_object( $transient->response[ $plugin ] ) ) ||
				( isset( $transient->no_update[ $plugin ] ) && is_object( $transient->no_update[ $plugin ] ) )
			)
		) {
			return;
		}

		// Use cache if warm; else run version check and update network transient.
		$this->inject_update( $this->fetch_latest_version( false ) );

		// Avoid running again for 2 hours.
		set_site_transient( 'rank_math_pro_prime_throttle', 1, 120 * MINUTE_IN_SECONDS );
	}

	/**
	 * Inject update fetched from the rankmath.com API or pushed to this site via the REST API.
	 *
	 * @param object $transient Origial transient.
	 * @return mixed
	 */
	public function maybe_inject_update( $transient ) {
		$force_check = $this->is_check_requested();
		return $this->inject_update( $this->fetch_latest_version( $force_check ), $transient );
	}

	/**
	 * Remove package download URL if needed.
	 *
	 * @param object $transient Original transient.
	 * @return mixed
	 */
	public function maybe_disable_update( $transient ) {
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return $transient;
		}

		// If we're in the process of updating RM Free then don't disable RM Pro update.
		if (
			( Param::get( 'action' ) === 'update-selected' && strpos( Param::get( 'plugins', '' ), 'seo-by-rank-math/rank-math.php' ) !== false ) ||
			$this->do_filter( 'updates/remove_restrictions', false )
		) {
			return $transient;
		}

		if ( isset( $transient->response['seo-by-rank-math/rank-math.php'] )
			&& isset( $transient->response['seo-by-rank-math-pro/rank-math-pro.php'] )
			&& ! empty( $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->package )
		) {
			unset( $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->package );
			$transient->response['seo-by-rank-math-pro/rank-math-pro.php']->unavailability_reason = 'update_free';
		}

		return $transient;
	}

	/**
	 * Add upgrade notice if needed, which is displayed on the Updates page (wp-admin/update-core.php)
	 *
	 * @param object $transient Original transient.
	 * @return mixed
	 */
	public function maybe_add_upgrade_notice( $transient ) {
		if ( ! $this->can_update( $transient ) ) {
			$before  = self::NOTICE_START_MARKER;
			$before .= __( 'Automatic updates are not available.', 'rank-math-pro' );
			$before .= ' ';
			$after   = self::NOTICE_END_MARKER;

			$message = $this->get_update_message( $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->unavailability_reason );
			$message = $before . $message . $after;

			$transient->response['seo-by-rank-math-pro/rank-math-pro.php']->upgrade_notice = $message;
		}

		return $transient;
	}

	/**
	 * Inject our update in the update_plugins transient.
	 *
	 * @param  mixed $update    New update object or array, or false to clear current update.
	 * @param  mixed $transient Original updates transient.
	 * @return mixed
	 */
	public function inject_update( $update, $transient = null ) {
		$save = false;
		if ( is_null( $transient ) ) {
			$transient = get_site_transient( 'update_plugins' );
			$save      = true;
		}

		$plugin = plugin_basename( RANK_MATH_PRO_FILE );
		if ( false !== $update ) {
			$obj = $this->get_default_update_data();
			$obj = (object) array_merge( (array) $obj, (array) $update );

			$obj = $this->maybe_apply_beta( $obj );

			// If a newer version is already present in the update_plugins transient then don't inject.
			if ( $this->has_newer_version( $transient, $obj->new_version ) ) {
				return $transient;
			}

			// Default transient data.
			if ( empty( $transient ) ) {
				$transient = new \stdClass();

				$transient->last_checked = time();
				$transient->checked      = [ $plugin => RANK_MATH_PRO_VERSION ];
				$transient->response     = [];
			}

			// Inject if new data has URL and is a newer version than the one currently in use.
			if ( version_compare( $obj->new_version, RANK_MATH_PRO_VERSION, '>' ) ) {
				$transient->response[ $plugin ] = $obj;
				if ( $save ) {
					set_site_transient( 'update_plugins', $transient );
				}
			} else {
				// No update is available.
				$item = (object) [
					'id'            => 'seo-by-rank-math-pro/rank-math-pro.php',
					'slug'          => 'seo-by-rank-math-pro',
					'plugin'        => 'seo-by-rank-math-pro/rank-math-pro.php',
					'new_version'   => RANK_MATH_PRO_VERSION,
					'url'           => '',
					'package'       => '',
					'icons'         => [],
					'banners'       => [],
					'banners_rtl'   => [],
					'tested'        => '',
					'requires_php'  => '',
					'compatibility' => new \stdClass(),
				];

				if ( empty( $transient->no_update ) ) {
					$transient->no_update = [];
				}

				$transient->no_update[ $plugin ] = $item;
			}
		} elseif ( isset( $transient->response[ $plugin ] ) ) {
			unset( $transient->response[ $plugin ] );
		}

		return $transient;
	}

	/**
	 * Replace `new_version` & `package` properties with beta if needed.
	 *
	 * @param object $update_object Plugin update data object.
	 * @return object
	 */
	public function maybe_apply_beta( $update_object ) {
		if ( ! Helper::get_settings( 'general.beta_optin' ) ) {
			return $update_object;
		}

		if (
			empty( $update_object->new_version )
			|| empty( $update_object->beta_version )
			|| empty( $update_object->package )
			|| empty( $update_object->beta_package )
		) {
			return $update_object;
		}

		if ( version_compare( $update_object->new_version, $update_object->beta_version, '>' ) ) {
			return $update_object;
		}

		$update_object->is_beta = true;

		// Store stable version data in case it's needed later.
		$update_object->stable_version = $update_object->new_version;
		$update_object->stable_package = $update_object->package;

		// Override stable version data with beta.
		$update_object->new_version = $update_object->beta_version;
		$update_object->package     = $update_object->beta_package;

		// Add notice that's shown on the Updates page.
		$update_object->upgrade_notice = self::NOTICE_START_MARKER . ' ' . __( 'This update will install a beta version of Rank Math SEO PRO.', 'rank-math-pro' ) . ' ' . self::NOTICE_END_MARKER;

		return $update_object;
	}

	/**
	 * Show beta update message on the Plugins screen.
	 *
	 * @param  array $plugin_data An array of plugin metadata.
	 */
	public function beta_update_message( $plugin_data ) {
		if ( empty( $plugin_data['is_beta'] ) ) {
			return;
		}

		printf(
			'</p><p class="rank-math-beta-update-notice">%s',
			esc_html__( 'This update will install a beta version of Rank Math SEO PRO.', 'rank-math-pro' )
		);

		$this->action( 'admin_footer', 'print_beta_notice_css' );
	}

	/**
	 * Print CSS related to the beta notice on the Plugins screen.
	 *
	 * @return void
	 */
	public function print_beta_notice_css() {
		?>
		<style>
			.update-message .rank-math-beta-update-notice {
				font-weight: bold;
				margin-top: 20px;
			}
			.update-message p.rank-math-beta-update-notice:before {
				content: "\f534";
			}
		</style>
		<?php
	}

	/**
	 * Check if the transient already contains newer version of the plugin.
	 *
	 * @param object $transient   Site transient: 'update_plugins'.
	 * @param string $new_version New version we check against, e.g. '1.2'.
	 * @return boolean
	 */
	public function has_newer_version( $transient, $new_version ) {
		$plugin = plugin_basename( RANK_MATH_PRO_FILE );

		return isset( $transient->response[ $plugin ] )
			&& is_object( $transient->response[ $plugin ] )
			&& isset( $transient->response[ $plugin ]->new_version )
			&& version_compare( $transient->response[ $plugin ]->new_version, $new_version, '>' );
	}

	/**
	 * Filter plugin information.
	 *
	 * @param false|object|array $result The result object or array. Default false.
	 * @param string             $action The type of information being requested from the Plugin Installation API.
	 * @param object             $args   Plugin API arguments.
	 * @return false|object      false or Response object.
	 */
	public function filter_info( $result, $action, $args ) {
		if ( ! isset( $args->slug ) || ! ( $this->slug === $args->slug && 'plugin_information' === $action ) ) {
			return $result;
		}

		$information = $this->get_plugin_info();
		if ( $this->has_beta_update( $information ) ) {
			$information->sections['changelog'] = $information->sections['beta_changelog'];
		}
		unset( $information->sections['beta_changelog'] );

		return $information;
	}

	/**
	 * Add JS in plugin-info iframe, to disable the Update button if necessary.
	 *
	 * @return void
	 */
	public function iframe_footer_scripts() {
		if ( Param::get( 'plugin' ) !== 'seo-by-rank-math-pro' ) {
			return;
		}

		$transient = get_site_transient( 'update_plugins' );
		$response  = isset( $transient->response['seo-by-rank-math-pro/rank-math-pro.php'] ) ? $transient->response['seo-by-rank-math-pro/rank-math-pro.php'] : new \stdClass();
		if ( empty( $response->package ) && isset( $response->unavailability_reason ) ) {
				$message = $this->get_update_message( $response->unavailability_reason );
			?>
			<script type="text/javascript">jQuery( function() {
				var $button = jQuery( '#plugin_update_from_iframe' );
				if ( $button.data( 'plugin' ) !== 'seo-by-rank-math-pro/rank-math-pro.php' ) {
					return;
				}

				$button.css( 'pointer-events', 'none' ).addClass( 'disabled' ).text( '<?php echo esc_js( __( 'Cannot Update', 'rank-math-pro' ) ); ?>' );
				jQuery( '#section-holder' ).prepend( '<div class="notice notice-error notice-alt"><p><strong><?php echo esc_js( $message ); ?></strong></p></div>' );
			} );</script>
			<?php
		}
	}

	/**
	 * Check if plugin update data & info object contain a valid beta update.
	 *
	 * @param object $plugin_info Plugin info object.
	 * @return boolean
	 */
	private function has_beta_update( $plugin_info ) {
		$beta_optin_enabled = Helper::get_settings( 'general.beta_optin' );
		$plugin             = plugin_basename( RANK_MATH_PRO_FILE );
		$transient          = get_site_transient( 'update_plugins' );

		return (
			$beta_optin_enabled
			&& ! empty( $transient->response[ $plugin ]->beta_version )
			&& ! empty( $plugin_info->sections['beta_changelog'] )
			&& version_compare( $transient->response[ $plugin ]->beta_version, ! empty( $transient->response[ $plugin ]->stable_version ) ? $transient->response[ $plugin ]->stable_version : $transient->response[ $plugin ]->new_version, '>=' )
		);
	}

	/**
	 * Check if version check endpoint's response contains new beta verison.
	 *
	 * @param array $data Latest versions.
	 * @return boolean
	 */
	private function new_beta_version_available( $data ) {
		return (
			Helper::get_settings( 'general.beta_optin' )
			&& version_compare( $data['beta_version'], $data['new_version'], '>' )
			&& version_compare( $data['beta_version'], RANK_MATH_PRO_VERSION, '>' )
		);
	}

	/**
	 * Merge default plugin data with fetched data.
	 *
	 * @param bool   $force_check Disregard cached data & always re-fetch.
	 * @param string $locale      Requested locale. Optional. Default: site locale.
	 *
	 * @return object|bool If there is an update, returns the license information.
	 *                     Otherwise returns false.
	 */
	private function get_plugin_info( $force_check = false, $locale = '' ) {
		$information = $this->get_default_plugin_info();
		$fetched     = $this->fetch_plugin_info( $force_check, $locale );
		if ( is_object( $fetched ) ) {
			$information = (object) array_merge( (array) $information, (array) $fetched );
		}

		return $information;
	}

	/**
	 * Get default plugin info.
	 *
	 * @return object
	 */
	private function get_default_plugin_info() {
		$description  = '<p><strong>' . __( 'Rank Math SEO PRO For WordPress', 'rank-math-pro' ) . '</strong><br />';
		$description .= '★★★★★</p>';
		$description .= '<p><strong>' . __( 'SEO is the most consistent source of traffic for any website', 'rank-math-pro' ) . '.</strong> ';
		// Translators: placeholders are the anchor tag opening and closing.
		$description .= sprintf( __( 'We created %1$sRank Math, a WordPress SEO plugin%2$s, to help every website owner get access to the SEO tools they need to improve their SEO and attract more traffic to their website.', 'rank-math-pro' ), '<a href="' . KB::get( 'logo', 'PRO Update Popup Description Tab' ) . '" rel="nofollow ugc"><strong>', '</strong></a>' ) . '</p>';

		$plugin_info = [
			'external' => true,
			'name'     => 'Rank Math SEO PRO',
			'slug'     => $this->slug,
			'author'   => '<a href="' . KB::get( 'seo-suite', 'PRO Update Popup Author Link' ) . '">Rank Math</a>',
			'homepage' => KB::get( 'seo-suite', 'PRO Update Popup Homepage Link' ),
			'banners'  => [
				'low'  => 'https://ps.w.org/seo-by-rank-math/assets/banner-772x250.png',
				'high' => 'https://ps.w.org/seo-by-rank-math/assets/banner-1544x500.png',
			],
			'sections' => [
				'description' => $description,
			],
			'version'  => RANK_MATH_PRO_VERSION,
		];

		return (object) $plugin_info;
	}

	/**
	 * Get default plugin info if we can't get anything from the API.
	 *
	 * @return object
	 */
	private function get_default_update_data() {
		$plugin = plugin_basename( RANK_MATH_PRO_FILE );

		$update = [
			'slug'        => $this->slug,
			'plugin'      => $plugin,
			'url'         => KB::get( 'seo-suite', 'PRO Update Popup Update Home Link' ),
			'icons'       => [
				'svg' => 'https://ps.w.org/seo-by-rank-math/assets/icon.svg?rev=2348086',
				'1x'  => 'https://ps.w.org/seo-by-rank-math/assets/icon-128x128.png',
				'2x'  => 'https://ps.w.org/seo-by-rank-math/assets/icon-256x256.png',
			],
			'new_version' => '',
			'package'     => '',
		];

		return (object) $update;
	}

	/**
	 * Checks the license manager to see if there is an update available for this product.
	 *
	 * @param bool $force_check Disregard cached data & always re-fetch.
	 * @return object|bool If there is an update, returns the license information.
	 *                     Otherwise returns false.
	 */
	public function fetch_latest_version( $force_check = false ) {

		$stored_versions = get_site_transient( 'rank_math_pro_versions' );
		if ( empty( $stored_versions ) || $force_check ) {
			// Check the latest versions.
			$versions_response = wp_remote_get(
				$this->api_url . '/versionCheck/',
				[
					'timeout' => defined( 'DOING_CRON' ) && DOING_CRON ? 30 : 10,
					'body'    => [
						'product_slug' => $this->slug,
					],
				]
			);

			if ( is_wp_error( $versions_response ) ) {
				return false;
			}

			$versions_response_body = wp_remote_retrieve_body( $versions_response );
			$versions_result        = json_decode( $versions_response_body, true );
			if ( ! is_array( $versions_result ) || ! isset( $versions_result['new_version'] ) ) {
				return false;
			}

			$stored_versions = $versions_result;

			set_site_transient( 'rank_math_pro_versions', $versions_result, 3600 * 3 );
			update_site_option( 'rank_math_pro_google_updates', $versions_result['g_updates'] );
		}

		$stored = get_site_transient( 'rank_math_pro_updates' );
		if ( ! $force_check && ! empty( $stored ) ) {
			return $stored;
		}

		if (
			version_compare( $stored_versions['new_version'], RANK_MATH_PRO_VERSION, '<=' )
			&& ! $this->new_beta_version_available( $stored_versions )
		) {
			return $stored;
		}

		if ( ! empty( $this->requested['version'] ) ) {
			return ( false === $stored ? [] : $stored );
		}

		$params = [
			'site_url'     => untrailingslashit( is_multisite() ? network_site_url() : home_url() ),
			'product_slug' => $this->slug,
			'new_api'      => 2,
			'v'            => RANK_MATH_PRO_VERSION,
		];

		$this->maybe_add_auth_params( $params );

		if ( ! isset( $params['username'] ) || Admin_Helper::is_plan_expired() ) {
			return false;
		}

		$response = wp_remote_post(
			add_query_arg( 'v', RANK_MATH_PRO_VERSION, $this->api_url . '/updateCheck2/' ),
			[
				'timeout' => defined( 'DOING_CRON' ) && DOING_CRON ? 30 : 10,
				'body'    => $params,
			]
		);

		$this->requested['version'] = true;

		if ( is_wp_error( $response ) ) {
			return false;
		}

		$response_body = wp_remote_retrieve_body( $response );
		$result        = json_decode( $response_body, true );
		if ( ! is_array( $result ) || ! isset( $result['new_version'] ) ) {
			return false;
		}

		set_site_transient( 'rank_math_pro_updates', $result, 3600 * 12 );

		return $result;
	}


	/**
	 * Checks the license manager to see if there is an update available for this product.
	 *
	 * @param bool   $force_check Disregard cached data & always re-fetch.
	 * @param string $locale      Requested locale. Optional. Default: site locale.
	 * @return object|bool If there is an update, returns the license information.
	 *                     Otherwise returns false.
	 */
	public function fetch_plugin_info( $force_check = false, $locale = '' ) {
		if ( ! $locale ) {
			$locale = get_locale();
		}

		$stored = get_site_transient( 'rank_math_pro_info_' . $locale );
		if ( ! $force_check && ! empty( $stored ) ) {
			return $stored;
		}

		if ( ! empty( $this->requested['info'] ) ) {
			return ( false === $stored ? (object) [] : $stored );
		}

		$params = [
			'product_slug' => $this->slug,
			'locale'       => $locale,
			'site_url'     => is_multisite() ? network_site_url() : home_url(),
		];

		$this->maybe_add_auth_params( $params );

		// Send the request.
		$response = wp_remote_post(
			$this->api_url . '/pluginInfo/',
			[
				'timeout' => defined( 'DOING_CRON' ) && DOING_CRON ? 30 : 10,
				'body'    => $params,
			]
		);

		$this->requested['info'] = true;

		if ( is_wp_error( $response ) ) {
			return false;
		}

		$response_body = wp_remote_retrieve_body( $response );

		// We do assoc=true and then cast to object to keep sub-items as array.
		$result = (object) json_decode( $response_body, true );

		if ( ! is_object( $result ) ) {
			return false;
		}

		set_site_transient( 'rank_math_pro_info_' . $locale, $result, 3600 * 3 );
		return $result;
	}

	/**
	 * Add username & api key if the site is connected.
	 *
	 * @param array $params Params passed by reference.
	 * @return void
	 */
	private function maybe_add_auth_params( &$params ) {
		$registered = Admin_Helper::get_registration_data();
		if ( $registered && isset( $registered['username'] ) && isset( $registered['api_key'] ) ) {
			$params['username'] = $registered['username'];
			$params['api_key']  = $registered['api_key'];
		}
	}

	/**
	 * Add additional text to notice if download is not available and account is connected.
	 *
	 * @param  array  $plugin_data An array of plugin metadata.
	 * @param  object $response    An array of metadata about the available plugin update.
	 * @return void
	 */
	public function in_plugin_update_message( $plugin_data, $response ) {
		if ( current_user_can( 'update_plugins' ) ) {
			if ( empty( $response->package ) && isset( $response->unavailability_reason ) ) {
				$message = $this->get_update_message( $response->unavailability_reason );
				echo ' <strong>' . wp_kses_post( $message ) . '</strong>';
			}
		}
	}

	/**
	 * Get unavailability reason message.
	 *
	 * @param string $reason  Unavailability reason ID, like 'not_connected'.
	 * @param mixed  $defaults Default text to return when specified ID has no message attached to it.
	 * @return string
	 */
	public function get_update_message( $reason = '', $defaults = null ) {
		if ( is_null( $defaults ) ) {
			$defaults = '';
		}

		$unavailability_reasons = [
			'update_free'    => __( 'Please update the free version before updating Rank Math SEO PRO.', 'rank-math-pro' ),
			'not_subscribed' => sprintf(
				/* translators: 1: Plugin name, 2: Pricing Link's opening HTML anchor tag, 3: Pricing Link's closing HTML anchor tag. */
				__( 'It seems that you don\'t have an active subscription for %1$s. Please see %2$sdetails and pricing%3$s.', 'rank-math-pro' ),
				'Rank Math SEO PRO',
				'<a href="' . KB::get( 'pro', 'PRO Update Popup Upgrade Notice' ) . '">',
				'</a>'
			),
			'not_connected'  => sprintf(
				/* translators: 1: Link's opening HTML anchor tag, 2: Link's closing HTML anchor tag. */
				__( 'Please %1$s connect Rank Math SEO PRO %2$s for automatic updates.', 'rank-math-pro' ),
				'<a href="' . Helper::get_connect_url() . '">',
				'</a>'
			),
		];

		$unavailability_reasons = $this->do_filter( 'updates/unavailability_reasons', $unavailability_reasons );

		if ( isset( $unavailability_reasons[ $reason ] ) ) {
			return $unavailability_reasons[ $reason ];
		}

		return $defaults;
	}

	/**
	 * Toggle auto updates option.
	 * Hooked to run when the option is changed in the free version.
	 *
	 * @param string $toggle New status.
	 * @return void
	 */
	public function toggle_auto_update( $toggle ) {
		$auto_updates = (array) get_site_option( 'auto_update_plugins', [] );
		if ( ! empty( $toggle ) && 'off' !== $toggle ) {
			$auto_updates[] = 'seo-by-rank-math-pro/rank-math-pro.php';
			update_site_option( 'auto_update_plugins', array_unique( $auto_updates ) );
			return;
		}

		update_site_option( 'auto_update_plugins', array_diff( $auto_updates, [ 'seo-by-rank-math-pro/rank-math-pro.php' ] ) );
	}

	/**
	 * Don't auto-update if it's a beta version.
	 *
	 * @param bool  $update Whether to update the plugin or not.
	 * @param array $item  The update plugin object.
	 *
	 * @return bool
	 */
	public function auto_update_plugin( $update, $item ) {
		// Show auto-updates control on Plugins page.
		if ( did_action( 'load-plugins.php' ) ) {
			return $update;
		}

		if ( $this->is_rm_pro_update( $item ) && $this->is_beta_update( $item ) ) {
			return false;
		}

		return $update;
	}

	/**
	 * Check if updatable object is RM.
	 *
	 * @param object $item Updatable object.
	 * @return boolean
	 */
	public function is_rm_pro_update( $item ) {
		return isset( $item->slug ) &&
			'seo-by-rank-math-pro' === $item->slug &&
			isset( $item->new_version );
	}

	/**
	 * Check if given version is beta.
	 *
	 * @param object $item Update data.
	 * @return boolean
	 */
	public function is_beta_update( $item ) {
		return ( is_object( $item ) && isset( $item->new_version ) && false !== stripos( $item->new_version, 'beta' ) );
	}

	/**
	 * Hide "update scheduled in X hours" message if update is a beta version because we don't auto-update those.
	 *
	 * @param string $html        HTML string.
	 * @param string $plugin_file Plugin file relative to the plugin directory.
	 * @param array  $plugin_data Plugin update data.
	 * @return string
	 */
	public function plugin_auto_update_setting_html( $html, $plugin_file, $plugin_data ) {
		if ( 'seo-by-rank-math-pro/rank-math-pro.php' !== $plugin_file ) {
			return $html;
		}

		if ( ! empty( $plugin_data['is_beta'] ) ) {
			$html = str_replace( 'class="auto-update-time"', 'class="auto-update-time hidden"', $html );
		}

		return $html;
	}

	/**
	 * Set should_send to true if Pro update is available, to send the notification email.
	 * Notification feature is only available if auto-updates are disabled.
	 *
	 * @param boolean $should_send Original should_send value.
	 * @param mixed   $transient   update_plugins site ransient value.
	 * @return boolean
	 */
	public function should_send_update_notification( $should_send, $transient ) {
		if ( $this->get_auto_update_setting() ) {
			return $should_send;
		}

		if ( ! $this->is_update_available( $transient ) ) {
			return $should_send;
		}

		$new_version = $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->new_version;

		// Now let's check if we've already sent this email.
		$sent = get_option( 'rank_math_update_notifications_sent', [ 'pro' => [ 'new_version' => '1.0' ] ] );
		if ( ! isset( $sent['pro'] ) ) {
			$sent['pro'] = [ 'new_version' => '1.0' ];
		}

		if ( version_compare( $sent['pro']['new_version'], $new_version, '>=' ) ) {
			return $should_send;
		}

		return true;
	}

	/**
	 * Add Pro to notification product list if an update is available.
	 *
	 * @param array $products  Products data array.
	 * @param mixed $transient Transient value.
	 * @return array
	 */
	public function update_notification_products( $products, $transient ) {
		if ( $this->get_auto_update_setting() ) {
			return $products;
		}

		if ( ! $this->is_update_available( $transient ) ) {
			return $products;
		}

		$products['pro'] = [
			'name'        => __( 'Rank Math PRO', 'rank-math-pro' ),
			'old_version' => RANK_MATH_PRO_VERSION,
			'new_version' => $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->new_version,
			'changelog'   => KB::get( 'changelog', 'PRO Update Popup Chagelog Link' ),
		];

		return $products;
	}

	/**
	 * Check if update is available for the PRO version.
	 *
	 * @param  mixed $transient The update_plugins transient value.
	 * @return boolean
	 */
	public function is_update_available( $transient ) {
		if ( ! is_object( $transient )
			|| empty( $transient->response )
			|| empty( $transient->response['seo-by-rank-math-pro/rank-math-pro.php'] )
			|| empty( $transient->response['seo-by-rank-math-pro/rank-math-pro.php']->new_version )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Get Auto update setting status.
	 *
	 * @return bool
	 */
	public function get_auto_update_setting() {
		return in_array( 'seo-by-rank-math-pro/rank-math-pro.php', (array) get_site_option( 'auto_update_plugins', [] ), true );
	}

	/**
	 * Connect auto-update toggles: if we enable it for the Pro, then Free should be enabled too,
	 * and if we disable it for Free then it should be disabled for the Pro too.
	 *
	 * @param string $option     Option name.
	 * @param mixed  $value      Option value.
	 * @param mixed  $old_value  Previous option value before the change.
	 * @return void
	 */
	public function connect_auto_update_toggles( $option, $value, $old_value ) {
		$this->remove_action( 'update_site_option_auto_update_plugins', 'connect_auto_update_toggles', 20 );
		if ( ! is_array( $value ) ) {
			return;
		}

		$free_file = 'seo-by-rank-math/rank-math.php';
		$pro_file  = 'seo-by-rank-math-pro/rank-math-pro.php';

		// If we just enabled it for Rank Math SEO Pro.
		if ( in_array( $pro_file, $value, true ) && ! in_array( $pro_file, $old_value, true ) && ! in_array( $free_file, $value, true ) ) {
			$value[] = $free_file;
			update_site_option( $option, $value );
			return;
		}

		// If we just disabled it for Rank Math SEO Free.
		if ( ! in_array( $free_file, $value, true ) && in_array( $free_file, $old_value, true ) && in_array( $pro_file, $value, true ) ) {
			$value = array_diff( $value, [ $pro_file ] );
			update_site_option( $option, $value );
			return;
		}
	}
}
