<?php
/**
 * Endpoints to retrieve information about profile fields
 *
 * @package Nisje
 */

namespace Dekode\Nisje\Components\Rest;

defined( 'ABSPATH' ) || die( 'Shame on you' );

/**
 * Xprofile Fields Rest Class
 */
class XProfile_Fields_Controller extends \WP_REST_Controller {

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->namespace = nisje_get_rest_namespace();
		$this->rest_base = buddypress()->profile->id . '/fields';
		$this->hook_base = strtolower( buddypress()->profile->id . '_fields' );
	}

	/**
	 * Register the routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace, '/' . $this->rest_base, [
				[
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => [ $this, 'update_items' ],
					'permission_callback' => [ $this, 'update_items_permissions_check' ],
					'args'                => $this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
				],
				'schema' => [ $this, 'get_public_item_schema' ],
			]
		);

		register_rest_route(
			$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
				'args'   => [
					'id' => [
						'description' => esc_html__( 'Unique identifier for the object.', 'nisje' ),
						'type'        => 'integer',
					],
				],
				[
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => [ $this, 'get_item' ],
					'permission_callback' => [ $this, 'get_item_permissions_check' ],
					'args'                => $this->get_item_params(),
				],
				'schema' => [ $this, 'get_public_item_schema' ],
			]
		);
	}

	/**
	 * Check if a given request has access to create a group.
	 *
	 * @param  WP_REST_Request $request Full details about the request.
	 * @return WP_Error|boolean
	 */
	public function update_items_permissions_check( $request ) {
		$auth = nisje_validate_rest_authentication( $this->hook_base, 'update_item' );
		if ( is_wp_error( $auth ) ) {
			return $auth;
		}

		$user_id = bp_loggedin_user_id();
		if ( ! empty( $request['user_id'] ) ) {
			$user_id = (int) $request['user_id'];
		}

		$user_check = nisje_validate_current_user( $user_id );
		if ( is_wp_error( $user_check ) ) {
			return $user_check;
		}

		return true;
	}

	/**
	 * Update user items.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function update_items( $request ) {
		$user_id = bp_loggedin_user_id();
		if ( ! empty( $request['user_id'] ) ) {
			$user_id = (int) $request['user_id'];
		}

		$fields_obj = $this->prepare_items_for_database( $request );
		if ( is_wp_error( $fields_obj ) ) {
			return $fields_obj;
		}

		// field_ids.
		$posted_field_ids = wp_parse_id_list( $fields_obj->fields_ids );
		$is_required      = [];

		$fields     = $fields_obj->fields;
		$fields_ids = [];

		$errors = false;
		// Loop through the posted fields formatting any datebox values
		// then validate the field.
		foreach ( (array) $posted_field_ids as $field_id ) {
			if ( isset( $fields[ 'field_' . $field_id ] ) ) {
				if ( ! empty( $fields[ 'field_' . $field_id . '_day' ] ) && ! empty( $fields[ 'field_' . $field_id . '_month' ] ) && ! empty( $fields[ 'field_' . $field_id . '_year' ] ) ) {
					// Concatenate the values.
					$date_value = $fields[ 'field_' . $field_id . '_day' ] . ' ' . $fields[ 'field_' . $field_id . '_month' ] . ' ' . $fields[ 'field_' . $field_id . '_year' ];
					// Turn the concatenated value into a timestamp.
					$fields[ 'field_' . $field_id ] = gmdate( 'Y-m-d H:i:s', strtotime( $date_value ) );
				}

				$is_required[ $field_id ] = xprofile_check_is_required_field( $field_id ) && ! bp_current_user_can( 'bp_moderate' );
				if ( $is_required[ $field_id ] && empty( $fields[ 'field_' . $field_id ] ) ) {
					$errors = true;
				}
			}
		}

		if ( $errors ) {
			return new \WP_Error( 'nisje_rest_missing_profile_fields', esc_html__( 'Please make sure you fill in all required fields in this profile field group before saving.', 'nisje' ), [ 'status' => rest_authorization_required_code() ] );
		} else {
			// Reset the errors var.
			$errors = false;

			// Now we've checked for required fields, lets save the values.
			$old_values = [];
			$new_values = [];
			foreach ( (array) $posted_field_ids as $field_id ) {

				// Certain types of fields (checkboxes, multiselects) may come through empty. Save them as an empty array so that they don't get overwritten by the default on the next edit.
				$value = isset( $fields[ 'field_' . $field_id ] ) ? $fields[ 'field_' . $field_id ] : '';

				$visibility_level = ! empty( $fields[ 'field_' . $field_id . '_visibility' ] ) ? $fields[ 'field_' . $field_id . '_visibility' ] : 'public';

				// Save the old and new values. They will be
				// passed to the filter and used to determine
				// whether an item should be posted.
				$old_values[ $field_id ] = [
					'value'      => xprofile_get_field_data( $field_id, $user_id ),
					'visibility' => xprofile_get_field_visibility_level( $field_id, $user_id ),
				];

				// Update the field data and visibility level.
				xprofile_set_field_visibility_level( $field_id, $user_id, $visibility_level );
				$field_updated = xprofile_set_field_data( $field_id, $user_id, $value, $is_required[ $field_id ] );
				$value         = xprofile_get_field_data( $field_id, $user_id );

				$new_values[ $field_id ] = [
					'value'      => $value,
					'visibility' => xprofile_get_field_visibility_level( $field_id, $user_id ),
				];

				if ( ! $field_updated ) {
					$errors = true;
				} else {

					/**
					 * Fires on each iteration of an XProfile field being saved with no error.
					 *
					 * @since 1.1.0
					 *
					 * @param int    $field_id ID of the field that was saved.
					 * @param string $value    Value that was saved to the field.
					 */
					do_action( 'xprofile_profile_field_data_updated', $field_id, $value );
				}

				$fields_ids[] = $field_id;
			}

			/**
			 * Fires after all XProfile fields have been saved for the current profile.
			 *
			 * @since 1.0.0
			 *
			 * @param int   $value            Displayed user ID.
			 * @param array $posted_field_ids Array of field IDs that were edited.
			 * @param bool  $errors           Whether or not any errors occurred.
			 * @param array $old_values       Array of original values before updated.
			 * @param array $new_values       Array of newly saved values after update.
			 */
			do_action( 'xprofile_updated_profile', $user_id, $posted_field_ids, $errors, $old_values, $new_values );

			if ( ! empty( $errors ) ) {
				return new \WP_Error( 'nisje_rest_accept_invite', esc_html__( 'The profile data was not saved. Please contact the administrator with error code 2401.', 'nisje' ), [ 'status' => 500 ] );
			}

			bp_delete_user_meta( $user_id, 'not_saved_profile' );
		}

		$schema = $this->get_item_schema();

		/**
		 * Fires after fields are saved.
		 */
		do_action( "nisje_rest_after_{$this->hook_base}", $fields_ids, $request );

		$request->set_param( 'context', 'edit' );
		$response = [ 'success' ];
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

		return $response;
	}

	/**
	 * Retrieve single xprofile field.
	 *
	 * @param WP_REST_Request $request  The Request.
	 * @return WP_REST_Request|WP_Error Plugin object data on success, WP_Error otherwise.
	 */
	public function get_item( $request ) {
		$profile_field_id = (int) $request['id'];

		$field = xprofile_get_field( $profile_field_id );

		// @TODO: Visibility doesn't protect field values in this function.
		if ( ! empty( $request['user_id'] ) ) {
			$field->data        = new \stdClass();
			$field->data->value = xprofile_get_field_data( $profile_field_id, $request['user_id'] );
			// Set 'fetch_field_data' to true so that the data is included in the response.
			$request['fetch_field_data'] = true;
		}

		if ( empty( $profile_field_id ) || empty( $field->id ) ) {
			return new \WP_Error( 'bp_rest_invalid_field_id', esc_html__( 'Invalid resource id.', 'nisje' ), [ 'status' => 404 ] );
		} else {
			$retval = $this->prepare_item_for_response( $field, $request );
		}

		return rest_ensure_response( $retval );
	}

	/**
	 * Prepare a fields for update
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_Error|stdClass $prepared_group group object.
	 */
	protected function prepare_items_for_database( $request ) {
		$prepared_fields = new \stdClass();
		$schema          = $this->get_item_schema();

		if ( ! empty( $schema['properties']['fields_ids'] ) && isset( $request['fields_ids'] ) && ! empty( $request['fields_ids'] ) ) {
			$prepared_fields->fields_ids = $request['fields_ids'];
		} else {
			return new \WP_Error( 'dekode_rest_missing_fields_ids', esc_html__( 'Missing fields ids.', 'nisje' ), [ 'status' => 404 ] );
		}

		if ( ! empty( $schema['properties']['fields'] ) && isset( $request['fields_ids'] ) && ! empty( $request['fields'] ) ) {
			$prepared_fields->fields = $request['fields'];
		} else {
			return new \WP_Error( 'dekode_rest_missing_fields', esc_html__( 'Missing fields.', 'nisje' ), [ 'status' => 404 ] );
		}

		/**
		 * Filter a group before it is inserted via the REST API.
		 */
		return apply_filters( "nisje_rest_pre_insert_{$this->hook_base}", $prepared_fields, $request );
	}

	/**
	 * Prepares single xprofile field data for return as an object.
	 *
	 * @param stdClass        $item    Xprofile group data.
	 * @param WP_REST_Request $request The Request.
	 * @param boolean         $is_raw  Optional, not used. Defaults to false.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $item, $request, $is_raw = false ) {
		$schema = $this->get_item_schema();

		$data = [];

		if ( ! empty( $schema['properties']['id'] ) ) {
			$data['id'] = (int) $item->id;
		}

		if ( ! empty( $schema['properties']['group_id'] ) ) {
			$data['group_id'] = (int) $item->group_id;
		}

		if ( ! empty( $schema['properties']['parent_id'] ) ) {
			$data['parent_id'] = (int) $item->parent_id;
		}

		if ( ! empty( $schema['properties']['type'] ) ) {
			$data['type'] = $item->type;
		}

		if ( ! empty( $schema['properties']['name'] ) ) {
			$data['name'] = $item->name;
		}

		if ( ! empty( $schema['properties']['description'] ) ) {
			$data['description'] = $item->description;
		}

		if ( ! empty( $schema['properties']['is_required'] ) ) {
			$data['is_required'] = (bool) $item->is_required;
		}

		if ( ! empty( $schema['properties']['can_delete'] ) ) {
			$data['can_delete'] = (bool) $item->can_delete;
		}

		if ( ! empty( $schema['properties']['field_order'] ) ) {
			$data['field_order'] = (int) $item->field_order;
		}

		if ( ! empty( $schema['properties']['option_order'] ) ) {
			$data['option_order'] = (int) $item->option_order;
		}

		if ( ! empty( $schema['properties']['order_by'] ) ) {
			$data['order_by'] = $item->order_by;
		}

		if ( ! empty( $schema['properties']['is_default_option'] ) ) {
			$data['is_default_option'] = (bool) $item->is_default_option;
		}

		if ( ! empty( $request['fetch_visibility_level'] ) ) {
			$data['visibility_level'] = $item->visibility_level;
		}

		if ( ! empty( $schema['properties']['can_edit'] ) ) {
			$can_edit = bp_xprofile_get_meta( $item->id, 'field', 'member_can_edit' );
			$can_edit = ( ! $can_edit || 'yes' === $can_edit ) ? true : false;

			$data['can_edit'] = $can_edit;
		}

		if ( ! empty( $request['fetch_field_data'] ) ) {
			if ( isset( $item->data->id ) ) {
				$data['data']['id'] = $item->data->id;
			}
			$data['data']['value'] = maybe_unserialize( $item->data->value );
		}

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

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

		$response = rest_ensure_response( $data );
		$response->add_links( $this->prepare_links( $item ) );

		/**
		 * Filter the xprofile fields value returned from the API.
		 *
		 * @param array           $response
		 * @param WP_REST_Request $request Request used to generate the response.
		 */
		return apply_filters( 'nisje_rest_prepare_xprofile_field_value', $response, $request );
	}


	/**
	 * Check if a given request has access to get information about a specific field.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return bool
	 */
	public function get_item_permissions_check( $request ) {
		return $this->get_items_permissions_check( $request );
	}

	/**
	 * Prepare links for the request.
	 *
	 * @param array $item Field.
	 * @return array Links for the given plugin.
	 */
	protected function prepare_links( $item ) {
		$base = sprintf( '/%s/%s/', $this->namespace, $this->rest_base );

		// Entity meta.
		$links = [
			'self'       => [
				'href' => rest_url( $base . $item->id ),
			],
			'collection' => [
				'href' => rest_url( $base ),
			],
		];

		return $links;
	}

	/**
	 * Get the extended profile field schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = [
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->hook_base,
			'type'       => 'object',
			'properties' => [
				'id'                => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'A unique alphanumeric ID for the object.', 'nisje' ),
					'readonly'    => true,
					'type'        => 'integer',
				],
				'group_id'          => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The ID of the group the field is part of.', 'nisje' ),
					'type'        => 'integer',
				],
				'parent_id'         => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The ID of the field parent.', 'nisje' ),
					'type'        => 'integer',
				],
				'type'              => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The type of field, like checkbox or select.', 'nisje' ),
					'type'        => 'string',
				],
				'name'              => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The name of the profile field group.', 'nisje' ),
					'type'        => 'string',
				],
				'description'       => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The description of the profile field group.', 'nisje' ),
					'type'        => 'string',
				],
				'is_required'       => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'Whether the profile field must have a value.', 'nisje' ),
					'type'        => 'boolean',
				],
				'can_delete'        => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'Whether the profile field can be deleted or not.', 'nisje' ),
					'type'        => 'boolean',
				],
				'can_edit'          => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'Whether the profile field can be edited by the user or not.', 'nisje' ),
					'type'        => 'boolean',
				],
				'field_order'       => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The order of the field.', 'nisje' ),
					'type'        => 'integer',
				],
				'option_order'      => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The order of the field\'s options.', 'nisje' ),
					'type'        => 'integer',
				],
				'order_by'          => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'How the field\'s options are ordered.', 'nisje' ),
					'type'        => 'string',
				],
				'is_default_option' => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'Whether the option is the default option.', 'nisje' ),
					'type'        => 'boolean',
				],
				'visibility_level'  => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'Who may see the saved value for this field.', 'nisje' ),
					'type'        => 'string',
				],
				'data'              => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'The saved value for this field.', 'nisje' ),
					'type'        => 'array',
					'items'       => [
						'type' => 'string',
					],
				],
				'fields_ids'        => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'A collection of all fields to save. Keys', 'nisje' ),
					'type'        => 'array',
					'items'       => [
						'type' => 'integer',
					],
					'required'    => true,
				],
				'fields'            => [
					'context'     => [ 'view', 'edit' ],
					'description' => esc_html__( 'A collection of all fields to save. Fields', 'nisje' ),
					'type'        => 'object',
					'required'    => true,
				],
			],
		];

		return $this->add_additional_fields_schema( $schema );
	}

	/**
	 * Get the query params for single xprofile fields.
	 *
	 * @return array
	 */
	public function get_item_params() {
		$params                       = parent::get_collection_params();
		$params['context']['default'] = 'view';

		$params['user_id'] = [
			'description'       => esc_html__( 'Required if you want to load a specific user\'s data.', 'nisje' ),
			'type'              => 'integer',
			'default'           => bp_loggedin_user_id(),
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		];

		return $params;
	}
}
