<?php
/**
 * IndexNow API
 *
 * @since      1.0.56
 * @package    RankMath
 * @author     Rank Math <support@rankmath.com>
 */

namespace RankMath\Instant_Indexing;

use RankMath\Helper;
use RankMath\Traits\Hooker;

defined( 'ABSPATH' ) || exit;

/**
 * API class.
 *
 * @codeCoverageIgnore
 */
class Api {

	use Hooker;

	/**
	 * IndexNow API URL.
	 *
	 * @var string
	 */
	private $api_url = 'https://api.indexnow.org/indexnow/';

	/**
	 * IndexNow API key.
	 *
	 * @var string
	 */
	protected $api_key = '';

	/**
	 * Was the last request successful.
	 *
	 * @var bool
	 */
	protected $is_success = false;

	/**
	 * Last error.
	 *
	 * @var string
	 */
	protected $last_error = '';

	/**
	 * Last response.
	 *
	 * @var array
	 */
	protected $last_response = '';

	/**
	 * Last response header code.
	 *
	 * @var int
	 */
	protected $last_code = 0;

	/**
	 * Next submission is a manual submission.
	 *
	 * @var bool
	 */
	public $is_manual = true;

	/**
	 * User agent used for the API requests.
	 *
	 * @var string
	 */
	protected $user_agent = '';

	/**
	 * User agent used for the API requests.
	 *
	 * @var string
	 */
	protected $version = '';

	/**
	 * Main instance
	 *
	 * Ensure only one instance is loaded or can be loaded.
	 *
	 * @return Api
	 */
	public static function get() {
		static $instance;

		if ( is_null( $instance ) && ! ( $instance instanceof Api ) ) {
			$instance             = new Api();
			$instance->user_agent = 'RankMath/' . md5( esc_url( home_url( '/' ) ) );
			$instance->version    = rank_math()->version;
		}

		return $instance;
	}

