<?php
/**
 * Class for the Jetpack partner coupon logic.
 *
 * @package automattic/jetpack-connection
 */

namespace Automattic\Jetpack;

use Automattic\Jetpack\Connection\Client as Connection_Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack_Options;

/**
 * Disable direct access.
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit( 0 );
}

/**
 * Class Jetpack_Partner_Coupon
 *
 * @since partner-1.6.0
 * @since 2.0.0
 */
class Partner_Coupon {

	/**
	 * Name of the Jetpack_Option coupon option.
	 *
	 * @var string
	 */
	public static $coupon_option = 'partner_coupon';

	/**
	 * Name of the Jetpack_Option added option.
	 *
	 * @var string
	 */
	public static $added_option = 'partner_coupon_added';

	/**
	 * Name of "last availability check" transient.
	 *
	 * @var string
	 */
	public static $last_check_transient = 'jetpack_partner_coupon_last_check';

	/**
	 * Callable that executes a blog-authenticated request.
	 *
	 * @var callable
	 */
	protected $request_as_blog;

	/**
	 * Jetpack_Partner_Coupon
	 *
	 * @var Partner_Coupon|null
	 **/
	private static $instance = null;

	/**
	 * A list of supported partners.
	 *
	 * @var array
	 */
	private static $supported_partners = array(
		'IONOS' => array(
			'name' => 'IONOS',
			'logo' => array(
				'src'    => '/images/ionos-logo.jpg',
				'width'  => 119,
				'height' => 32,
			),
		),
	);

	/**
	 * A list of supported presets.
	 *
	 * @var array
	 */
	private static $supported_presets = array(
		'IONA' => 'jetpack_backup_daily',
	);

	/**
	 * Get singleton instance of class.
	 *
	 * @return Partner_Coupon
	 */
	public static function get_instance() {
		if ( self::$instance === null ) {
			self::$instance = new Partner_Coupon( array( Connection_Client::class, 'wpcom_json_api_request_as_blog' ) );
		}

		return self::$instance;
	}

	/**
	 * Constructor.
	 *
	 * @param callable $request_as_blog Callable that executes a blog-authenticated request.
	 */
	public function __construct( $request_as_blog ) {
		$this->request_as_blog = $request_as_blog;
	}

	/**
	 * Register hooks to catch and purge coupon.
	 *
	 * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
	 * @param string $redirect_location The location we should redirect to after catching the coupon.
	 */
	public static function register_coupon_admin_hooks( $plugin_slug, $redirect_location ) {
		$instance = self::get_instance();

		// We have to use an anonymous function, so we can pass along relevant information
		// and not have to hardcode values for a single plugin.
		// This open up the opportunity for e.g. the "all-in-one" and backup plugins
		// to both implement partner coupon logic.
		add_action(
			'admin_init',
			function () use ( $plugin_slug, $redirect_location, $instance ) {
				$instance->catch_coupon( $plugin_slug, $redirect_location );
				$instance->maybe_purge_coupon( $plugin_slug );
			}
		);
	}

