<?php
/**
 * Represents a request to generate a pair of speed scores.
 *
 * @package automattic/jetpack-boost-speed-score
 */

namespace Automattic\Jetpack\Boost_Speed_Score;

use Automattic\Jetpack\Boost_Core\Lib\Boost_API;
use Automattic\Jetpack\Boost_Core\Lib\Cacheable;
use Automattic\Jetpack\Boost_Core\Lib\Url;

/**
 * Class Speed_Score_Request
 */
class Speed_Score_Request extends Cacheable {
	/**
	 * Algorithm to use when defining a hash for the cache.
	 */
	const CACHE_KEY_HASH_ALGO = 'md5';

	/**
	 * The URL to get the Speed Scores for.
	 *
	 * @var string $url Url to get the Speed Scores for.
	 */
	private $url;

	/**
	 * Active Jetpack Boost modules.
	 *
	 * @var array $active_modules Active modules.
	 */
	private $active_modules;

	/**
	 * When the Speed Scores request was created, in seconds since epoch.
	 *
	 * @var float $created Speed Scores request creation timestamp.
	 */
	private $created;

	/**
	 * Current status of the Speed Score request.
	 *
	 * @var string $status Speed Scores request status.
	 */
	private $status;

	/**
	 * Number of retries attempted.
	 *
	 * @var int $retry_count Number of times this Speed Score request has been retried.
	 */
	private $retry_count;

	/**
	 * The error returned
	 *
	 * @var array $error Speed Scores error.
	 */
	private $error;

	/**
	 * Where the Speed Scores request was made from.
	 *
	 * @var string $client A string passed to Speed_Score to identify where the request was made from.
	 */
	private $client;

	/**
	 * Constructor.
	 *
	 * @param string $url The URL to get the Speed Scores for.
	 * @param array  $active_modules Active modules.
	 * @param null   $created When the Speed Scores request was created, in seconds since epoch.
	 * @param string $status Status of the Speed Scores request.
	 * @param null   $error The Speed Scores error.
	 * @param string $client A string identifying where the request was made from.
	 */
	public function __construct( $url, $active_modules = array(), $created = null, $status = 'pending', $error = null, $client = null ) {
		$this->set_cache_id( self::generate_cache_id_from_url( $url ) );

		$this->url            = $url;
		$this->active_modules = $active_modules;
		$this->created        = $created === null ? microtime( true ) : $created;
		$this->status         = $status;
		$this->error          = $error;
		$this->client         = $client;
		$this->retry_count    = 0;
	}

	/**
	 * Generate the cache ID from the URL.
	 *
	 * @param string $url The URL to get the Speed Scores for.
	 *
	 * @return string
	 */
	public static function generate_cache_id_from_url( $url ) {
		return hash( self::CACHE_KEY_HASH_ALGO, $url );
	}

	/**
	 * Get the list of active performance modules while this request was created.
	 *
	 * @return string
	 */
	public function get_active_performance_modules() {

		// List of modules that affect the speed score.
		$performance_modules = array(
			'cloud_css',
			'critical_css',
			'image_cdn',
			'minify_css',
			'minify_js',
			'render_blocking_js',
		);

		return array_intersect( $this->active_modules, $performance_modules );
	}

	/**
	 * Convert this object to a plain array for JSON serialization.
	 */
	#[\ReturnTypeWillChange]
	public function jsonSerialize() {
		return array(
			'id'             => $this->get_cache_id(),
			'url'            => $this->url,
			'active_modules' => $this->get_active_performance_modules(),
			'created'        => $this->created,
			'status'         => $this->status,
			'error'          => $this->error,
			'client'         => $this->client,
			'retry_count'    => $this->retry_count,
		);
	}

