<?php
/**
 * REST API endpoint.
 *
 * @package Effortless_Taiwan_Mandarin_Helper
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Registers and handles POST /wp-json/eltmh/v1/translate.
 */
class ELTMH_Rest {

	/**
	 * Sets up the instance.
	 *
	 * @param ELTMH_Cache $cache Cache instance.
	 * @param ELTMH_API   $api   API instance.
	 */
	public function __construct( private ELTMH_Cache $cache, private ELTMH_API $api ) {}

	/** Registers hooks. */
	public function init(): void {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/** Registers the translate route. */
	public function register_routes(): void {
		register_rest_route(
			'eltmh/v1',
			'/translate',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'handle_translate' ),
				'permission_callback' => array( $this, 'check_permission' ),
				'args'                => array(
					'text' => array(
						'required'          => true,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
						'validate_callback' => array( $this, 'validate_text' ),
					),
				),
			)
		);

		register_rest_route(
			'eltmh/v1',
			'/nonce',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'handle_nonce' ),
				'permission_callback' => array( $this, 'allow_public_nonce_request' ),
			)
		);
	}

	/**
	 * Verifies the request nonce.
	 *
	 * This is a same-origin/CSRF check, not an access-control check: the nonce it
	 * verifies is obtainable by any caller from the public /nonce route (by design —
	 * the shortcode/admin widget must work for anonymous visitors), so it does not
	 * itself limit who can trigger this billable translation call. Cost control is
	 * enforced separately by check_rate_limit(), which applies to every caller,
	 * logged in or not.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return bool
	 */
	public function check_permission( WP_REST_Request $request ): bool {
		$nonce = $request->get_header( 'X-ELTMH-Nonce' );
		return (bool) wp_verify_nonce( $nonce, 'eltmh_translate' );
	}

	/**
	 * Permission callback for GET /nonce.
	 *
	 * Intentionally public, not a bare __return_true: this endpoint only issues a
	 * fresh eltmh_translate nonce, which carries no sensitive data by itself. The
	 * nonce is verified by check_permission() before /translate does anything, and
	 * anonymous /translate calls are separately rate-limited in check_rate_limit().
	 * An attacker gains nothing from calling this directly that they couldn't
	 * already get by loading any page containing the shortcode.
	 *
	 * @return true
	 */
	public function allow_public_nonce_request(): bool {
		return true;
	}

	/**
	 * Validates the text parameter.
	 *
	 * @param mixed $value Raw value.
	 * @return true|\WP_Error
	 */
	public function validate_text( $value ) {
		if ( '' === trim( (string) $value ) ) {
			return new WP_Error( 'eltmh_empty', __( 'Text cannot be empty.', 'effortless-taiwan-mandarin-helper' ), array( 'status' => 400 ) );
		}

		if ( mb_strlen( (string) $value ) > 500 ) {
			return new WP_Error( 'eltmh_too_long', __( 'Text must be 500 characters or fewer.', 'effortless-taiwan-mandarin-helper' ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Returns a fresh translation nonce.
	 *
	 * @return WP_REST_Response
	 */
	public function handle_nonce(): WP_REST_Response {
		return new WP_REST_Response(
			array( 'nonce' => wp_create_nonce( 'eltmh_translate' ) ),
			200
		);
	}

	/**
	 * Handles a translate request: rate-limit → cache → API → cache store → response.
	 *
	 * @param WP_REST_Request $request Incoming request.
	 * @return WP_REST_Response
	 */
	public function handle_translate( WP_REST_Request $request ): WP_REST_Response {
		$rate_error = $this->check_rate_limit();
		if ( $rate_error ) {
			return new WP_REST_Response(
				array(
					'code'    => $rate_error->get_error_code(),
					'message' => $rate_error->get_error_message(),
				),
				429
			);
		}

		$text     = $request->get_param( 'text' );
		$provider = $this->api->get_provider();
		$cached   = $this->cache->get( $text, $provider );

		if ( false !== $cached ) {
			return new WP_REST_Response( $cached, 200 );
		}

		$result = $this->api->translate( $text );

		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				array(
					'code'    => $result->get_error_code(),
					'message' => $result->get_error_message(),
				),
				500
			);
		}

		$this->cache->set( $text, $result, $provider );

		return new WP_REST_Response( $result, 200 );
	}

	/**
	 * Returns a WP_Error if the caller has exceeded 10 requests/minute, null otherwise.
	 *
	 * Applies to every caller, not just anonymous visitors: check_permission() only
	 * verifies a nonce obtainable by anyone from the public /nonce route, so it does
	 * not itself restrict who can trigger this billable translation call. Logged-in
	 * callers are keyed by user ID (one quota per account regardless of IP); everyone
	 * else is keyed by IP, as before.
	 *
	 * @return \WP_Error|null
	 */
	private function check_rate_limit(): ?\WP_Error {
		if ( is_user_logged_in() ) {
			$key = 'eltmh_rl_user_' . get_current_user_id();
		} else {
			$raw_ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$ip     = filter_var( $raw_ip, FILTER_VALIDATE_IP ) ? $raw_ip : 'unknown';
			$key    = 'eltmh_rl_' . md5( $ip );
		}

		// Non-atomic read-increment-write; concurrent requests may briefly exceed the limit.
		// Acceptable trade-off for a soft rate limit on a standard WordPress install.
		$hits = (int) get_transient( $key );

		if ( $hits >= 10 ) {
			return new \WP_Error(
				'eltmh_rate_limit',
				__( 'Too many requests. Please wait a minute.', 'effortless-taiwan-mandarin-helper' ),
				array( 'status' => 429 )
			);
		}

		set_transient( $key, $hits + 1, 60 );

		return null;
	}
}
