<?php
/**
 * Class file for WCPay\Core\Server\Request.
 *
 * @package WooCommerce Payments
 */

namespace WCPay\Core\Server;

use DateTime;
use WC_Payments;
use WC_Payments_Http_Interface;
use WC_Payments_API_Client;
use WCPay\Core\Exceptions\Server\Request\Extend_Request_Exception;
use WCPay\Core\Exceptions\Server\Request\Immutable_Parameter_Exception;
use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception;
use WCPay\Core\Server\Request\Get_Request;
use WCPay\Exceptions\API_Exception;
use WP_Error;

/**
 * Base for requests to the WCPay server.
 */
abstract class Request {
	/**
	 * Contains a set of params, which the class considers immutable by others.
	 *
	 * Overwrite this in your class for individual properties.
	 *
	 * @var string[]
	 */
	const IMMUTABLE_PARAMS = [];

	/**
	 * Indicates which parameters are required (keys only).
	 *
	 * @var string[]
	 */
	const REQUIRED_PARAMS = [];

	/**
	 * Contains default values for parameters, which are not set automatically.
	 *
	 * @var string[]
	 */
	const DEFAULT_PARAMS = [];

	/**
	 * Holds the parameters of the request.
	 *
	 * @var []
	 */
	private $params = [];

	/**
	 * True when `->apply_filters()` is called to protect read-only props.
	 *
	 * In protected mode, if somebody tries to change an immutable param,
	 * as declared in `IMMUTABLE_PARAMS`, an exception will be thrown.
	 *
	 * This way important params can be safe from modifications by extensions.
	 *
	 * @var bool
	 */
	private $protected_mode = false;

	/**
	 * Stores the base class when `->apply_filters` is called.
	 * This class will be checked when `::extend` is called.
	 *
	 * @var string
	 */
	private $base_class;

	/**
	 * Holds the API client of WCPay.
	 *
	 * @var WC_Payments_API_Client
	 */
	protected $api_client;

	/**
	 * Holds the HTTP interface of WCPay.
	 *
	 * @var WC_Payments_Http_Interface
	 */
	protected $http_interface;

	/**
	 * Holds the ID of an item, which is included in the request URL.
	 *
	 * @var mixed (int|string)
	 */
	protected $id;

	/**
	 * Specifies the WordPress hook name that will be triggered upon calling the send() method.
	 *
	 * @var string
	 */
	protected $hook = '';

	/**
	 * Used to set the arguments for the request class WordPress hook. Make sure to add hook name first.
	 *
	 * @var array
	 */
	protected $hook_args = [];

	/**
	 * Creates a new request, loading dependencies in there.
	 *
	 * @param mixed $id The identifier for various update/get/delete requests.
	 *
	 * @indexof $this->routeList
	 *
	 * @return static
	 */

