<?php

namespace LearnPress\Background;

use WP_Error;

/**
 * Abstract LP_Async_Request class.
 *
 * This class is used to create asynchronous requests in LearnPress.
 * It is recommended to use this class for tasks that can be processed in the background
 * It is non-blocking, therefore it does not return data when done.
 * Note: don't call too many times on a progress, it can cause server hang.
 * Should be considered when using
 *
 * @since 4.2.8.7 instead of LP_Async_Request since 4.1.6.9.4, only change class name to autoload
 * @version 1.0.0
 */
abstract class LPAsyncRequest {
	/**
	 * Prefix
	 * @var string
	 */
	protected $prefix = 'lp';

	/**
	 * Action
	 * @var string
	 */
	protected $action = 'async_request';

	/**
	 * Identifier
	 *
	 * @var string
	 */
	protected $identifier;

	/**
	 * Constant identifier for a task that should be available to logged-in users
	 */
	const LOGGED_IN = 1;

	/**
	 * Constant identifier for a task that should be available to logged-out users
	 */
	const LOGGED_OUT = 2;

	/**
	 * Constant identifier for a task that should be available to all users regardless of auth status
	 */
	const BOTH = 3;

	/**
	 * Data
	 *
	 * (default value: array())
	 *
	 * @var array
	 */
	protected $data = array();

	/**
	 * Initiate new async request
	 */
	public function __construct( $auth_level = self::BOTH ) {
		$this->identifier = $this->prefix . '_' . $this->action;

		//add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) );
		//add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) );

		if ( $auth_level & self::LOGGED_IN ) {
			add_action( "admin_post_lp_async_$this->identifier", [ $this, 'maybe_handle' ] );
		}
		if ( $auth_level & self::LOGGED_OUT ) {
			add_action( "admin_post_nopriv_lp_async_$this->identifier", [ $this, 'maybe_handle' ] );
		}
	}

	/**
	 * Set data used during the request
	 *
	 * @param array $data Data.
	 *
	 * @return $this
	 */
	public function data( array $data ): LPAsyncRequest {
		$this->data = $data;

		return $this;
	}

	/**
	 * Dispatch the async request
	 *
	 * @return array|WP_Error
	 */
	public function dispatch() {
		$url  = esc_url_raw( $this->get_query_url() );
		$args = $this->get_post_args();

		return wp_remote_post( $url, $args );
	}

	/**
	 * Get query URL
	 *
	 * @return string
	 */
	protected function get_query_url(): string {
		if ( property_exists( $this, 'query_url' ) ) {
			return $this->query_url;
		}

		$url = admin_url( 'admin-post.php' );
		return apply_filters( $this->identifier . '/query_url', $url );
	}

	/**
	 * Get post args
	 *
	 * @return array
	 */
	protected function get_post_args(): array {
		$identifier           = $this->identifier;
		$this->data['action'] = "lp_async_{$identifier}";
		$this->data['_nonce'] = $this->create_async_nonce();

		/**
		 * Must set timeout to 0.01 to avoid blocking the request.
		 * Don't change it, because it can make sever hang.
		 */
		$args = array(
			'timeout'   => 0.01,
			'blocking'  => false,
			'body'      => $this->data,
			'cookies'   => $_COOKIE,
			'sslverify' => is_ssl(),
		);

		/**
		 * Filters the post arguments used during an async request.
		 *
		 * @param array $args
		 */
		return apply_filters( $this->identifier . '_post_args', $args );
	}

	/**
	 * Create nonce for async request
	 *
	 * @return false|string
	 */
	protected function create_async_nonce() {
		$action = $this->get_nonce_action();
		$i      = wp_nonce_tick();

		return substr( wp_hash( $i . $action . get_class( $this ), 'nonce' ), - 12, 10 );
	}

	/**
	 * Verify that the correct nonce was used within the time limit.
	 *
	 * @param string $nonce
	 *
	 * @return bool
	 */
	protected function verify_async_nonce( string $nonce ): bool {
		$action = $this->get_nonce_action();
		$i      = wp_nonce_tick();

		// Nonce generated 0-12 hours ago
		if ( substr( wp_hash( $i . $action . get_class( $this ), 'nonce' ), - 12, 10 ) == $nonce ) {
			return 1;
		}

		// Nonce generated 12-24 hours ago
		if ( substr( wp_hash( ( $i - 1 ) . $action . get_class( $this ), 'nonce' ), - 12, 10 ) == $nonce ) {
			return 2;
		}

		// Invalid nonce
		return false;
	}

	/**
	 * Get a nonce action based on the $action property of the class
	 *
	 * @return string The nonce action for the current instance
	 */
	protected function get_nonce_action(): string {
		$action = $this->identifier;
		if ( substr( $action, 0, 7 ) === 'nopriv_' ) {
			$action = substr( $action, 7 );
		}

		return "lp_async_$action";
	}

	/**
	 * Maybe handle
	 *
	 * Check for correct nonce and pass to handler.
	 */
	public function maybe_handle() {
		// Don't lock up other requests while processing
		session_write_close();

		/**
		 * set params $_POST['lp_no_check_referer'] = 1
		 * for case: send request when user not login, but get request when user logged
		 * @editor tungnx
		 * @modify 4.1.4
		 */
		/*if ( ! isset( $_POST['lp_no_check_referer'] ) ) {
			check_ajax_referer( $this->identifier, 'nonce' );
		}*/

		if ( isset( $_POST['_nonce'] ) && $this->verify_async_nonce( $_POST['_nonce'] ) ) {
			if ( ! is_user_logged_in() ) {
				$this->identifier = "nopriv_$this->identifier";
			}

			$this->handle();
		}

		wp_die();
	}

	/**
	 * Handle
	 *
	 * Override this method to perform any actions required
	 * during the async request.
	 */
	abstract protected function handle();
}
