<?php
/**
 * Contains the NPS Controller Class
 *
 * @since 1.4.0
 * @package Crowdsignal_Forms\Rest_Api
 */

namespace Crowdsignal_Forms\Rest_Api\Controllers;

use Crowdsignal_Forms\Crowdsignal_Forms;
use Crowdsignal_Forms\Frontend\Blocks\Crowdsignal_Forms_Nps_Block;

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

/**
 * NPS Controller Class
 *
 * @since 1.4.0
 */
class Nps_Controller {
	/**
	 * The namespace
	 *
	 * @var string
	 */
	protected $namespace = 'crowdsignal-forms/v1';

	/**
	 * The rest api base.
	 *
	 * @var string
	 */
	protected $rest_base = 'nps';

	/**
	 * Register the routes for NPS response submissions.
	 *
	 * Note: Create/update routes have been removed. Survey creation
	 * and updates now happen via a `post_save` hook rather than
	 * WP REST API endpoints.
	 *
	 * @since 1.4.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<survey_client_id>[a-zA-Z0-9\-\_]+)',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_survey' ),
					'permission_callback' => array( $this, 'get_survey_permissions_check' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<survey_id>\d+)/response',
			array(
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'upsert_nps_response' ),
					'permission_callback' => array( $this, 'create_or_update_nps_response_permissions_check' ),
					'args'                => $this->get_nps_fetch_params(),
				),
			)
		);
	}

	/**
	 * Get cached survey data by client ID (UUID).
	 *
	 * @since 1.8.0
	 *
	 * @param  \WP_REST_Request $request The API Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_survey( \WP_REST_Request $request ) {
		$survey_client_id = $request->get_param( 'survey_client_id' );

		if ( null === $survey_client_id ) {
			return new \WP_Error(
				'invalid-survey-client-id',
				__( 'Invalid survey client ID', 'crowdsignal-forms' ),
				array( 'status' => 400 )
			);
		}

		$survey_data = Crowdsignal_Forms::instance()
			->get_post_survey_meta_gateway()
			->get_survey_data_for_client_id( null, $survey_client_id );

		if ( empty( $survey_data ) || ! isset( $survey_data['id'] ) ) {
			return new \WP_Error(
				'resource-not-found',
				__( 'Resource not found', 'crowdsignal-forms' ),
				array( 'status' => 404 )
			);
		}

		return rest_ensure_response( $survey_data );
	}

	/**
	 * The permission check for getting survey data.
	 *
	 * @since 1.8.0
	 *
	 * @return bool
	 */
	public function get_survey_permissions_check() {
		return true;
	}

	/**
	 * This route acts as a proxy for Crowdsignal's NPS response endpoint,
	 * which allows recording and updating responses.
	 *
	 * @since 1.4.0
	 *
	 * Updating an existing response (when `r` is supplied) requires a checksum
	 * keyed with a server-side secret, so a caller cannot submit someone
	 * else's response id without the checksum issued to the original
	 * submitter. See {@see get_response_checksum()}.
	 *
	 * @param  \WP_REST_Request $request The API Request.
	 * @return \WP_REST_Response|WP_ERROR
	 */
	public function upsert_nps_response( \WP_REST_Request $request ) {
		$data      = $request->get_json_params();
		$survey_id = $request->get_param( 'survey_id' );

		$nonce       = isset( $data['nonce'] ) ? $data['nonce'] : '';
		$response_id = isset( $data['r'] ) ? $data['r'] : '';
		$checksum    = isset( $data['checksum'] ) ? (string) $data['checksum'] : '';

		$verifies = Crowdsignal_Forms_Nps_Block::verify_nonce( $nonce );

		if (
			! $verifies ||
			(
				$response_id &&
				! hash_equals( $this->get_response_checksum( $response_id ), $checksum )
			)
		) {
			return new \WP_Error(
				'forbidden',
				__( 'Forbidden', 'crowdsignal-forms' ),
				array( 'status' => 403 )
			);
		}

		$result = Crowdsignal_Forms::instance()->get_api_gateway()->update_nps_response(
			$survey_id,
			$data
		);

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

		$result['checksum'] = $this->get_response_checksum( $result['r'] );

		return rest_ensure_response( $result );
	}

	/**
	 * The permission check for creating/updating nps responses.
	 *
	 * @since 1.4.0
	 *
	 * @return bool
	 */
	public function create_or_update_nps_response_permissions_check() {
		return true;
	}

	/**
	 * Returns a validator array for the NPS endpoints params.
	 *
	 * @since 1.4.0
	 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/
	 *
	 * @return array
	 */
	protected function get_nps_fetch_params() {
		return array(
			'survey_id' => array(
				'validate_callback' => function ( $param, $request, $key ) {
					return is_numeric( $param );
				},
			),
		);
	}

	/**
	 * Creates a keyed checksum for a response ID.
	 *
	 * The checksum binds an update to whoever received it back from the
	 * original submission, so it must be unforgeable by a caller who only
	 * knows the response ID. It is therefore keyed with a server-side secret
	 * ( `wp_salt()` ): an attacker cannot recompute it for a response ID they
	 * did not create. The previous `sha1( $response_id . $nonce )` provided no
	 * authorization because every input was attacker-known (the nonce is the
	 * same shared value for all anonymous visitors).
	 *
	 * @param  string $response_id Response ID.
	 * @return string
	 */
	private function get_response_checksum( $response_id ) {
		return hash_hmac( 'sha256', (string) $response_id, wp_salt( 'nonce' ) );
	}
}