	/**
	 * Route list.
	 *
	 * @var string[]
	 */
	private $route_list = [
		WC_Payments_API_Client::ACCOUNTS_API               => 'accounts',
		WC_Payments_API_Client::CAPABILITIES_API           => 'accounts/capabilities',
		WC_Payments_API_Client::WOOPAY_ACCOUNTS_API        => 'accounts/platform_checkout',
		WC_Payments_API_Client::WOOPAY_COMPATIBILITY_API   => 'woopay/compatibility',
		WC_Payments_API_Client::DOMAIN_REGISTRATION_API    => 'payment_method_domains',
		WC_Payments_API_Client::CHARGES_API                => 'charges',
		WC_Payments_API_Client::CONN_TOKENS_API            => 'terminal/connection_tokens',
		WC_Payments_API_Client::TERMINAL_LOCATIONS_API     => 'terminal/locations',
		WC_Payments_API_Client::CUSTOMERS_API              => 'customers',
		WC_Payments_API_Client::CURRENCY_API               => 'currency',
		WC_Payments_API_Client::INTENTIONS_API             => 'intentions',
		WC_Payments_API_Client::REFUNDS_API                => 'refunds',
		WC_Payments_API_Client::DEPOSITS_API               => 'deposits',
		WC_Payments_API_Client::TRANSACTIONS_API           => 'transactions',
		WC_Payments_API_Client::DISPUTES_API               => 'disputes',
		WC_Payments_API_Client::FILES_API                  => 'files',
		WC_Payments_API_Client::ONBOARDING_API             => 'onboarding',
		WC_Payments_API_Client::TIMELINE_API               => 'timeline',
		WC_Payments_API_Client::PAYMENT_METHODS_API        => 'payment_methods',
		WC_Payments_API_Client::SETUP_INTENTS_API          => 'setup_intents',
		WC_Payments_API_Client::TRACKING_API               => 'tracking',
		WC_Payments_API_Client::PAYMENT_PROCESS_CONFIG_API => 'payment_process_config',
		WC_Payments_API_Client::PRODUCTS_API               => 'products',
		WC_Payments_API_Client::PRICES_API                 => 'products/prices',
		WC_Payments_API_Client::INVOICES_API               => 'invoices',
		WC_Payments_API_Client::SUBSCRIPTIONS_API          => 'subscriptions',
		WC_Payments_API_Client::SUBSCRIPTION_ITEMS_API     => 'subscriptions/items',
		WC_Payments_API_Client::READERS_CHARGE_SUMMARY     => 'reader-charges/summary',
		WC_Payments_API_Client::TERMINAL_READERS_API       => 'terminal/readers',
		WC_Payments_API_Client::MINIMUM_RECURRING_AMOUNT_API => 'subscriptions/minimum_amount',
		WC_Payments_API_Client::CAPITAL_API                => 'capital',
		WC_Payments_API_Client::WEBHOOK_FETCH_API          => 'webhook/failed_events',
		WC_Payments_API_Client::DOCUMENTS_API              => 'documents',
		WC_Payments_API_Client::VAT_API                    => 'vat',
		WC_Payments_API_Client::LINKS_API                  => 'links',
		WC_Payments_API_Client::AUTHORIZATIONS_API         => 'authorizations',
		WC_Payments_API_Client::FRAUD_OUTCOMES_API         => 'fraud_outcomes',
		WC_Payments_API_Client::FRAUD_RULESET_API          => 'fraud_ruleset',
		WC_Payments_API_Client::COMPATIBILITY_API          => 'compatibility',
	];

	/**
	 * Creates a new request, loading dependencies in there.
	 *
	 * @param mixed $id The identifier for various update/get/delete requests.
	 *
	 * @return static
	 */
	public static function create( $id = null ) {
		return WC_Payments::create_request( static::class, $id );
	}

	/**
	 * GET Request wrapped for easier request creation.
	 *
	 * @param string $api Api method.
	 * @param mixed  $id An optional ID for the item that will be updated/retrieved/deleted.
	 *
	 * @return Request|Get_Request
	 *
	 * @throws Invalid_Request_Parameter_Exception|\Exception
	 */
	public static function get( string $api, $id = null ) {
		/**
		 * Request variable.
		 *
		 * @var Get_Request $request
		 */
		$request = WC_Payments::create_request( Get_Request::class, $id );
		$request->set_api( $api );
		return $request;
	}

	/**
	 * Prevents the class from being constructed directly.
	 *
	 * @param WC_Payments_API_Client     $api_client The API client to use to send requests.
	 * @param WC_Payments_Http_Interface $http_interface The HTTP interface for the server.
	 * @param mixed                      $id An optional ID for the item that will be updated/retrieved/deleted.
	 *
	 * @throws Invalid_Request_Parameter_Exception
	 */
	public function __construct( WC_Payments_API_Client $api_client, WC_Payments_Http_Interface $http_interface, $id = null ) {
		$this->api_client     = $api_client;
		$this->http_interface = $http_interface;

		$this->set_request_route_id_parameter( $id );
	}

