<?php
/**
 * REST models controller class
 *
 * @package Leaves_And_Love\Plugin_Lib
 * @since 1.0.0
 */

namespace Leaves_And_Love\Plugin_Lib\DB_Objects;

use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

if ( ! class_exists( 'Leaves_And_Love\Plugin_Lib\DB_Objects\REST_Models_Controller' ) ) :

	/**
	 * Class to access models via the REST API.
	 *
	 * @since 1.0.0
	 */
	abstract class REST_Models_Controller extends WP_REST_Controller {
		/**
		 * The manager instance.
		 *
		 * @since 1.0.0
		 * @var Manager
		 */
		protected $manager;

		/**
		 * Model types controller class name.
		 *
		 * @since 1.0.0
		 * @var string
		 */
		protected $types_controller_class_name = REST_Model_Types_Controller::class;

		/**
		 * Model statuses controller class name.
		 *
		 * @since 1.0.0
		 * @var string
		 */
		protected $statuses_controller_class_name = REST_Model_Statuses_Controller::class;

		/**
		 * REST model types controller.
		 *
		 * @since 1.0.0
		 * @var REST_Model_Types_Controller
		 */
		protected $types_controller;

		/**
		 * REST model statuses controller.
		 *
		 * @since 1.0.0
		 * @var REST_Model_Statuses_Controller
		 */
		protected $statuses_controller;

		/**
		 * Constructor.
		 *
		 * @since 1.0.0
		 *
		 * @param Manager $manager The manager instance.
		 */
		public function __construct( $manager ) {
			$this->manager = $manager;

			$prefix = $this->manager->get_prefix();
			if ( '_' === substr( $prefix, -1 ) ) {
				$prefix = substr( $prefix, 0, -1 );
			}

			$this->namespace = $prefix;
			$this->rest_base = $this->manager->get_plural_slug();

			if ( method_exists( $this->manager, 'get_type_property' ) ) {
				$class_name = $this->types_controller_class_name;

				$this->types_controller = new $class_name( $this->manager );
			}

			if ( method_exists( $this->manager, 'get_status_property' ) ) {
				$class_name = $this->statuses_controller_class_name;

				$this->statuses_controller = new $class_name( $this->manager );
			}
		}

		/**
		 * Registers the routes for the objects of the controller.
		 *
		 * @since 1.0.0
		 */
		public function register_routes() {
			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base,
				array(
					array(
						'methods'             => WP_REST_Server::READABLE,
						'callback'            => array( $this, 'get_items' ),
						'permission_callback' => array( $this, 'get_items_permissions_check' ),
						'args'                => $this->get_collection_params(),
					),
					array(
						'methods'             => WP_REST_Server::CREATABLE,
						'callback'            => array( $this, 'create_item' ),
						'permission_callback' => array( $this, 'create_item_permissions_check' ),
						'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);

			$primary_property = $this->manager->get_primary_property();

			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/(?P<' . $primary_property . '>[\d]+)',
				array(
					'args'   => array(
						$primary_property => array(
							'description' => $this->manager->get_message( 'rest_item_id_description' ),
							'type'        => 'integer',
						),
					),
					array(
						'methods'             => WP_REST_Server::READABLE,
						'callback'            => array( $this, 'get_item' ),
						'permission_callback' => array( $this, 'get_item_permissions_check' ),
						'args'                => array(
							'context' => $this->get_context_param( array( 'default' => 'view' ) ),
						),
					),
					array(
						'methods'             => WP_REST_Server::EDITABLE,
						'callback'            => array( $this, 'update_item' ),
						'permission_callback' => array( $this, 'update_item_permissions_check' ),
						'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
					),
					array(
						'methods'             => WP_REST_Server::DELETABLE,
						'callback'            => array( $this, 'delete_item' ),
						'permission_callback' => array( $this, 'delete_item_permissions_check' ),
						'args'                => array(),
					),
					'schema' => array( $this, 'get_public_item_schema' ),
				)
			);

			if ( isset( $this->types_controller ) ) {
				$this->types_controller->register_routes();
			}

			if ( isset( $this->statuses_controller ) ) {
				$this->statuses_controller->register_routes();
			}
		}

		/**
		 * Checks if a given request has access to read models.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
		 */
		public function get_items_permissions_check( $request ) {
			$capabilities = $this->manager->capabilities();

			if ( 'edit' === $request['context'] ) {
				if ( ! $capabilities || ! $capabilities->user_can_edit() ) {
					return new WP_Error( 'rest_forbidden_context', $this->manager->get_message( 'rest_cannot_edit_items' ), array( 'status' => rest_authorization_required_code() ) );
				}

				return true;
			}

			if ( $this->manager->is_public() ) {
				if ( ! method_exists( $this->manager, 'get_status_property' ) ) {
					return true;
				}

				$public_statuses = $this->manager->statuses()->get_public();

				if ( ! empty( $public_statuses ) ) {
					$public = true;
					foreach ( (array) $request[ $this->manager->get_status_property() ] as $status ) {
						if ( ! in_array( $status, $public_statuses, true ) ) {
							$public = false;
							break;
						}
					}

					if ( $public ) {
						return true;
					}
				}
			}

			if ( ! $capabilities || ! $capabilities->user_can_read() ) {
				return new WP_Error( 'rest_cannot_read_items', $this->manager->get_message( 'rest_cannot_read_items' ), array( 'status' => rest_authorization_required_code() ) );
			}

			if ( method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				if ( ! empty( $request[ $author_property ] ) && get_current_user_id() !== $request[ $author_property ] && ( ! $capabilities || ! $capabilities->current_user_can( 'read_others_items' ) ) ) {
					return new WP_Error( 'rest_cannot_read_others_items', $this->manager->get_message( 'rest_cannot_read_others_items' ), array( 'status' => rest_authorization_required_code() ) );
				}
			}

			return true;
		}

		/**
		 * Retrieves a collection of models.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function get_items( $request ) {
			$registered_args = $this->get_collection_params();

			$args = array();

			$special_args = array(
				'per_page' => 10,
				'page'     => 1,
				'orderby'  => $this->manager->get_primary_property(),
				'order'    => 'asc',
			);

			$date_query     = array();
			$date_query_map = array();

			if ( method_exists( $this->manager, 'get_all_date_properties' ) ) {
				$date_properties = $this->manager->get_all_date_properties();
			}

			foreach ( $registered_args as $property => $params ) {
				if ( ! isset( $request[ $property ] ) ) {
					continue;
				}

				if ( isset( $date_properties ) && isset( $params['format'] ) && 'date-time' === $params['format'] ) {
					$date_column = '';
					$mode        = '';

					if ( '_before' === substr( $property, -7 ) ) {
						$date_column = substr( $property, 0, -7 );
						$mode        = 'before';
					} elseif ( '_after' === substr( $property, -6 ) ) {
						$date_column = substr( $property, -6 );
						$mode        = 'after';
					}

					if ( ! empty( $date_column ) && ! empty( $mode ) && in_array( $date_column, $date_properties, true ) ) {
						if ( ! isset( $date_query_map[ $date_column ] ) ) {
							$date_query_map[ $date_column ]                          = count( $date_query );
							$date_query[ $date_query_map[ $date_column ] ]['column'] = $date_column;
						}

						$date_query[ $date_query_map[ $date_column ] ][ $mode ] = $request[ $property ];
						continue;
					}
				}

				if ( isset( $special_args[ $property ] ) ) {
					$special_args[ $property ] = $request[ $property ];
				} else {
					$args[ $property ] = $request[ $property ];
				}
			}

			if ( ! empty( $date_query ) ) {
				$args['date_query'] = $date_query;
			}

			$args['number']  = $special_args['per_page'];
			$args['offset']  = ( $special_args['page'] - 1 ) * $special_args['per_page'];
			$args['orderby'] = array( $special_args['orderby'] => $special_args['order'] );

			if ( ! $this->manager->is_public() && method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				if ( ! $capabilities || ! $capabilities->current_user_can( 'read_others_items' ) ) {
					$args[ $author_property ] = get_current_user_id();
				}
			}

			$collection = $this->manager->query( $args );

			$results = array();

			foreach ( $collection as $model ) {
				$data      = $this->prepare_item_for_response( $model, $request );
				$results[] = $this->prepare_response_for_collection( $data );
			}

			$page      = $special_args['page'];
			$total     = $collection->get_total();
			$max_pages = ceil( $total / $special_args['per_page'] );

			if ( $page > $max_pages && $total > 0 ) {
				return new WP_Error( 'rest_invalid_page_number', $this->manager->get_message( 'rest_invalid_page_number' ), array( 'status' => 400 ) );
			}

			$response = rest_ensure_response( $results );

			$response->header( 'X-WP-Total', (int) $total );
			$response->header( 'X-WP-TotalPages', (int) $max_pages );

			$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

			if ( $page > 1 ) {
				$prev_page = $page - 1;
				if ( $prev_page > $max_pages ) {
					$prev_page = $max_pages;
				}

				$response->link_header( 'prev', add_query_arg( 'page', $prev_page, $base ) );
			}

			if ( $max_pages > $page ) {
				$next_page = $page + 1;

				$response->link_header( 'next', add_query_arg( 'next', $next_page, $base ) );
			}

			return $response;
		}

		/**
		 * Checks if a given request has access to read a model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return bool|WP_Error True if the request has read access for the model, WP_Error object otherwise.
		 */
		public function get_item_permissions_check( $request ) {
			$primary_property = $this->manager->get_primary_property();

			$model = $this->manager->get( $request[ $primary_property ] );
			if ( ! $model ) {
				return new WP_Error( 'rest_invalid_id', $this->manager->get_message( 'rest_invalid_id' ), array( 'status' => 404 ) );
			}

			$capabilities = $this->manager->capabilities();

			if ( 'edit' === $request['context'] ) {
				if ( ! $capabilities || ! $capabilities->user_can_edit( null, $request[ $primary_property ] ) ) {
					return new WP_Error( 'rest_forbidden_context', $this->manager->get_message( 'rest_cannot_edit_item' ), array( 'status' => rest_authorization_required_code() ) );
				}

				return true;
			}

			if ( $this->manager->is_public() ) {
				if ( ! method_exists( $this->manager, 'get_status_property' ) ) {
					return true;
				}

				$public_statuses = $this->manager->statuses()->get_public();
				$status_property = $this->manager->get_status_property();

				if ( in_array( $model->$status_property, $public_statuses, true ) ) {
					return true;
				}
			}

			if ( ! $capabilities || ! $capabilities->user_can_read( null, $request[ $primary_property ] ) ) {
				return new WP_Error( 'rest_cannot_read_item', $this->manager->get_message( 'rest_cannot_read_item' ), array( 'status' => rest_authorization_required_code() ) );
			}

			return true;
		}

		/**
		 * Retrieves a single model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function get_item( $request ) {
			$primary_property = $this->manager->get_primary_property();

			$model = $this->manager->get( $request[ $primary_property ] );
			if ( ! $model ) {
				return new WP_Error( 'rest_invalid_id', $this->manager->get_message( 'rest_invalid_id' ), array( 'status' => 404 ) );
			}

			$data     = $this->prepare_item_for_response( $model, $request );
			$response = rest_ensure_response( $data );

			$permalink = $model->get_permalink();
			if ( ! empty( $permalink ) ) {
				$response->link_header( 'alternate', $permalink, array( 'type' => 'text/html' ) );
			}

			return $response;
		}

		/**
		 * Checks if a given request has access to create a model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return true|WP_Error True if the request has access to create models, WP_Error object otherwise.
		 */
		public function create_item_permissions_check( $request ) {
			$primary_property = $this->manager->get_primary_property();

			if ( ! empty( $request[ $primary_property ] ) ) {
				return new WP_Error( 'rest_item_exists', $this->manager->get_message( 'rest_item_exists' ), array( 'status' => 400 ) );
			}

			$capabilities = $this->manager->capabilities();

			if ( ! $capabilities || ! $capabilities->user_can_create() ) {
				return new WP_Error( 'rest_cannot_create_items', $this->manager->get_message( 'rest_cannot_create_items' ), array( 'status' => rest_authorization_required_code() ) );
			}

			if ( method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				if ( ! empty( $request[ $author_property ] ) && get_current_user_id() !== $request[ $author_property ] && ( ! $capabilities || ! $capabilities->current_user_can( 'edit_others_items' ) ) ) {
					return new WP_Error( 'rest_cannot_create_others_items', $this->manager->get_message( 'rest_cannot_create_others_items' ), array( 'status' => rest_authorization_required_code() ) );
				}
			}

			return true;
		}

		/**
		 * Creates a single model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function create_item( $request ) {
			$primary_property = $this->manager->get_primary_property();

			if ( ! empty( $request[ $primary_property ] ) ) {
				return new WP_Error( 'rest_item_exists', $this->manager->get_message( 'rest_item_exists' ), array( 'status' => 400 ) );
			}

			$model = $this->prepare_item_for_database( $request );
			if ( is_wp_error( $model ) ) {
				return $model;
			}

			$result = $model->sync_upstream();

			if ( is_wp_error( $result ) ) {
				if ( 'db_insert_error' === $result->get_error_code() ) {
					$result->add_data( array( 'status' => 500 ) );
				} else {
					$result->add_data( array( 'status' => 400 ) );
				}

				return $result;
			}

			$request->set_param( 'context', 'edit' );

			$response = $this->prepare_item_for_response( $model, $request );
			$response = rest_ensure_response( $response );

			$response->set_status( 201 );
			$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $model->$primary_property ) ) );

			return $response;
		}

		/**
		 * Checks if a given request has access to update a model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
		 */
		public function update_item_permissions_check( $request ) {
			$primary_property = $this->manager->get_primary_property();

			$model = $this->manager->get( $request[ $primary_property ] );
			if ( ! $model ) {
				return new WP_Error( 'rest_invalid_id', $this->manager->get_message( 'rest_invalid_id' ), array( 'status' => 404 ) );
			}

			$capabilities = $this->manager->capabilities();

			if ( ! $capabilities || ! $capabilities->user_can_edit( null, $request[ $primary_property ] ) ) {
				return new WP_Error( 'rest_cannot_edit_item', $this->manager->get_message( 'rest_cannot_edit_item' ), array( 'status' => rest_authorization_required_code() ) );
			}

			if ( method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				if ( ! empty( $request[ $author_property ] ) && get_current_user_id() !== $request[ $author_property ] && ( ! $capabilities || ! $capabilities->current_user_can( 'edit_others_items' ) ) ) {
					return new WP_Error( 'rest_cannot_edit_others_item', $this->manager->get_message( 'rest_cannot_edit_others_item' ), array( 'status' => rest_authorization_required_code() ) );
				}
			}

			return true;
		}

		/**
		 * Updates a single model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function update_item( $request ) {
			$model = $this->prepare_item_for_database( $request );
			if ( is_wp_error( $model ) ) {
				return $model;
			}

			$result = $model->sync_upstream();

			if ( is_wp_error( $result ) ) {
				if ( 'db_update_error' === $result->get_error_code() ) {
					$result->add_data( array( 'status' => 500 ) );
				} else {
					$result->add_data( array( 'status' => 400 ) );
				}

				return $result;
			}

			$request->set_param( 'context', 'edit' );

			$response = $this->prepare_item_for_response( $model, $request );
			$response = rest_ensure_response( $response );

			return $response;
		}

		/**
		 * Checks if a given request has access to delete a model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
		 */
		public function delete_item_permissions_check( $request ) {
			$primary_property = $this->manager->get_primary_property();

			$model = $this->manager->get( $request[ $primary_property ] );
			if ( ! $model ) {
				return new WP_Error( 'rest_invalid_id', $this->manager->get_message( 'rest_invalid_id' ), array( 'status' => 404 ) );
			}

			$capabilities = $this->manager->capabilities();

			if ( ! $capabilities || ! $capabilities->user_can_delete( null, $request[ $primary_property ] ) ) {
				return new WP_Error( 'rest_cannot_delete_item', $this->manager->get_message( 'rest_cannot_delete_item' ), array( 'status' => rest_authorization_required_code() ) );
			}

			return true;
		}

		/**
		 * Deletes a single model.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Full details about the request.
		 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
		 */
		public function delete_item( $request ) {
			$primary_property = $this->manager->get_primary_property();

			$model = $this->manager->get( $request[ $primary_property ] );
			if ( ! $model ) {
				return new WP_Error( 'rest_invalid_id', $this->manager->get_message( 'rest_invalid_id' ), array( 'status' => 404 ) );
			}

			$request->set_param( 'context', 'edit' );

			$previous = $this->prepare_item_for_response( $model, $request );

			$result = $model->delete();

			if ( is_wp_error( $result ) ) {
				if ( 'db_delete_error' === $result->get_error_code() ) {
					$result->add_data( array( 'status' => 500 ) );
				} else {
					$result->add_data( array( 'status' => 400 ) );
				}

				return $result;
			}

			$response = new WP_REST_Response();
			$response->set_data(
				array(
					'deleted'  => true,
					'previous' => $previous->get_data(),
				)
			);

			return $response;
		}

		/**
		 * Prepares a single model for create or update.
		 *
		 * @since 1.0.0
		 *
		 * @param WP_REST_Request $request Request object.
		 * @return Leaves_And_Love\Plugin_Lib\DB_Objects\Model|WP_Error The prepared model, or WP_Error object on failure.
		 */
		protected function prepare_item_for_database( $request ) {
			$primary_property = $this->manager->get_primary_property();

			if ( isset( $request[ $primary_property ] ) ) {
				$model = $this->manager->get( $request[ $primary_property ] );
				if ( ! $model ) {
					return new WP_Error( 'rest_invalid_id', $this->manager->get_message( 'rest_invalid_id' ), array( 'status' => 404 ) );
				}
			} else {
				$model = $this->manager->create();
			}

			$schema = $this->get_item_schema();

			foreach ( $schema['properties'] as $property => $params ) {
				if ( ! empty( $params['readonly'] ) ) {
					continue;
				}

				if ( ! isset( $request[ $property ] ) ) {
					continue;
				}

				if ( isset( $params['format'] ) && 'date-time' === $params['format'] ) {
					if ( '_gmt' === substr( $property, -4 ) ) {
						$date_data = rest_get_date_with_gmt( $request[ $property ], true );
						if ( ! empty( $date_data ) ) {
							$model->$property = $date_data[1];

							$property_no_gmt = substr( $property, 0, -4 );
							if ( isset( $model->$property_no_gmt ) ) {
								$model->$property_no_gmt = $date_data[0];
							}
						}
					} else {
						$date_data = rest_get_date_with_gmt( $request[ $property ] );
						if ( ! empty( $date_data ) ) {
							$model->$property = $date_data[0];

							$property_gmt = $property . '_gmt';
							if ( isset( $model->$property_gmt ) ) {
								$model->$property_gmt = $date_data[1];
							}
						}
					}
				} else {
					$model->$property = $request[ $property ];
				}
			}

			return $model;
		}

		/**
		 * Prepares a single model output for response.
		 *
		 * @since 1.0.0
		 *
		 * @param Model           $model   Model object.
		 * @param WP_REST_Request $request Request object.
		 * @return WP_REST_Response Response object.
		 */
		public function prepare_item_for_response( $model, $request ) {
			$schema = $this->get_item_schema();

			if ( method_exists( $this, 'get_fields_for_response' ) ) {
				$fields = $this->get_fields_for_response( $request );
			} else {
				$fields = array_keys( $schema['properties'] );
			}

			$data = array();

			foreach ( $fields as $property ) {
				$params = $schema['properties'][ $property ] ?? [];
				if ( isset( $params['format'] ) && 'date-time' === $params['format'] ) {
					$data[ $property ] = $this->prepare_date_for_response( $model->$property );
				} else {
					$data[ $property ] = $model->$property;
				}
			}

			$context = ! empty( $request['context'] ) ? $request['context'] : 'view';

			$data = $this->filter_response_by_context( $data, $context );

			$response = rest_ensure_response( $data );

			$response->add_links( $this->prepare_links( $model ) );

			return $response;
		}

		/**
		 * Prepares a date for response.
		 *
		 * @since 1.0.0
		 *
		 * @param string $date The datetime string to prepare.
		 * @return string|null ISO8601/RFC3339 formatted datetime.
		 */
		protected function prepare_date_for_response( $date ) {
			if ( '0000-00-00 00:00:00' === $date ) {
				return null;
			}

			return mysql_to_rfc3339( $date );
		}

		/**
		 * Prepares links for the request.
		 *
		 * @since 1.0.0
		 *
		 * @param Model $model Model object.
		 * @return array Links for the given model.
		 */
		protected function prepare_links( $model ) {
			$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );

			$primary_property = $this->manager->get_primary_property();

			$links = array(
				'self'       => array(
					'href' => rest_url( trailingslashit( $base ) . $model->$primary_property ),
				),
				'collection' => array(
					'href' => rest_url( $base ),
				),
			);

			if ( method_exists( $this->manager, 'get_type_property' ) && isset( $this->types_controller ) ) {
				$type_property = $this->manager->get_type_property();

				$type = $model->$type_property;

				if ( ! empty( $type ) ) {
					$links['about'] = array(
						'href' => rest_url( trailingslashit( $base ) . 'types/' . $type ),
					);
				}
			}

			if ( method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				$author_id = $model->$author_property;
				if ( ! empty( $author_id ) ) {
					$links['author'] = array(
						'href'       => rest_url( 'wp/v2/users/' . $author_id ),
						'embeddable' => true,
					);
				}
			}

			return $links;
		}

		/**
		 * Retrieves the model's schema, conforming to JSON Schema.
		 *
		 * @since 1.0.0
		 *
		 * @return array Model schema data.
		 */
		public function get_item_schema() {
			$schema = array(
				'$schema'    => 'http://json-schema.org/schema#',
				'title'      => $this->manager->get_singular_slug(),
				'type'       => 'object',
				'properties' => array(),
			);

			$primary_property = $this->manager->get_primary_property();

			$schema['properties'][ $primary_property ] = array(
				'description' => $this->manager->get_message( 'rest_item_id_description' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit', 'embed' ),
				'readonly'    => true,
			);

			if ( method_exists( $this->manager, 'get_slug_property' ) ) {
				$slug_property = $this->manager->get_slug_property();

				$schema['properties'][ $slug_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_slug_description' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'sanitize_slug' ),
					),
				);
			}

			if ( method_exists( $this->manager, 'get_title_property' ) ) {
				$title_property = $this->manager->get_title_property();

				$schema['properties'][ $title_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_title_description' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				);
			}

			if ( method_exists( $this->manager, 'get_content_property' ) ) {
				$content_property = $this->manager->get_content_property();

				$schema['properties'][ $content_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_content_description' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				);
			}

			if ( method_exists( $this->manager, 'get_type_property' ) ) {
				$type_property = $this->manager->get_type_property();

				$schema['properties'][ $type_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_type_description' ),
					'type'        => 'string',
					'enum'        => array_keys( $this->manager->types()->query() ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'arg_options' => array(
						'default'  => $this->manager->types()->get_default(),
						'required' => true,
					),
				);
			}

			if ( method_exists( $this->manager, 'get_status_property' ) ) {
				$status_property = $this->manager->get_status_property();

				$schema['properties'][ $status_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_status_description' ),
					'type'        => 'string',
					'enum'        => array_keys( $this->manager->statuses()->query() ),
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'sanitize_set_status' ),
						'default'           => $this->manager->statuses()->get_default(),
						'required'          => true,
					),
				);
			}

			if ( method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				$schema['properties'][ $author_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_author_description' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'sanitize_author' ),
					),
				);
			}

			if ( method_exists( $this->manager, 'get_date_property' ) ) {
				$date_property = $this->manager->get_date_property();

				$schema['properties'][ $date_property ] = array(
					'description' => $this->manager->get_message( 'rest_item_date_description' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit', 'embed' ),
					/* Sanitization is handled directly in the prepare methods. */
				);

				foreach ( $this->manager->get_secondary_date_properties() as $secondary_date_property ) {
					$description = $this->manager->get_message( 'rest_item_date_' . $secondary_date_property . '_description' );
					if ( empty( $description ) ) {
						continue;
					}

					$schema['properties'][ $secondary_date_property ] = array(
						'description' => $description,
						'type'        => 'string',
						'format'      => 'date-time',
						'context'     => array( 'view', 'edit', 'embed' ),
						/* Sanitization is handled directly in the prepare methods. */
					);
				}
			}

			return $schema;
		}

		/**
		 * Retrieves the query params for the models collection.
		 *
		 * @since 1.0.0
		 *
		 * @return array Collection parameters.
		 */
		public function get_collection_params() {
			$query_params = parent::get_collection_params();

			$primary_property = $this->manager->get_primary_property();
			$query_object     = $this->manager->create_query_object();

			$query_params['context']['default'] = 'view';

			$search_fields = $query_object->get_search_fields();
			if ( empty( $search_fields ) ) {
				unset( $query_params['search'] );
			}

			$query_params['include'] = array(
				'description' => $this->manager->get_message( 'rest_collection_include_description' ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'default'     => array(),
			);

			$query_params['exclude'] = array(
				'description' => $this->manager->get_message( 'rest_collection_exclude_description' ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'default'     => array(),
			);

			$query_params['orderby'] = array(
				'description' => $this->manager->get_message( 'rest_collection_orderby_description' ),
				'type'        => 'string',
				'default'     => $primary_property,
				'enum'        => $query_object->get_valid_orderby_fields(),
			);

			$query_params['order'] = array(
				'description' => $this->manager->get_message( 'rest_collection_order_description' ),
				'type'        => 'string',
				'default'     => 'asc',
				'enum'        => array( 'asc', 'desc' ),
			);

			if ( method_exists( $this->manager, 'get_slug_property' ) ) {
				$slug_property = $this->manager->get_slug_property();

				$query_params[ $slug_property ] = array(
					'description'       => $this->manager->get_message( 'rest_collection_slug_description' ),
					'type'              => 'array',
					'items'             => array(
						'type' => 'string',
					),
					'sanitize_callback' => 'wp_parse_slug_list',
				);
			}

			if ( method_exists( $this->manager, 'get_title_property' ) ) {
				$title_property = $this->manager->get_title_property();

				$query_params[ $title_property ] = array(
					'description'       => $this->manager->get_message( 'rest_collection_title_description' ),
					'type'              => 'string',
					'sanitize_callback' => 'strip_tags',
				);
			}

			if ( method_exists( $this->manager, 'get_type_property' ) ) {
				$type_property = $this->manager->get_type_property();

				$query_params[ $type_property ] = array(
					'description'       => $this->manager->get_message( 'rest_collection_type_description' ),
					'type'              => 'array',
					'items'             => array(
						'type' => 'string',
						'enum' => array_keys( $this->manager->types()->query() ),
					),
					'default'           => $this->manager->types()->get_public(),
					'sanitize_callback' => array( $this, 'sanitize_types' ),
				);
			}

			if ( method_exists( $this->manager, 'get_status_property' ) ) {
				$status_property = $this->manager->get_status_property();

				$query_params[ $status_property ] = array(
					'description'       => $this->manager->get_message( 'rest_collection_status_description' ),
					'type'              => 'array',
					'items'             => array(
						'type' => 'string',
						'enum' => array_keys( $this->manager->statuses()->query() ),
					),
					'default'           => $this->manager->statuses()->get_public(),
					'sanitize_callback' => array( $this, 'sanitize_statuses' ),
				);
			}

			if ( method_exists( $this->manager, 'get_author_property' ) ) {
				$author_property = $this->manager->get_author_property();

				$query_params[ $author_property ] = array(
					'description'       => $this->manager->get_message( 'rest_collection_author_description' ),
					'type'              => 'integer',
					'sanitize_callback' => array( $this, 'sanitize_author' ),
				);
			}

			if ( method_exists( $this->manager, 'get_date_property' ) ) {
				$date_property = $this->manager->get_date_property();

				$query_params[ $date_property . '_after' ] = array(
					'description' => $this->manager->get_message( 'rest_collection_date_after_description' ),
					'type'        => 'string',
					'format'      => 'date-time',
				);

				$query_params[ $date_property . '_before' ] = array(
					'description' => $this->manager->get_message( 'rest_collection_date_before_description' ),
					'type'        => 'string',
					'format'      => 'date-time',
				);

				foreach ( $this->manager->get_secondary_date_properties() as $secondary_date_property ) {
					$after_description = $this->manager->get_message( 'rest_collection_date_' . $secondary_date_property . '_after_description' );
					if ( ! empty( $after_description ) ) {
						$query_params[ $secondary_date_property . '_after' ] = array(
							'description' => $after_description,
							'type'        => 'string',
							'format'      => 'date-time',
						);
					}

					$before_description = $this->manager->get_message( 'rest_collection_date_' . $secondary_date_property . '_before_description' );
					if ( ! empty( $before_description ) ) {
						$query_params[ $secondary_date_property . '_before' ] = array(
							'description' => $before_description,
							'type'        => 'string',
							'format'      => 'date-time',
						);
					}
				}
			}

			return $query_params;
		}

		/**
		 * Sanitizes the model author.
		 *
		 * @since 1.0.0
		 *
		 * @param string          $author    The author ID.
		 * @param WP_REST_Request $request   Full details about the request.
		 * @param string          $parameter Additional parameter to pass to validation.
		 * @return string|WP_Error Sanitized author ID, otherwise WP_Error object.
		 */
		public function sanitize_author( $author, $request, $parameter ) {
			$author = absint( $author );

			if ( get_current_user_id() !== $author ) {
				$user = get_userdata( $author );
				if ( ! $user ) {
					return new WP_Error( 'rest_invalid_author', $this->manager->get_message( 'rest_invalid_author' ), array( 'status' => 400 ) );
				}
			}

			return $author;
		}

		/**
		 * Sanitizes the model status to set.
		 *
		 * @since 1.0.0
		 *
		 * @param string          $status    The status to set.
		 * @param WP_REST_Request $request   Full details about the request.
		 * @param string          $parameter Additional parameter to pass to validation.
		 * @return string|WP_Error Sanitized status, otherwise WP_Error object.
		 */
		public function sanitize_set_status( $status, $request, $parameter ) {
			$capabilities = $this->manager->capabilities();

			$public_statuses = $this->manager->statuses()->get_public();

			if ( in_array( $status, $public_statuses, true ) ) {
				$id = isset( $request['id'] ) ? absint( $request['id'] ) : null;

				if ( ! $capabilities || ! $capabilities->user_can_publish( null, $id ) ) {
					return new WP_Error( 'rest_cannot_publish_item', $this->manager->get_message( 'rest_cannot_publish_item' ), rest_authorization_required_code() );
				}
			}

			return $status;
		}

		/**
		 * Sanitizes and validates the list of model statuses.
		 *
		 * @since 1.0.0
		 *
		 * @param string|array    $statuses  One or more model statuses.
		 * @param WP_REST_Request $request   Full details about the request.
		 * @param string          $parameter Additional parameter to pass to validation.
		 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
		 */
		public function sanitize_statuses( $statuses, $request, $parameter ) {
			$capabilities = $this->manager->capabilities();

			$public_statuses = $this->manager->statuses()->get_public();

			$statuses = wp_parse_slug_list( $statuses );

			foreach ( $statuses as $status ) {
				if ( in_array( $status, $public_statuses, true ) ) {
					continue;
				}

				if ( ! $capabilities || ! $capabilities->user_can_edit() ) {
					return new WP_Error( 'rest_cannot_view_status', $this->manager->get_message( 'rest_cannot_view_status' ), rest_authorization_required_code() );
				}

				$result = rest_validate_request_arg( $status, $request, $parameter );
				if ( is_wp_error( $result ) ) {
					return $result;
				}
			}

			return $statuses;
		}

		/**
		 * Sanitizes and validates the list of model types.
		 *
		 * @since 1.0.0
		 *
		 * @param string|array    $types     One or more model types.
		 * @param WP_REST_Request $request   Full details about the request.
		 * @param string          $parameter Additional parameter to pass to validation.
		 * @return array|WP_Error A list of valid types, otherwise WP_Error object.
		 */
		public function sanitize_types( $types, $request, $parameter ) {
			$capabilities = $this->manager->capabilities();

			$public_types = $this->manager->types()->get_public();

			$types = wp_parse_slug_list( $types );

			foreach ( $types as $type ) {
				if ( in_array( $type, $public_types, true ) ) {
					continue;
				}

				if ( ! $capabilities || ! $capabilities->user_can_edit() ) {
					return new WP_Error( 'rest_cannot_view_type', $this->manager->get_message( 'rest_cannot_view_type' ), rest_authorization_required_code() );
				}

				$result = rest_validate_request_arg( $type, $request, $parameter );
				if ( is_wp_error( $result ) ) {
					return $result;
				}
			}

			return $types;
		}
	}

endif;
