<?php
/**
 * Postomatic Plugin - Server-side proxy for browser-initiated API calls.
 *
 * @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;
}

/**
 * Proxies the two remote API calls that used to be made from the browser.
 *
 * The blog-posts sync and the agreement signature both called the remote service
 * directly with `fetch()`, which meant the site's API service key had to be
 * printed into the page so JavaScript could put it in the `Api-Key` header. That
 * put a long-lived credential for the whole account into admin page HTML, where
 * it is readable from view-source, kept in the browser cache and history, and
 * visible to every browser extension and every script on the page. It was also
 * sent from the visitor's network rather than the server's.
 *
 * Both calls now go through admin-ajax instead: the browser sends only the data,
 * and the key never leaves the server.
 *
 * @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 Api_Proxy_Handler {

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'wp_ajax_postomatic_sync_blog_posts', array( $this, 'sync_blog_posts' ) );
		add_action( 'wp_ajax_postomatic_sign_agreement', array( $this, 'sign_agreement' ) );
	}

	/**
	 * 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' => 'Security verification failed.' ), 403 );
		}

		if ( ! current_user_can( Constants::CAPABILITY ) ) {
			wp_send_json_error( array( 'message' => 'You do not have permission to perform this action.' ), 403 );
		}
	}

	/**
	 * Forward a request to the remote service using the stored API key.
	 *
	 * A 401 triggers one key refresh and one retry, so an expired key recovers on
	 * its own instead of surfacing as a failed sync.
	 *
	 * @param  string $method HTTP method.
	 * @param  string $url    Remote endpoint.
	 * @param  array  $body   Request body, encoded as JSON.
	 * @return array{code:int, body:mixed}|\WP_Error
	 */
	private function forward( string $method, string $url, array $body ) {
		$response = $this->request( $method, $url, $body, Utils::get_api_service_key() );

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

		if ( 401 === (int) wp_remote_retrieve_response_code( $response ) && Activator::refresh_api_key() ) {
			$retry = $this->request( $method, $url, $body, Utils::get_api_service_key() );

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

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

	/**
	 * Perform a single request against the remote service.
	 *
	 * @param  string $method  HTTP method.
	 * @param  string $url     Remote endpoint.
	 * @param  array  $body    Request body, encoded as JSON.
	 * @param  string $api_key API service key.
	 * @return array|\WP_Error
	 */
	private function request( string $method, string $url, array $body, $api_key ) {
		return wp_remote_request(
			$url,
			array(
				'method'      => $method,
				'timeout'     => 30,
				'httpversion' => '1.1',
				'headers'     => array(
					'Content-Type' => 'application/json',
					'Accept'       => 'application/json',
					'Api-Key'      => (string) $api_key,
				),
				'body'        => wp_json_encode( $body ),
			)
		);
	}

	/**
	 * Send a batch of local post summaries to the remote service.
	 *
	 * @return void
	 */
	public function sync_blog_posts() {
		$this->verify_request();

		$offset = isset( $_POST['offset'] ) ? absint( wp_unslash( $_POST['offset'] ) ) : 0;
		$raw    = isset( $_POST['posts'] ) ? wp_unslash( $_POST['posts'] ) : '[]'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- decoded and sanitized field by field below.
		$posts  = json_decode( (string) $raw, true );

		if ( ! is_array( $posts ) ) {
			$posts = array();
		}

		$clean = array();

		foreach ( $posts as $post ) {
			if ( ! is_array( $post ) ) {
				continue;
			}

			$clean[] = array(
				'id'      => isset( $post['id'] ) ? absint( $post['id'] ) : 0,
				'title'   => isset( $post['title'] ) ? sanitize_text_field( wp_strip_all_tags( (string) $post['title'] ) ) : '',
				'url'     => isset( $post['url'] ) ? esc_url_raw( (string) $post['url'] ) : '',
				'tags'    => isset( $post['tags'] ) && is_array( $post['tags'] ) ? array_map( 'absint', $post['tags'] ) : array(),
				'excerpt' => isset( $post['excerpt'] ) ? sanitize_textarea_field( wp_strip_all_tags( (string) $post['excerpt'] ) ) : '',
			);
		}

		$result = $this->forward(
			'POST',
			Constants::API_POST_FETCHER,
			array(
				'posts'  => $clean,
				'offset' => $offset,
			)
		);

		$this->send( $result );
	}

	/**
	 * Record the signed agreement with the remote service.
	 *
	 * @return void
	 */
	public function sign_agreement() {
		$this->verify_request();

		$current_user = wp_get_current_user();

		$result = $this->forward(
			'PUT',
			Constants::API_AGREEMENT,
			array(
				'is_signed' => true,
				'wp_user'   => $current_user ? $current_user->user_login : '',
				// Generated here rather than accepted from the request, so the
				// payload keeps its original shape without trusting client input.
				'_wpnonce'  => wp_create_nonce( 'postomatic_agreement_nonce' ),
			)
		);

		$this->send( $result );
	}

	/**
	 * 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 ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 500 );
		}

		if ( $result['code'] >= 400 ) {
			$message = is_array( $result['body'] ) && ! empty( $result['body']['message'] )
				? $result['body']['message']
				: 'The request could not be completed.';

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

		wp_send_json_success( $result['body'] );
	}
}