	/**
	 * Returns the needed API.
	 *
	 * @return string Check WCPay\Core\Server\APIs.
	 */
	abstract public function get_api(): string;

	/**
	 * Returns the method of the request.
	 *
	 * @return string See the constants in WordPress's `Requests` class.
	 */
	abstract public function get_method(): string;

	/**
	 * This is a legacy method, and is the same throughout the codebase.
	 * Might be worth removing while refactoring to use the Core\Server API.
	 *
	 * @return bool
	 */
	public function is_site_specific(): bool {
		return true;
	}

	/**
	 * If true, the request will be signed with the user token rather than blog token. Defaults to false.
	 *
	 * @return bool
	 */
	public function should_use_user_token(): bool {
		return false;
	}

	/**
	 * Indicates if the raw response should be returned.
	 *
	 * @return bool
	 */
	public function should_return_raw_response(): bool {
		return false;
	}

	/**
	 * Returns all of the parameters for the request.
	 *
	 * @return array
	 * @throws Invalid_Request_Parameter_Exception If the request has not been initialized yet.
	 */
	final public function get_params() {
		$defaults = static::get_default_params();
		$params   = array_merge( $defaults, $this->params );

		$missing_params = [];
		foreach ( static::get_required_params() as $name ) {
			if ( ! isset( $params[ $name ] ) ) {
				$missing_params[] = $name;
			}
		}

		if ( ! empty( $missing_params ) ) {
			throw new Invalid_Request_Parameter_Exception(
				esc_html(
					sprintf(
						'Trying to access the parameters of a request which is not (fully) initialized yet. Missing parameter(s) for %s: %s',
						get_class( $this ),
						implode( ', ', $missing_params )
					)
				),
				'wcpay_core_invalid_request_parameter_missing_parameters'
			);
		}

		foreach ( $params as $key => $value ) {
			if ( true === $value ) {
				// The WCPay server requires the string 'true'.
				$params[ $key ] = 'true';
			}
		}

		return $params;
	}

	/**
	 * Get request param by key.
	 *
	 * @param string $key Key to get.
	 *
	 * @return mixed
	 * @throws Invalid_Request_Parameter_Exception
	 */
	final public function get_param( $key ) {
		if ( array_key_exists( $key, $this->params ) ) {
			return $this->params[ $key ];
		}
		throw new Invalid_Request_Parameter_Exception(
			esc_html(
				sprintf(
					'The passed key %s does not exist in Request class',
					$key
				)
			),
			'wcpay_core_invalid_request_parameter_uninitialized_param'
		);
	}

	/**
	 * Allows the request to be modified, and then sends it.
	 *
	 * @return mixed               Either the response array, or the correct object.
	 *
	 * @throws Extend_Request_Exception
	 * @throws Immutable_Parameter_Exception
	 * @throws Invalid_Request_Parameter_Exception
	 */
	final public function send() {
		return $this->format_response(
			$this->api_client->send_request( $this->apply_filters( $this->hook, ...$this->hook_args ) )
		);
	}

	/**
	 * This is mimic of send method, but where API exception is handled.
	 * The reason behind this is that sometimes API request can fail for valid reasons and instead of handling this exception on every request, you could use this function.
	 *
	 * @return mixed               Either the response array, or the correct object.
	 *
	 * @throws Extend_Request_Exception
	 * @throws Immutable_Parameter_Exception
	 * @throws Invalid_Request_Parameter_Exception
	 */
	final public function handle_rest_request() {
		try {
			$data = $this->send();
			// Make sure to return array if $data is instance or has parent as a Response class.
			if ( is_a( $data, Response::class ) ) {
				return $data->to_array();
			}

			// Return the data and let caller to parse it as it pleases.
			return $data;
		} catch ( API_Exception $e ) {
			return new WP_Error( $e->get_error_code(), $e->getMessage() );
		}
	}