	/**
	 * This is intended to be the reverse of JsonSerializable->jsonSerialize.
	 *
	 * @param mixed $data The data to turn into an object.
	 *
	 * @return Speed_Score_Request
	 */
	public static function jsonUnserialize( $data ) {
		$object = new Speed_Score_Request(
			$data['url'],
			$data['active_modules'],
			$data['created'],
			$data['status'],
			$data['error'],
			$data['client']
		);

		if ( ! empty( $data['id'] ) ) {
			$object->set_cache_id( $data['id'] );
		}

		if ( ! empty( $data['retry_count'] ) ) {
			$object->retry_count = intval( $data['retry_count'] );
		}

		return $object;
	}

	/**
	 * Return the cache prefix.
	 *
	 * @return string
	 */
	protected static function cache_prefix() {
		return 'jetpack_boost_speed_scores_';
	}

	/**
	 * Send a Speed Scores request to the API.
	 *
	 * @return true|\WP_Error True on success, WP_Error on failure.
	 */
	public function execute() {
		$response = Boost_API::post(
			'speed-scores',
			array(
				'request_id'     => $this->get_cache_id(),
				'url'            => Url::normalize( $this->url ),
				'active_modules' => $this->active_modules,
				'client'         => $this->client,
			)
		);

		if ( is_wp_error( $response ) ) {
			$this->status = 'error';
			$this->error  = $response->get_error_message();
			$this->store();

			return $response;
		}

		return true;
	}

	/**
	 * Is this request pending?
	 */
	public function is_pending() {
		return 'pending' === $this->status;
	}

	/**
	 * Did the request fail?
	 */
	public function is_error() {
		return 'error' === $this->status;
	}

	/**
	 * Did the request succeed?
	 */
	public function is_success() {
		return 'success' === $this->status;
	}

	/**
	 * Poll for updates to this Speed Scores request.
	 *
	 * @return true|\WP_Error True on success, WP_Error on failure.
	 */
	public function poll_update() {
		$response = Boost_API::get(
			sprintf(
				'speed-scores/%s',
				$this->get_cache_id()
			)
		);

		if ( is_wp_error( $response ) ) {
			// Special case: If the request is not found, restart it.
			if ( 'not_found' === $response->get_error_code() ) {
				return $this->restart();
			}

			return $response;
		}

		switch ( $response['status'] ) {
			case 'pending':
				// The initial job probably failed, dispatch again if so.
				if ( $this->created <= strtotime( '-15 mins' ) ) {
					return $this->restart();
				}
				break;

			case 'error':
				$this->status = 'error';
				$this->error  = $response['error'];
				$this->store();
				break;

			case 'success':
				$this->status = 'success';
				$this->store();
				$this->record_history( $response );

				break;

			default:
				return new \WP_Error(
					'invalid_response',
					__(
						'Invalid response from WPCOM API while polling for speed scores',
						'jetpack-boost-speed-score'
					),
					$response
				);
		}

		return true;
	}

	/**
	 * Restart this request; useful when WPCOM doesn't recognize the request or it times out.
	 */
	private function restart() {
		// Enforce a maximum number of restarts.
		if ( $this->retry_count > 2 ) {
			$this->status = 'error';
			$this->error  = 'Maximum number of retries exceeded';
			$this->store();

			return new \WP_Error(
				'error',
				$this->error
			);
		}

		$result = $this->execute();
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$this->created = time();
		++$this->retry_count;
		$this->store();

		return true;
	}

	/**
	 * Save the speed score record to history.
	 *
	 * @param array $response Response from api.
	 */
	private function record_history( $response ) {
		$history       = new Speed_Score_History( $this->url );
		$last_history  = $history->latest();
		$last_scores   = $last_history ? $last_history['scores'] : null;
		$last_theme    = $last_history ? $last_history['theme'] : null;
		$current_theme = wp_get_theme()->get( 'Name' );

		// Only change if there is a difference from last score or the theme changed.
		if ( $last_scores !== $response['scores'] || $current_theme !== $last_theme ) {
			$history->push(
				array(
					'timestamp' => time(),
					'scores'    => $response['scores'],
					'theme'     => $current_theme,
				)
			);
		}
	}
}
