<?php
/**
 * Abstract class for REST API routes.
 *
 * @package northmill-online-shared
 */

declare(strict_types=1);

namespace Northmill_Online_Shared\Abstracts\REST;

use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

/**
 * Abstract class for REST API routes.
 */
abstract class Abstract_Route {

	/**
	 * The route for the REST API.
	 *
	 * @var string
	 */
	protected string $route;

	/**
	 * The version of the REST API.
	 *
	 * @var string
	 */
	protected string $version;

	/**
	 * The full route for the REST API.
	 *
	 * @var string
	 */
	protected string $full_route;

	/**
	 * Cached results of get_item_schema.
	 *
	 * @var array
	 */
	protected array $schema;

	/**
	 * Instance of Abstract_API for this route.
	 *
	 * @var Abstract_API
	 */
	protected Abstract_API $api;

	/**
	 * Array of endpoints for the route.
	 *
	 * @var array
	 */
	protected array $endpoints;

	/**
	 * Class constructor.
	 *
	 * Registers the REST API route with the specified version and route.
	 *
	 * @param Abstract_API $api     The API instance for the route.
	 * @param string       $route   The route for the REST API endpoint.
	 * @param string[]     $methods The methods that the REST API endpoint should respond to.
	 * @param string       $version The version of the REST API. Default is 'v1'.
	 */
	public function __construct(
		Abstract_API $api,
		string $route,
		array $methods,
		string $version = 'v1'
	) {
		$this->api        = $api;
		$this->route      = $route;
		$this->version    = $version;
		$this->full_route = $this->api->get_rest_namespace() . '/' . $this->version . '/' . $this->route;

		foreach ( $methods as $method_key => $method ) {
			$methods[ $method_key ] = strtoupper( $method );
		}

		$endpoints = array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get' ),
				'permission_callback' => array( $this, 'get_permission_check' ),
				'args'                => $this->get_args(),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'post' ),
				'permission_callback' => array( $this, 'post_permission_check' ),
				'args'                => $this->post_args(),
			),
			array(
				'methods'             => 'PUT',
				'callback'            => array( $this, 'put' ),
				'permission_callback' => array( $this, 'put_permission_check' ),
				'args'                => $this->put_args(),
			),
			array(
				'methods'             => 'PATCH',
				'callback'            => array( $this, 'patch' ),
				'permission_callback' => array( $this, 'patch_permission_check' ),
				'args'                => $this->patch_args(),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'delete' ),
				'permission_callback' => array( $this, 'delete_permission_check' ),
				'args'                => $this->delete_args(),
			),
		);

		foreach ( $endpoints as $key => $endpoint ) {
			$method = $endpoint['methods'];

			if ( ! in_array( $method, $methods, true ) ) {
				unset( $endpoints[ $key ] );
			}
		}

		$endpoints['schema'] = array( $this, 'get_schema' );

		$this->endpoints = $endpoints;
	}

	/**
	 * Registers the REST route.
	 */
	public function register_rest_route(): void {
		register_rest_route(
			$this->api->get_rest_namespace() . '/' . $this->version,
			$this->route,
			$this->endpoints,
		);
	}

	/**
	 * Retrieves the route for the REST API.
	 *
	 * @return string The route for the REST API.
	 */
	public function get_route(): string {
		return $this->route;
	}

	/**
	 * Responds to a REST API request by ensuring the response is of type WP_Error or WP_REST_Response.
	 *
	 * @param mixed $response The response that needs to be ensured to be of type WP_Error or WP_REST_Response.
	 *
	 * @return WP_Error|WP_REST_Response The ensured response.
	 */
	protected function process_response( mixed $response ): WP_Error|WP_REST_Response {
		return rest_ensure_response( $response );
	}

	/**
	 * Responds to a REST API request with an error.
	 *
	 * @param string $message The error message.
	 * @param string $code    The error code.
	 * @param int    $status  The HTTP status code for the error.
	 *
	 * @return WP_Error
	 */
	protected function process_error( string $message, string $code = 'api_error', int $status = 400 ): WP_Error {
		return new WP_Error( $code, $message, array( 'status' => $status ) );
	}

	/**
	 * Default method for the REST API. Responds with a 501 Not Implemented status code.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 *
	 * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function get( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		return $this->process_error(
			'This endpoint is not implemented.',
			'not_implemented',
			501,
		);
	}
	// phpcs:enable

	/**
	 * Checks the permission for the 'get' method in the REST API.
	 *
	 * @return bool True if the permission check passes, false otherwise.
	 */
	public function get_permission_check(): bool {
		return false;
	}

	/**
	 * Default method for the REST API. Responds with a 501 Not Implemented status code.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 *
	 * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function post( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		return $this->process_error(
			'This endpoint is not implemented.',
			'not_implemented',
			501,
		);
	}
	// phpcs:enable

	/**
	 * Checks the permission for the 'post' method in the REST API.
	 *
	 * @return bool True if the permission check passes, false otherwise.
	 */
	public function post_permission_check(): bool {
		return false;
	}

	/**
	 * Default method for the REST API. Responds with a 501 Not Implemented status code.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 *
	 * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function put( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		return $this->process_error(
			'This endpoint is not implemented.',
			'not_implemented',
			501,
		);
	}
	// phpcs:enable

	/**
	 * Checks the permission for the 'put' method in the REST API.
	 *
	 * @return bool True if the permission check passes, false otherwise.
	 */
	public function put_permission_check(): bool {
		return false;
	}

	/**
	 * Default method for the REST API. Responds with a 501 Not Implemented status code.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 *
	 * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function patch( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		return $this->process_error(
			'This endpoint is not implemented.',
			'not_implemented',
			501,
		);
	}
	// phpcs:enable

	/**
	 * Checks the permission for the 'patch' method in the REST API.
	 *
	 * @return bool True if the permission check passes, false otherwise.
	 */
	public function patch_permission_check(): bool {
		return false;
	}

	/**
	 * Default method for the REST API. Responds with a 501 Not Implemented status code.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_Error|WP_REST_Response
	 *
	 * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
	 * @noinspection PhpUnusedParameterInspection
	 */
	public function delete( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		return $this->process_error(
			'This endpoint is not implemented.',
			'not_implemented',
			501,
		);
	}
	// phpcs:enable

	/**
	 * Checks the permission for the 'delete' method in the REST API.
	 *
	 * @return bool True if the permission check passes, false otherwise.
	 */
	public function delete_permission_check(): bool {
		return false;
	}

	/**
	 * Retrieve the arguments for the get method.
	 *
	 * @return array The arguments for the get method.
	 */
	private function get_args(): array {
		$args = $this->default_args();
		return array_merge( $args, $this->additional_get_args() );
	}

	/**
	 * Returns an array of arguments for the POST request.
	 *
	 * @return array An array of arguments for the POST request.
	 */
	private function post_args(): array {
		$args = $this->default_args();
		return array_merge( $args, $this->additional_post_args() );
	}

	/**
	 * Retrieves the arguments passed in the PUT request.
	 *
	 * @return array The arguments passed in the PUT request.
	 */
	private function put_args(): array {
		$args = $this->default_args();
		return array_merge( $args, $this->additional_put_args() );
	}

	/**
	 * Checks if the request method is PATCH and returns the arguments for handling the patch request.
	 *
	 * @return array Returns the arguments for handling the patch request.
	 */
	private function patch_args(): array {
		$args = $this->default_args();
		return array_merge( $args, $this->additional_patch_args() );
	}

	/**
	 * Returns the arguments to be used in the DELETE request.
	 *
	 * @return array The arguments to be used in the DELETE request.
	 */
	private function delete_args(): array {
		$args = $this->default_args();
		return array_merge( $args, $this->additional_delete_args() );
	}

	/**
	 * Retrieve the arguments for the get method.
	 *
	 * @return array The arguments for the get method.
	 */
	protected function additional_get_args(): array {
		return array();
	}

	/**
	 * Returns an array of arguments for the POST request.
	 *
	 * @return array An array of arguments for the POST request.
	 */
	protected function additional_post_args(): array {
		return array();
	}

	/**
	 * Retrieves the arguments passed in the PUT request.
	 *
	 * @return array The arguments passed in the PUT request.
	 */
	protected function additional_put_args(): array {
		return array();
	}

	/**
	 * Checks if the request method is PATCH and returns the arguments for handling the patch request.
	 *
	 * @return array Returns the arguments for handling the patch request.
	 */
	protected function additional_patch_args(): array {
		return array();
	}

	/**
	 * Returns the arguments to be used in the DELETE request.
	 *
	 * @return array The arguments to be used in the DELETE request.
	 */
	protected function additional_delete_args(): array {
		return array();
	}

	/**
	 * Defines the default arguments for the REST API method.
	 *
	 * @return array An array of default arguments for the REST API method.
	 */
	protected function default_args(): array {
		return array();
	}

	/**
	 * Retrieves the schema for the REST API endpoint.
	 *
	 * @return array The schema for the REST API endpoint.
	 */
	public function get_schema(): array {
		if ( ! isset( $this->schema ) ) {
			$this->schema = array(
				'$schema'    => 'http://json-schema.org/draft-04/schema#',
				'title'      => $this->route,
				'type'       => 'object',
				'properties' => static::get_schema_properties(),
			);
		}

		return $this->schema;
	}

	/**
	 * Retrieves the schema properties for the current REST API endpoint.
	 *
	 * @return array The schema properties for the current REST API endpoint.
	 */
	abstract protected static function get_schema_properties(): array;
}