	/**
	 * Formats the response from the server.
	 *
	 * @param  mixed $response The response from `WC_Payments_API_Client::request`.
	 * @return mixed           Either the same response, or the correct object.
	 */
	public function format_response( $response ) {
		return new Response( $response );
	}

	/**
	 * Used to validate passed ID from the constructor. You can easily override it if you need custom id handling in your classes.
	 *
	 * @param mixed $id ID parameter.
	 *
	 * @return void
	 * @throws Invalid_Request_Parameter_Exception
	 */
	protected function set_request_route_id_parameter( $id ) {
		if ( method_exists( $this, 'set_id' ) ) {
			if ( null !== $id ) {
				$this->set_id( $id );
			} else {
				throw new Invalid_Request_Parameter_Exception( 'This request requires an item ID.', 'wcpay_core_invalid_request_parameter_missing_id' );
			}
		}
	}
	/**
	 * Assign the WordPress hook and the arguments specific to the previously assigned hook.
	 *
	 * @param string $hook WordPress hook name.
	 */
	public function assign_hook( string $hook ) {
		$this->hook = $hook;
	}

	/**
	 * Set hook arguments. Used when hook is predefined in the request class, but you want to pass hook args.
	 *
	 * @param mixed ...$args Arguments for the hook.
	 * @return void
	 */
	public function set_hook_args( ...$args ) {
		$this->hook_args = $args;
	}

	/**
	 * Stores a parameter within the internal props.
	 *
	 * Use this method within child classes in order to allow
	 * those properties to be protected by overwriting.
	 *
	 * @param string $key   The name of the parameter.
	 * @param mixed  $value And the value to set.
	 */
	final protected function set_param( string $key, $value ) {
		if ( $this->protected_mode && in_array( $key, static::get_immutable_params(), true ) ) {
			$this->throw_immutable_exception( $key );
		}

		$this->params[ $key ] = $value;
	}

	/**
	 * Unsets an existing parameter if it was set before.
	 *
	 * @param string $key The key of the parameter.
	 */
	final protected function unset_param( string $key ) {
		if ( $this->protected_mode && in_array( $key, static::get_immutable_params(), true ) ) {
			$this->throw_immutable_exception( $key );
		}

		if ( isset( $this->params[ $key ] ) ) {
			unset( $this->params[ $key ] );
		}
	}

	/**
	 * Replaces all internal parameters of the class.
	 * Only accessible from methods of this class, used for the `extend` method.
	 *
	 * @param array $params The new parameters to use.
	 */
	private function set_params( $params ) {
		$this->params = $params;
	}

	/**
	 * Creates a new instance of the called class with the same props
	 * as an existing request, which must be of a parent class.
	 *
	 * This method is only available within `apply_filters()`.
	 *
	 * @param  Request $base_request    The request to extend.
	 * @return static                   An instance of the class.
	 * @throws Extend_Request_Exception In case this is not a subclass of the base request.
	 */
	final public static function extend( Request $base_request ) {
		$current_class = static::class;
		$base_request->validate_extended_class( $current_class, $base_request->base_class ?? get_class( $base_request ) );

		if ( ! $base_request->protected_mode ) {
			throw new Extend_Request_Exception(
				esc_html( get_class( $base_request ) . ' can only be extended within its ->apply_filters() method.' ),
				'wcpay_core_extend_class_incorrectly'
			);
		}
		$obj = new $current_class( $base_request->api_client, $base_request->http_interface, $base_request->id ?? null );
		$obj->set_params( array_merge( static::DEFAULT_PARAMS, $base_request->params ) );

		// Carry over the base class and protected mode into the child request.
		$obj->base_class     = $base_request->base_class;
		$obj->protected_mode = true;

		return $obj;
	}