	/**
	 * Catch partner coupon and redirect to claim component.
	 *
	 * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
	 * @param string $redirect_location The location we should redirect to after catching the coupon.
	 */
	public function catch_coupon( $plugin_slug, $redirect_location ) {
		// Accept and store a partner coupon if present, and redirect to Jetpack connection screen.
		$partner_coupon = isset( $_GET['jetpack-partner-coupon'] ) ? sanitize_text_field( wp_unslash( $_GET['jetpack-partner-coupon'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( $partner_coupon ) {
			Jetpack_Options::update_options(
				array(
					self::$coupon_option => $partner_coupon,
					self::$added_option  => time(),
				)
			);

			$connection = new Connection_Manager( $plugin_slug );
			if ( $connection->is_connected() ) {
				$redirect_location = add_query_arg( array( 'showCouponRedemption' => 1 ), $redirect_location );
				wp_safe_redirect( $redirect_location );
			} else {
				wp_safe_redirect( $redirect_location );
			}
		}
	}

	/**
	 * Purge partner coupon.
	 *
	 * We try to remotely check if a coupon looks valid. We also automatically purge
	 * partner coupons after a certain amount of time to prevent unnecessary look-ups
	 * and/or promoting a product for months or years in the future due to unknown
	 * errors.
	 *
	 * @param string $plugin_slug The plugin slug to differentiate between Jetpack connections.
	 */
	public function maybe_purge_coupon( $plugin_slug ) {
		// Only run coupon checks on Jetpack admin pages.
		// The "admin-ui" package is responsible for registering the Jetpack admin
		// page for all Jetpack plugins and has hardcoded the settings page to be
		// "jetpack", so we shouldn't need to allow for dynamic/custom values.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['page'] ) || 'jetpack' !== $_GET['page'] ) {
			return;
		}

		if ( ( new Status() )->is_offline_mode() ) {
			return;
		}

		$connection = new Connection_Manager( $plugin_slug );
		if ( ! $connection->is_connected() ) {
			return;
		}

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

		// Limit checks to happen once a minute at most.
		if ( get_transient( self::$last_check_transient ) ) {
			return;
		}

		set_transient( self::$last_check_transient, true, MINUTE_IN_SECONDS );

		$this->maybe_purge_coupon_by_availability_check();
	}

	/**
	 * Purge coupon based on local added date.
	 *
	 * We automatically remove the coupon after a month to "self-heal" if
	 * something in the claim process has broken with the site.
	 *
	 * @return bool Return whether we should skip further purge checks.
	 */
	protected function maybe_purge_coupon_by_added_date() {
		$date = Jetpack_Options::get_option( self::$added_option, '' );

		if ( empty( $date ) ) {
			return true;
		}

		$expire_date = strtotime( '+30 days', $date );
		$today       = time();

		if ( $today >= $expire_date ) {
			$this->delete_coupon_data();

			return true;
		}

		return false;
	}

	/**
	 * Purge coupon based on availability check.
	 *
	 * @return bool Return whether we deleted coupon data.
	 */
	protected function maybe_purge_coupon_by_availability_check() {
		$blog_id = Jetpack_Options::get_option( 'id', false );

		if ( ! $blog_id ) {
			return false;
		}

		$coupon = self::get_coupon();

		if ( ! $coupon ) {
			return false;
		}

		$response = call_user_func_array(
			$this->request_as_blog,
			array(
				add_query_arg(
					array( 'coupon_code' => $coupon['coupon_code'] ),
					sprintf(
						'/sites/%d/jetpack-partner/coupon/v1/site/coupon',
						$blog_id
					)
				),
				2,
				array( 'method' => 'GET' ),
				null,
				'wpcom',
			)
		);

		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if (
			200 === wp_remote_retrieve_response_code( $response ) &&
			is_array( $body ) &&
			isset( $body['available'] ) &&
			false === $body['available']
		) {
			$this->delete_coupon_data();

			return true;
		}

		return false;
	}

	/**
	 * Delete all coupon data.
	 */
	protected function delete_coupon_data() {
		Jetpack_Options::delete_option(
			array(
				self::$coupon_option,
				self::$added_option,
			)
		);
	}

	/**
	 * Get partner coupon data.
	 *
	 * @return array|bool
	 */
	public static function get_coupon() {
		$coupon_code = Jetpack_Options::get_option( self::$coupon_option, '' );

		if ( ! is_string( $coupon_code ) || empty( $coupon_code ) ) {
			return false;
		}

		$instance = self::get_instance();
		$partner  = $instance->get_coupon_partner( $coupon_code );

		if ( ! $partner ) {
			return false;
		}

		$preset = $instance->get_coupon_preset( $coupon_code );

		if ( ! $preset ) {
			return false;
		}

		$product = $instance->get_coupon_product( $preset );

		if ( ! $product ) {
			return false;
		}

		return array(
			'coupon_code' => $coupon_code,
			'partner'     => $partner,
			'preset'      => $preset,
			'product'     => $product,
		);
	}

	/**
	 * Get coupon partner.
	 *
	 * @param string $coupon_code Coupon code to go through.
	 * @return array|bool
	 */
	private function get_coupon_partner( $coupon_code ) {
		if ( ! is_string( $coupon_code ) || false === strpos( $coupon_code, '_' ) ) {
			return false;
		}

		$prefix             = strtok( $coupon_code, '_' );
		$supported_partners = $this->get_supported_partners();

		if ( ! isset( $supported_partners[ $prefix ] ) ) {
			return false;
		}

		return array(
			'name'   => $supported_partners[ $prefix ]['name'],
			'prefix' => $prefix,
			'logo'   => isset( $supported_partners[ $prefix ]['logo'] ) ? $supported_partners[ $prefix ]['logo'] : null,
		);
	}

	/**
	 * Get coupon product.
	 *
	 * @param string $coupon_preset The preset we wish to find a product for.
	 * @return array|bool
	 */
	private function get_coupon_product( $coupon_preset ) {
		if ( ! is_string( $coupon_preset ) ) {
			return false;
		}

		/**
		 * Allow for plugins to register supported products.
		 *
		 * @since 1.6.0
		 *
		 * @param array A list of product details.
		 * @return array
		 */
		$product_details = apply_filters( 'jetpack_partner_coupon_products', array() );
		$product_slug    = $this->get_supported_presets()[ $coupon_preset ];

		foreach ( $product_details as $product ) {
			if ( ! $this->array_keys_exist( array( 'title', 'slug', 'description', 'features' ), $product ) ) {
				continue;
			}

			if ( $product_slug === $product['slug'] ) {
				return $product;
			}
		}

		return false;
	}

	/**
	 * Checks if multiple keys are present in an array.
	 *
	 * @param array $needles The keys we wish to check for.
	 * @param array $haystack The array we want to compare keys against.
	 *
	 * @return bool
	 */
	private function array_keys_exist( $needles, $haystack ) {
		foreach ( $needles as $needle ) {
			if ( ! isset( $haystack[ $needle ] ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Get coupon preset.
	 *
	 * @param string $coupon_code Coupon code to go through.
	 * @return string|bool
	 */
	private function get_coupon_preset( $coupon_code ) {
		if ( ! is_string( $coupon_code ) ) {
			return false;
		}

		$regex   = '/^.*?_(?P<slug>.*?)_.+$/';
		$matches = array();

		if ( ! preg_match( $regex, $coupon_code, $matches ) ) {
			return false;
		}

		return isset( $this->get_supported_presets()[ $matches['slug'] ] ) ? $matches['slug'] : false;
	}

	/**
	 * Get supported partners.
	 *
	 * @return array
	 */
	private function get_supported_partners() {
		/**
		 * Allow external code to add additional supported partners.
		 *
		 * @since partner-1.6.0
		 * @since 2.0.0
		 *
		 * @param array $supported_partners A list of supported partners.
		 * @return array
		 */
		return apply_filters( 'jetpack_partner_coupon_supported_partners', self::$supported_partners );
	}

	/**
	 * Get supported presets.
	 *
	 * @return array
	 */
	private function get_supported_presets() {
		/**
		 * Allow external code to add additional supported presets.
		 *
		 * @since partner-1.6.0
		 * @since 2.0.0
		 *
		 * @param array $supported_presets A list of supported presets.
		 * @return array
		 */
		return apply_filters( 'jetpack_partner_coupon_supported_presets', self::$supported_presets );
	}
}
