<?php
/**
 * Postomatic Plugin - Regenerate cancelled posts handler.
 *
 * @package  Postomatic
 * @category Plugin
 * @license  GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link     https://buildomatics.com/postomatics
 * @since    1.0.0
 */

namespace Postomatic\Api;

use Postomatic\Constants;
use Postomatic\Core\Activator;
use Postomatic\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Puts cancelled posts back into the generation queue.
 *
 * Cancelling a post refunds the credits it reserved, so re-queueing one has to
 * debit them again. The amount is decided entirely by the service: the plugin
 * never sends a cost, and a 402 carries the authoritative figure.
 *
 * @category   Plugin
 * @subpackage Api
 * @license    GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link       https://buildomatics.com/postomatics
 * @since      1.0.0
 */
class Regenerate_Post_Handler {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'wp_ajax_postomatic_regenerate_post', array( $this, 'regenerate_post' ) );
		add_action( 'wp_ajax_postomatic_regenerate_cancelled_posts', array( $this, 'regenerate_cancelled_posts' ) );
	}

	/**
	 * Initialize the handler.
	 *
	 * @return void
	 */
	public static function init() {
		new self();
	}

	/**
	 * Verify the nonce and capability, ending the request when either fails.
	 *
	 * @return void
	 */
	private function verify_request() {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';

		if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'postomatic_admin_nonce' ) ) {
			wp_send_json_error( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		// Re-queueing spends credits, so it needs the same access as creating a
		// post in the first place.
		if ( ! current_user_can( 'edit_posts' ) ) {
			wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 );
		}
	}

	/**
	 * Re-queue a single cancelled post.
	 *
	 * @return void
	 */
	public function regenerate_post() {
		$this->verify_request();

		$post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : '';

		if ( '' === $post_id ) {
			wp_send_json_error( array( 'message' => 'Invalid post ID.' ), 400 );
		}

		// Matched exactly by the service, so sent verbatim and URL-encoded.
		$api_url = str_replace( '%postid%', rawurlencode( $post_id ), Constants::API_POSTS_REGENERATE );

		$this->send( $this->forward( $api_url ) );
	}

	/**
	 * Re-queue every cancelled post in one call.
	 *
	 * @return void
	 */
	public function regenerate_cancelled_posts() {
		$this->verify_request();

		$this->send( $this->forward( Constants::API_POSTS_REGENERATE_ALL ) );
	}

	/**
	 * POST to the service, refreshing the key once on a 401.
	 *
	 * @param  string $api_url Endpoint to call.
	 * @return array{code:int, body:mixed}|\WP_Error
	 */
	private function forward( string $api_url ) {
		$api_key = Utils::get_api_service_key();

		if ( empty( $api_key ) ) {
			return new \WP_Error( 'missing_api_key', 'API key not found.', array( 'status' => 403 ) );
		}

		$response = $this->request( $api_url, $api_key );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		/*
		 * A 429 is returned as-is. It must not be mistaken for a bad credential:
		 * rotating the key on a throttled response would spend an activation
		 * attempt, which is itself rate limited.
		 */
		if ( 401 === $code && Activator::refresh_api_key() ) {
			$retry = $this->request( $api_url, Utils::get_api_service_key() );

			if ( ! is_wp_error( $retry ) ) {
				$response = $retry;
				$code     = (int) wp_remote_retrieve_response_code( $retry );
			}
		}

		return array(
			'code'     => $code,
			'body'     => json_decode( wp_remote_retrieve_body( $response ), true ),
			'response' => $response,
		);
	}

	/**
	 * Perform a single request against the service.
	 *
	 * @param  string $api_url Endpoint to call.
	 * @param  string $api_key API service key.
	 * @return array|\WP_Error
	 */
	private function request( string $api_url, $api_key ) {
		return wp_remote_post(
			$api_url,
			array(
				'timeout'     => 45,
				'httpversion' => '1.1',
				'headers'     => array(
					'Content-Type' => 'application/json',
					'Accept'       => 'application/json',
					'Api-Key'      => (string) $api_key,
				),
				/*
				 * Deliberately no cost field. The service calculates the debit from
				 * the blog's plan; a client-supplied amount used to be trusted for
				 * both the balance check and the debit, which let a caller set its
				 * own price or be credited with a negative value.
				 */
				'body'        => wp_json_encode( array() ),
			)
		);
	}

	/**
	 * Translate a forward() result into a JSON response.
	 *
	 * @param  array|\WP_Error $result Result from forward().
	 * @return void
	 */
	private function send( $result ) {
		if ( is_wp_error( $result ) ) {
			$status = (int) ( $result->get_error_data()['status'] ?? 500 );

			wp_send_json_error( array( 'message' => $result->get_error_message() ), $status ? $status : 500 );
		}

		$body    = is_array( $result['body'] ) ? $result['body'] : array();
		$message = ! empty( $body['message'] ) ? $body['message'] : '';

		if ( 429 === $result['code'] ) {
			$retry_after = Utils::get_retry_after_seconds( $result['response'] );

			wp_send_json_error(
				array(
					'message'     => sprintf(
						/* translators: %d: seconds to wait. */
						esc_html__( 'Too many requests. Please try again in %d seconds.', 'postomatic' ),
						$retry_after
					),
					'retry_after' => $retry_after,
				),
				429
			);
		}

		if ( $result['code'] >= 400 ) {
			/*
			 * 402 messages carry the real amount required ("You only have 5
			 * credits, but need 12."), so the service's text is passed through
			 * rather than replaced with a generic line.
			 */
			if ( '' === $message ) {
				$message = 402 === $result['code']
					? esc_html__( 'You do not have enough credits to regenerate this content.', 'postomatic' )
					: esc_html__( 'The content could not be re-queued. Please try again.', 'postomatic' );
			}

			wp_send_json_error( array( 'message' => $message ), $result['code'] );
		}

		wp_send_json_success( $body );
	}
}