	/**
	 * Allows the request to be altered/replaced through a filter.
	 *
	 * Call this method when the request has been completely prepared,
	 * and is ready to be sent to the server. At this point functions,
	 * which hook into the filter cannot alter the IMMUTABLE_PARAMS
	 * of the request anymore. Instead they can either modify the other
	 * mutable params, or extend the request.
	 *
	 * @param string $hook                             The filter to use.
	 * @param mixed  ...$args                          Other parameters for the hook.
	 * @return static                                  Either the same instance, or an object from a sub-class.
	 * @throws Extend_Request_Exception                In case a class does not exist.
	 * @throws Immutable_Parameter_Exception           In case an immutable propery is tried to change.
	 * @throws Invalid_Request_Parameter_Exception     In case an invalid property is passed.
	 */
	final public function apply_filters( $hook, ...$args ) {
		// Lock the class in order to prevent `set_param` for protected props.
		$this->protected_mode = true;
		$this->base_class     = get_class( $this );

		// Validate API route.
		$this->validate_api_route( $this->get_api() );

		/**
		 * Allows a request to be modified, extended or replaced.
		 *
		 * @param Request $request The request to modify.
		 * @param mixed   ...$args Other provided parameters for the hook.
		 * @return Request         Either the same request, or a sub-class.
		 */
		$replacement = apply_filters( $hook, $this, ...$args );

		// Exit protected mode right after `apply_filters`.
		$this->protected_mode = false;

		$my_class  = get_class( $this );
		$new_class = get_class( $replacement );
		if ( $new_class !== $my_class ) {
			$this->validate_extended_class( $new_class, $my_class );
		}

		// NB: `array_diff` will only pick up updated props, not new ones.
		$difference = $this->array_diff( $this->params, $replacement->params );

		if ( empty( $difference ) ) {
			// Nothing got overwritten, it's the same request, or one with only new props.
			return $replacement;
		}

		foreach ( static::get_immutable_params() as $param ) {
			if ( isset( $difference[ $param ] ) ) {
				$this->throw_immutable_exception( $param );
			}
		}

		return $replacement;
	}

	/**
	 * Throws an exception upon attempts to mutate an immutable parameter.
	 *
	 * @param string $param The name of the param.
	 * @throws Immutable_Parameter_Exception   An exception, which indicates which property is immutable.
	 */
	private function throw_immutable_exception( string $param ) {
		throw new Immutable_Parameter_Exception(
			esc_html(
				sprintf(
					'The value of %s::%s is immutable and cannot be changed.',
					get_class( $this ),
					$param
				)
			),
			'wcpay_core_immutable_parameter_changed'
		);
	}

	/**
	 * Returns an array with the names of params, which should not be modified.
	 *
	 * @return string[] The names of those params.
	 */
	public static function get_immutable_params() {
		return static::traverse_class_constants( 'IMMUTABLE_PARAMS', true );
	}

	/**
	 * Returns an array with the names of params, which are required.
	 *
	 * @return string[] The names of those params.
	 */
	public static function get_required_params() {
		return static::traverse_class_constants( 'REQUIRED_PARAMS', true );
	}

	/**
	 * Returns an array with the combined default params from all classes.
	 */
	public static function get_default_params() {
		return static::traverse_class_constants( 'DEFAULT_PARAMS' );
	}

	/**
	 * Combines array constants from a class's tree.
	 *
	 * @param  string $constant_name The name of the constant to load.
	 * @param  bool   $unique        Whether to return unique items. Useful with numeric keys.
	 * @return string[] The unique combined values from the arrays.
	 */
	public static function traverse_class_constants( string $constant_name, bool $unique = false ) {
		$keys       = [];
		$class_name = static::class;

		do {
			$constant = "$class_name::$constant_name";

			if ( defined( $constant ) ) {
				$keys = array_merge( constant( $constant ), $keys );
			}

			$class_name = get_parent_class( $class_name );
		} while ( $class_name );

		if ( $unique ) {
			$keys = array_unique( $keys );
		}

		return $keys;
	}

