<?php
/**
 * Inbound delivery-receipt endpoint.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Webhook;

use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

defined( 'ABSPATH' ) || exit;

/**
 * Registers `POST /wp-json/texttango/v1/webhook` (spec §9).
 *
 * The signature check is the route's `permission_callback` rather than the first
 * few lines of the handler. That is where WordPress expects authorisation to
 * live, so an unsigned request is refused before any handler code runs, and the
 * route is never a `__return_true` endpoint that merely happens to check itself.
 */
final class Route {

	/**
	 * REST namespace.
	 */
	public const NAMESPACE_V1 = 'texttango/v1';

	/**
	 * Route path.
	 */
	public const ROUTE = '/webhook';

	/**
	 * Register hooks.
	 */
	public function register(): void {
		add_action( 'rest_api_init', array( $this, 'register_route' ) );
	}

	/**
	 * The public URL of this endpoint.
	 */
	public static function url(): string {
		return rest_url( self::NAMESPACE_V1 . self::ROUTE );
	}

	/**
	 * Declare the route.
	 */
	public function register_route(): void {
		register_rest_route(
			self::NAMESPACE_V1,
			self::ROUTE,
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle' ),
				'permission_callback' => array( $this, 'authorize' ),
			)
		);
	}

	/**
	 * Authorise by signature.
	 *
	 * @param WP_REST_Request $request Inbound request.
	 * @return true|WP_Error
	 */
	public function authorize( WP_REST_Request $request ) {
		if ( ! SignatureVerifier::is_configured() ) {
			return new WP_Error(
				'texttango_webhook_unconfigured',
				__( 'No webhook signing secret is configured on this site.', 'texttango' ),
				array( 'status' => 401 )
			);
		}

		$signature = (string) $request->get_header( SignatureVerifier::HEADER_KEY );

		if ( ! SignatureVerifier::verify( self::raw_body( $request ), $signature ) ) {
			return new WP_Error(
				'texttango_invalid_signature',
				__( 'Invalid signature.', 'texttango' ),
				array( 'status' => 401 )
			);
		}

		return true;
	}

	/**
	 * Process a verified receipt.
	 *
	 * @param WP_REST_Request $request Inbound request.
	 */
	public function handle( WP_REST_Request $request ): WP_REST_Response {
		$payload = json_decode( self::raw_body( $request ), true );

		if ( ! is_array( $payload ) ) {
			return new WP_REST_Response(
				array(
					'received' => false,
					'reason'   => 'invalid_json',
				),
				400
			);
		}

		$result = Handler::handle( $payload );

		/*
		 * Always 200 once the signature is good. A receipt for a message this site
		 * does not know about, or one already applied, is not an error TextTango
		 * should keep retrying — and retries would only repeat the same no-op.
		 */
		return new WP_REST_Response(
			array(
				'received' => true,
				'handled'  => $result['handled'],
				'reason'   => $result['reason'],
			),
			200
		);
	}

	/**
	 * The body exactly as it arrived.
	 *
	 * The signature covers these bytes, so it must never be re-encoded from a
	 * decoded array: key order and whitespace would change and no genuine receipt
	 * would ever verify.
	 *
	 * @param WP_REST_Request $request Inbound request.
	 */
	private static function raw_body( WP_REST_Request $request ): string {
		$body = (string) $request->get_body();

		if ( '' !== $body ) {
			return $body;
		}

		// A body-parsing plugin can consume the stream before REST sees it.
		$fallback = file_get_contents( 'php://input' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading the raw request body; there is no WordPress wrapper for this.

		return is_string( $fallback ) ? $fallback : '';
	}
}