	/**
	 * Make request to the IndexNow API.
	 *
	 * @param array $urls URLs to submit.
	 * @param bool  $manual Whether the request is manual or not.
	 *
	 * @return bool
	 */
	public function submit( $urls, $manual = null ) {
		$this->reset();

		if ( ! is_null( $manual ) ) {
			$this->is_manual = (bool) $manual;
		}

		$data = $this->get_payload( $urls );

		$response = wp_remote_post(
			'https://api.indexnow.org/indexnow/',
			[
				'body'    => $data,
				'headers' => [
					'Content-Type'  => 'application/json',
					'User-Agent'    => $this->user_agent,
					'X-Source-Info' => 'https://rankmath.com/' . $this->version . '/' . ( $this->is_manual ? '1' : '' ),
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			$this->last_error = 'WP_Error: ' . $response->get_error_message();
			$this->log( (array) $urls, 0, $this->last_error );
			return false;
		}

		$this->last_code     = wp_remote_retrieve_response_code( $response );
		$this->last_response = wp_remote_retrieve_body( $response );
		if ( in_array( $this->last_code, [ 200, 202, 204 ], true ) ) {
			$this->is_success = true;
			$this->log( (array) $urls, $this->last_code, 'OK' );
			return true;
		}

		$message = wp_remote_retrieve_response_message( $response );
		$this->set_error_message( $message );

		$this->log( (array) $urls, $this->last_code, $this->last_error );
		return false;
	}

	/**
	 * Get the last error message.
	 *
	 * @return string
	 */
	public function get_error() {
		return $this->last_error;
	}

	/**
	 * Get the last response code.
	 *
	 * @return int
	 */
	public function get_response_code() {
		return $this->last_code;
	}

	/**
	 * Get the last response.
	 *
	 * @return string
	 */
	public function get_response() {
		return $this->last_response;
	}

	/**
	 * Get the host parameter value to send to the API.
	 *
	 * @return string
	 */
	public function get_host() {
		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		if ( empty( $host ) ) {
			$host = 'localhost';
		}

		/**
		 * Filter the host parameter value to send to the API.
		 *
		 * @param string $host Host.
		 */
		return $this->do_filter( 'instant_indexing/indexnow_host', $host );
	}

	/**
	 * Get the API key.
	 *
	 * @return string
	 */
	public function get_key() {
		if ( ! empty( $this->api_key ) ) {
			return $this->api_key;
		}

		$api_key = Helper::get_settings( 'instant_indexing.indexnow_api_key' );

		/**
		 * Filter the API key.
		 *
		 * @param string $api_key API key.
		 */
		$this->api_key = $this->do_filter( 'instant_indexing/indexnow_key', $api_key );

		return $this->api_key;
	}

	/**
	 * Alias for get_key().
	 */
	public function get_api_key() {
		return $this->get_key();
	}

	/**
	 * Get the API key location.
	 *
	 * @param string $context Key context.
	 * @return string
	 */
	public function get_key_location( $context = '' ) {
		/**
		 * Filter the API key location.
		 *
		 * @param string $location Location.
		 */
		return $this->do_filter( 'instant_indexing/indexnow_key_location', trailingslashit( home_url() ) . $this->get_key() . '.txt', $context );
	}

	/**
	 * Log the request.
	 *
	 * @param array  $urls    URLs to submit.
	 * @param int    $status  Response code.
	 * @param string $message Response message.
	 */
	public function log( $urls, $status, $message = '' ) {
		$log = get_option( 'rank_math_indexnow_log', [] );
		$url = $this->get_loggable_url( $urls );

		if ( ! $url ) {
			return;
		}

		$log[] = [
			'url'               => $url,
			'status'            => (int) $status,
			'manual_submission' => (bool) $this->is_manual,
			'message'           => $message,
			'time'              => time(),
		];

		// Only keep the last 100 entries.
		$log = array_slice( $log, -100 );

		update_option( 'rank_math_indexnow_log', $log, false );
	}

	/**
	 * Get the loggable URL from an array of URLs.
	 * If multiple URLs are submitted, return the first one and [+12]
	 *
	 * @param array $urls URLs to submit.
	 *
	 * @return string
	 */
	public function get_loggable_url( $urls ) {
		$urls       = array_values( (array) $urls );
		$count_urls = count( $urls );
		if ( ! $count_urls ) {
			return '';
		}

		$url = $urls[0];
		if ( $count_urls > 1 ) {
			$url .= ' [+' . ( $count_urls - 1 ) . ']';
		}

		return $url;
	}

	/**
	 * Get the log.
	 *
	 * @return array
	 */
	public function get_log() {
		return get_option( 'rank_math_indexnow_log', [] );
	}

	/**
	 * Clear the log.
	 */
	public function clear_log() {
		delete_option( 'rank_math_indexnow_log' );
	}

	/**
	 * Reset object properties.
	 */
	private function reset() {
		$this->last_error    = '';
		$this->last_code     = 0;
		$this->last_response = '';
		$this->is_success    = false;
	}

	/**
	 * Get the additional data to send to the API.
	 *
	 * @param array $urls URLs to submit.
	 *
	 * @return array
	 */
	private function get_payload( $urls ) {
		return wp_json_encode(
			[
				'host'        => $this->get_host(),
				'key'         => $this->get_key(),
				'keyLocation' => $this->get_key_location( 'request_payload' ),
				'urlList'     => (array) $urls,
			]
		);
	}

	/**
	 * Get the error message from the response message.
	 *
	 * @param string $message Response message.
	 */
	private function set_error_message( $message ) {
		if ( ! empty( $message ) ) {
			$this->last_error = $message;
			return;
		}

		$message     = __( 'Unknown error.', 'seo-by-rank-math' );
		$message_map = [
			400 => __( 'Invalid request.', 'seo-by-rank-math' ),
			403 => __( 'Invalid API key.', 'seo-by-rank-math' ),
			422 => __( 'Invalid URL.', 'seo-by-rank-math' ),
			429 => __( 'Too many requests.', 'seo-by-rank-math' ),
			500 => __( 'Internal server error.', 'seo-by-rank-math' ),
		];

		if ( isset( $message_map[ $this->last_code ] ) ) {
			$message = $message_map[ $this->last_code ];
		}

		$this->last_error = $message;
	}


	/**
	 * Generate and save a new API key.
	 */
	public function reset_key() {
		$settings                     = Helper::get_settings( 'instant_indexing', [] );
		$settings['indexnow_api_key'] = $this->generate_api_key();
		$this->api_key                = $settings['indexnow_api_key'];
		update_option( 'rank-math-options-instant-indexing', $settings );
	}

	/**
	 * Generate new random API key.
	 */
	private function generate_api_key() {
		$api_key = wp_generate_uuid4();
		$api_key = preg_replace( '[-]', '', $api_key );

		return $api_key;
	}
}
