<?php

namespace WP_Rocket\Engine\Common\JobManager\APIHandler;

use WP_Error;

/**
 * Class AbstractSafeAPIClient
 *
 * This abstract class provides a base for making API requests.
 * It includes methods for sending GET and POST requests, and handles transient caching to mitigate the impact of API failures.
 *
 * @package WP_Rocket\Engine\Common\JobManager\APIHandler
 */
abstract class AbstractSafeAPIClient {

	/**
	 * Get the transient key.
	 *
	 * @return string The transient key.
	 */
	abstract protected function get_transient_key();

	/**
	 * Get the API URL.
	 *
	 * @return string The API URL.
	 */
	abstract protected function get_api_url();

	/**
	 * Send a GET request.
	 *
	 * @param array $params The request parameters.
	 * @param bool  $safe Send safe request WP functions or not, default to not.
	 *
	 * @return mixed The response from the API.
	 */
	public function send_get_request( $params = [], $safe = false ) {
		return $this->send_request( 'GET', $params, $safe );
	}

	/**
	 * Send a POST request.
	 *
	 * @param array $params The request parameters.
	 * @param bool  $safe Send safe request WP functions or not, default to not.
	 *
	 * @return mixed The response from the API.
	 */
	public function send_post_request( $params = [], $safe = false ) {
		return $this->send_request( 'POST', $params, $safe );
	}

	/**
	 * Send a request to the API.
	 *
	 * @param string $method The HTTP method (GET or POST).
	 * @param array  $params The request parameters.
	 * @param bool   $safe Send safe request WP functions or not, default to not.
	 * @return mixed The response from the API, or WP_Error if a timeout is active.
	 */
	private function send_request( $method, $params = [], $safe = false ) {
		$api_url = $this->get_api_url();
		if ( empty( $api_url ) ) {
			return new WP_Error( 404, 'Empty Url.' );
		}

		$transient_key = $this->get_transient_key();
		if ( get_transient( $transient_key . '_timeout_active' ) ) {
			return new WP_Error( 429, 'Too many requests.' );
		}
		// Get previous_expiration early to avoid multiple parallel requests increasing the expiration multiple times.
		$previous_expiration = (int) get_transient( $transient_key . '_timeout' );

		$params['method'] = strtoupper( $method );

		$response = $this->send_remote_request( $api_url, $method, $params, $safe );

		if ( is_wp_error( $response ) ) {
			$this->set_timeout_transients( $previous_expiration );
			return $response;
		}

		if ( ! $this->valid_response_code( $response ) ) {
			$this->set_timeout_transients( $previous_expiration );
			return new WP_Error( 500, 'Not valid response code.' );
		}

		if ( ! $this->valid_response_body( $response ) ) {
			$this->set_timeout_transients( $previous_expiration );
			return new WP_Error( 500, 'Not valid response body.' );
		}

		$this->delete_timeout_transients();

		return $response;
	}

	/**
	 * Set the timeout transients.
	 *
	 * @param string $previous_expiration The previous value of _timeout_active transient.
	 */
	private function set_timeout_transients( $previous_expiration ) {
		$transient_key = $this->get_transient_key();

		$expiration = ( 0 === (int) $previous_expiration )
			? 300
			: ( 2 * (int) $previous_expiration <= DAY_IN_SECONDS
				? 2 * (int) $previous_expiration
				: DAY_IN_SECONDS
			);

		set_transient( $transient_key . '_timeout', $expiration, WEEK_IN_SECONDS );
		set_transient( $transient_key . '_timeout_active', true, $expiration );
	}

	/**
	 * Delete the timeout transients.
	 *
	 * This method deletes the timeout transients for the API requests. It uses the transient key obtained from the `get_transient_key` method.
	 * The transients deleted are:
	 * - `{transient_key}_timeout_active`: This transient indicates if a timeout is currently active.
	 * - `{transient_key}_timeout`: This transient stores the timeout duration.
	 *
	 * @return void
	 */
	private function delete_timeout_transients() {
		$transient_key = $this->get_transient_key();
		delete_transient( $transient_key . '_timeout_active' );
		delete_transient( $transient_key . '_timeout' );
	}

	/**
	 * Decide which WP core function will be used to send the request based on the params.
	 *
	 * @param string $api_url API Url.
	 * @param string $method Request method (GET or POST).
	 * @param array  $params Parameters being sent with the request.
	 * @param bool   $safe Send safe request WP functions or not, default to not.
	 * @return array|WP_Error
	 */
	private function send_remote_request( $api_url, $method, $params, $safe ) {
		if ( ! $safe ) {
			return wp_remote_request( $api_url, $params );
		}

		unset( $params['method'] );

		switch ( $method ) {
			case 'GET':
				return wp_safe_remote_get( $api_url, $params );
			case 'POST':
				return wp_safe_remote_post( $api_url, $params );
		}

		return new WP_Error( 400, 'Not valid request type.' );
	}

	/**
	 * Validate response code.
	 *
	 * @param array $response Response object.
	 * @return bool
	 */
	protected function valid_response_code( $response ) {
		return empty( $response['response']['code'] ) || in_array( $response['response']['code'], [ 200, 202 ], true );
	}

	/**
	 * Validate response body.
	 *
	 * @param array $response Response object.
	 * @return bool
	 */
	protected function valid_response_body( $response ) {
		return ! empty( wp_remote_retrieve_body( $response ) );
	}
}