	/**
	 * Generates the difference between two arrays.
	 *
	 * @param array $array1 The first array.
	 * @param array $array2 The second array.
	 * @return array        The difference between the two arrays.
	 */
	private function array_diff( $array1, $array2 ) {
		$arr_to_json = function ( $item ) {
			return is_array( $item ) ? wp_json_encode( $item ) : $item;
		};

		return array_diff_assoc(
			array_map( $arr_to_json, $array1 ),
			array_map( $arr_to_json, $array2 )
		);
	}

	/**
	 * Validates Stripe identifiers.
	 *
	 * @param  string     $id        The identifier to validate.
	 * @param  mixed|null $prefixes  A prefix or an array of them (Optional).
	 * @throws Invalid_Request_Parameter_Exception An exception if the format is not matched.
	 * @return void
	 */
	protected function validate_stripe_id( $id, $prefixes = null ) {
		if ( empty( $id ) ) {
			throw new Invalid_Request_Parameter_Exception(
				esc_html__( 'Empty parameter is not allowed', 'woocommerce-payments' ),
				'wcpay_core_invalid_request_parameter_stripe_id'
			);
		}
		if ( is_null( $prefixes ) ) {
			$prefixes = '[a-z]+';
		} else {
			if ( ! is_array( $prefixes ) ) {
				$prefixes = [ $prefixes ];
			}

			$prefixes = '('
				. implode( '|', array_map( 'preg_quote', $prefixes ) )
				. ')';
		}

		/**
		 * IDs include a prefix (a few characters), and are up to 255 characters long.
		 *
		 * @see https://stripe.com/docs/upgrades#what-changes-does-stripe-consider-to-be-backwards-compatible
		 */
		$regex = "/^{$prefixes}_\w{1,250}$/";

		if ( preg_match( $regex, $id ) ) {
			return;
		}

		throw new Invalid_Request_Parameter_Exception(
			esc_html(
				sprintf(
				// Translators: %s is a Stripe ID.
					__( '%s is not a valid Stripe identifier', 'woocommerce-payments' ),
					$id
				)
			),
			'wcpay_core_invalid_request_parameter_stripe_id'
		);
	}

	/**
	 * Validate is number larger than passed compared number.
	 *
	 * @param  float $value_to_validate Value to validate.
	 * @param  float $value_to_compare Value to compare.
	 * @throws Invalid_Request_Parameter_Exception An exception if the format is not matched.
	 * @return void
	 */
	protected function validate_is_larger_than( float $value_to_validate, float $value_to_compare ) {
		if ( $value_to_validate > $value_to_compare ) {
			return;
		}

		throw new Invalid_Request_Parameter_Exception(
			esc_html(
				sprintf(
				/* translators: %1$s and %2$s are both numbers */
					__( 'Invalid number passed. Number %1$s needs to be larger than %2$s', 'woocommerce-payments' ),
					$value_to_validate,
					$value_to_compare
				)
			),
			'wcpay_core_invalid_request_parameter_order'
		);
	}

	/**
	 * Currency code validator.
	 *
	 * @param string $currency_code Currency code.
	 *
	 * @return void
	 * @throws Invalid_Request_Parameter_Exception
	 */
	public function validate_currency_code( string $currency_code ) {
		$account_data = WC_Payments::get_account_service()->get_cached_account_data();
		if ( isset( $account_data['customer_currencies']['supported'] ) && ! in_array( $currency_code, $account_data['customer_currencies']['supported'], true ) ) {
			throw new Invalid_Request_Parameter_Exception(
				esc_html(
					sprintf(
					// Translators: %s is a currency code.
						__( '%s is not a supported currency for payments.', 'woocommerce-payments' ),
						$currency_code
					)
				),
				'wcpay_core_invalid_request_parameter_currency_not_available'
			);
		}
	}

	/**
	 * Extend class validator.
	 *
	 * @param mixed  $child_class  Child class.
	 * @param string $parent_class Parent class.
	 *
	 * @return void
	 * @throws Extend_Request_Exception
	 */
	public function validate_extended_class( $child_class, string $parent_class ) {

		if ( ! is_subclass_of( $child_class, $parent_class ) ) {
			throw new Extend_Request_Exception(
				esc_html(
					sprintf(
						'Failed to extend request. %s is not a subclass of %s',
						is_string( $child_class ) ? $child_class : get_class( $child_class ),
						$parent_class
					)
				),
				'wcpay_core_extend_class_not_subclass'
			);
		}
	}

	/**
	 * Validate date with given format.
	 *
	 * @param string $date Date to validate.
	 * @param string $format Format to check.
	 *
	 * @return void
	 * @throws Invalid_Request_Parameter_Exception
	 */
	public function validate_date( string $date, string $format = 'Y-m-d H:i:s' ) {
		$d = DateTime::createFromFormat( $format, $date );
		if ( ! ( $d && $d->format( $format ) === $date ) ) {
			throw new Invalid_Request_Parameter_Exception(
				esc_html(
					sprintf(
					// Translators: %1$s is a provided date string, %2$s is a date format.
						__( '%1$s is not a valid date for format %2$s.', 'woocommerce-payments' ),
						$date,
						$format
					)
				),
				'wcpay_core_invalid_request_parameter_invalid_date'
			);
		}
	}

	/**
	 * Validate a redirect URL in the allowed_redirect_hosts filter.
	 *
	 * @param  string $redirect_url The provided redirect URL.
	 *
	 * @return void
	 * @throws Invalid_Request_Parameter_Exception
	 */
	public function validate_redirect_url( string $redirect_url ) {
		$check_fallback_url = wp_generate_password( 12, false );
		if ( hash_equals( $check_fallback_url, wp_validate_redirect( $redirect_url, $check_fallback_url ) ) ) {
			throw new Invalid_Request_Parameter_Exception(
				esc_html(
					sprintf(
					// Translators: %s is a currency code.
						__( '%1$s is not a valid redirect URL. Use a URL in the allowed_redirect_hosts filter.', 'woocommerce-payments' ),
						$redirect_url
					)
				),
				'wcpay_core_invalid_request_parameter_invalid_redirect_url'
			);
		}
	}

	/**
	 * Validate if the username exists and is valid on the site.
	 *
	 * @param string $user_name Username to validate.
	 *
	 * @return void
	 * @throws Invalid_Request_Parameter_Exception
	 */
	public function validate_user_name( string $user_name ) {
		$user = get_user_by( 'login', $user_name );
		if ( false === $user ) {
			throw new Invalid_Request_Parameter_Exception(
				esc_html(
					sprintf(
					// Translators: %s is a provided username.
						__( '%s is not a valid username.', 'woocommerce-payments' ),
						$user_name
					)
				),
				'wcpay_core_invalid_request_parameter_invalid_username'
			);
		}
	}

	/**
	 * Validates API endpoint or route.
	 *
	 * @param string $api_route API route to validate.
	 *
	 * @throws Invalid_Request_Parameter_Exception
	 */
	public function validate_api_route( string $api_route ) {

		$api_route = explode( '/', $api_route ); // In case if you have something after "/" like id or something similar.

		// Some routes have 2 URIs. In case route we want to validate have 2 (or more) URI,s lets validate that first.
		// There could be micro optimization to validate only array keys with two 'URI's but for now we can skip that part.
		if ( ( count( $api_route ) > 1 && array_key_exists( "$api_route[0]/$api_route[1]", $this->route_list ) ) || array_key_exists( $api_route[0], $this->route_list ) ) {
			return;
		}
		throw new Invalid_Request_Parameter_Exception( 'Invalid request api route', 'wcpay_core_invalid_request_parameter_api_route_not_defined' );
	}
}
