<?php
/**
 * Group Members Rest Endpoint
 *
 * @package Nisje
 */

namespace Dekode\Nisje\Components\Rest;

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

/**
 * Groups Member Class
 */
class Groups_Members_Controller extends \WP_REST_Controller {
	/**
	 * Constructor
	 */
	public function __construct() {
		$this->namespace   = nisje_get_rest_namespace();
		$this->parent_base = buddypress()->groups->id;
		$this->rest_base   = 'members';
		$this->hook_base   = 'groups_members';
	}

	/**
	 * Register routes
	 */
	public function register_routes() {
		register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P<id>[\d]+)/' . $this->rest_base, [
			'args'   => [
				'id' => [
					'description' => esc_html__( 'Unique identifier for the object.', 'nisje' ),
					'type'        => 'integer',
				],
			],
			[
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_items' ],
				'permission_callback' => [ $this, 'get_items_permissions_check' ],
				'args'                => $this->get_collection_params(),
			],
			'schema' => [ $this, 'get_public_item_schema' ],
		] );
	}

	/**
	 * Check if a given request has access to group member items.
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function get_items_permissions_check( $request ) {
		$auth = nisje_validate_rest_authentication( $this->hook_base, 'get_items' );
		if ( is_wp_error( $auth ) ) {
			return $auth;
		}

		$group_id = (int) $request['id'];

		$group = nisje_get_group( $group_id );
		if ( is_wp_error( $group ) ) {
			return $group;
		}

		if ( 'public' !== $group->status && ! groups_is_user_member( get_current_user_id(), $group->id ) ) {
			return new \WP_Error( 'nisje_rest_cannot_read', esc_html__( 'Sorry, you are not allowed to see members of this group.', 'nisje' ), [
				'status' => rest_authorization_required_code(),
			] );
		}

		if ( ( isset( $request['inviter_id'] ) || false === $request['is_confirmed'] ) && ! groups_is_user_admin( get_current_user_id(), $group->id ) ) {
			return new \WP_Error( 'nisje_rest_cannot_read_requests', esc_html__( 'You are not allowed to see member requests.', 'nisje' ), [
				'status' => rest_authorization_required_code(),
			] );
		}

		if ( 'edit' === $request['context'] ) {
			return new \WP_Error( 'nisje_rest_forbidden_context', esc_html__( 'Sorry, you cannot view this resource with edit context.', 'nisje' ), [
				'status' => rest_authorization_required_code(),
			] );
		}

		return true;
	}

	/**
	 * Get a collection of group members.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|WP_REST_Response
	 */
	public function get_items( $request ) {
		$args = [
			'group_id'                => $request['id'],
			'type'                    => $request['type'],
			'exclude'                 => $request['exclude'],
			'search'                  => $request['search'],
			'group_role'              => $request['group_role'],
			'per_page'                => $request['per_page'],
			'inviter_id'              => $request['inviter_id'],
			'is_confirmed'            => $request['is_confirmed'],
			'page'                    => $request['page'],
			'exclude_admins_mods'     => $request['exclude_admins_mods'],
			'exclude_banned'          => $request['exclude_admins_banned'],
			'exclude_from_search_log' => $request['exclude_from_search_log'],
		];

		$excludes = nisje_get_setting( 'exclude_members' );
		if ( is_array( $excludes ) && ! empty( $excludes ) ) {
			if ( is_array( $args['exclude'] ) ) {
				$args['exclude'] = array_merge( $args['exclude'], $excludes );
			} else {
				$args['exclude'] = $excludes;
			}
		}

		/**
		 * Filter the query arguments for a request.
		 * Enables adding extra arguments or setting defaults for a group collection request.
		 *
		 * @param array           $args    Key value array of query var to query value.
		 * @param WP_REST_Request $request The request used.
		 */
		$args       = apply_filters( 'nisje_rest_groups_members_query', $args, $request );
		$query_args = $this->prepare_items_query( $args, $request );
		$retval     = [];

		// Perform the group member query (extends BP_User_Query).
		$members_query = new \BP_Group_Member_Query( $query_args );

		// Structure the return value as expected by the template functions.
		$members = [
			'members' => array_values( $members_query->results ),
			'total'   => $members_query->total_users,
		];

		foreach ( $members['members'] as $member ) {
			$data     = $this->prepare_item_for_response( $member, $request );
			$retval[] = $this->prepare_response_for_collection( $data );
		}

		$page          = (int) $query_args['page'];
		$total_members = isset( $members['total'] ) ? $members['total'] : 0;

		if ( $total_members < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $query_args['page'] );
			$count_query   = groups_get_group_members( $query_args );
			$total_members = isset( $count_query['total'] ) ? $count_query['total'] : 0;
		}

		$max_pages = ceil( $total_members / (int) $query_args['per_page'] );

		$response = rest_ensure_response( $retval );
		$response->header( 'X-WP-Total', (int) $total_members );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$request_params = $request->get_query_params();

		$base = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->parent_base . '/' . $this->rest_base ) ) );

		if ( $page > 1 ) {
			$prev_page = $page - 1;
			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}
			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Determine the allowed query_vars for a get_items() response and prepare.
	 *
	 * @param array           $prepared_args Prepared Arguments.
	 * @param WP_REST_Request $request       The request.
	 * @return array          $query_args
	 */
	protected function prepare_items_query( $prepared_args = [], $request = null ) {
		$valid_vars = array_flip( $this->get_allowed_query_vars() );
		$query_args = [];
		foreach ( $valid_vars as $var => $index ) {
			if ( isset( $prepared_args[ $var ] ) ) {
				/**
				 * Filter the query_vars used in `get_items` for the constructed query.
				 * The dynamic portion of the hook name, $var, refers to the query_var key.
				 *
				 * @param mixed $prepared_args[ $var ] The query_var value.
				 */
				$query_args[ $var ] = apply_filters( "nisje_rest_groups_members_query_{$var}", $prepared_args[ $var ] );
			}
		}

		if ( empty( $query_args['exclude'] ) ) {
			$query_args['exclude'] = false;
		}

		if ( empty( $query_args['page'] ) ) {
			$query_args['page'] = false;
		}

		if ( empty( $query_args['per_page'] ) ) {
			$query_args['per_page'] = false;
		}

		if ( ! empty( $query_args['search'] ) ) {
			$query_args['search_terms'] = $query_args['search'];
		} else {
			$query_args['search_terms'] = false;
		}

		if ( ! empty( $query_args['exclude_from_search_log'] ) ) {
			$query_args['exclude_from_search_log'] = true === $query_args['exclude_from_search_log'] ? true : false;
		}

		// Both exclude_admins_mods and exclude_banned are legacy arguments.
		// Convert to group_role.
		if ( empty( $query_args['group_role'] ) ) {
			$query_args['group_role'] = [ 'member' ];

			if ( ! $query_args['exclude_admins_mods'] ) {
				$query_args['group_role'][] = 'mod';
				$query_args['group_role'][] = 'admin';
			}

			if ( ! $query_args['exclude_banned'] ) {
				$query_args['group_role'][] = 'banned';
			}
		}

		return $query_args;
	}

	/**
	 * Get all the groups vars that are allowed for the API request.
	 *
	 * @return array
	 */
	protected function get_allowed_query_vars() {
		global $wp;

		/**
		 * Filter the publicly allowed query vars.
		 * Allows adjusting of the default query vars that are made public.
		 *
		 * @param array  Array of allowed \WP_Query query vars.
		 */
		$valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );

		// Define our own in addition to WP's normal vars.
		$rest_valid = [
			'group_id',
			'type',
			'exclude',
			'search',
			'group_role',
			'per_page',
			'page',
			'inviter_id',
			'is_confirmed',
			'exclude_admins_mods',
			'exclude_banned',
			'exclude_from_search_log',
		];
		$valid_vars = array_merge( $valid_vars, $rest_valid );

		/**
		 * Filter allowed query vars for the REST API.
		 *
		 * This filter allows you to add or remove query vars from the final allowed
		 * list for all requests, including unauthenticated ones. To alter the
		 * vars for editors only, {@see rest_private_query_vars}.
		 *
		 * @param array {
		 *    Array of allowed \WP_Query query vars.
		 *
		 *    @param string $allowed_query_var The query var to allow.
		 * }
		 */
		$valid_vars = apply_filters( 'nisje_rest_group_members_query_vars', $valid_vars );

		return $valid_vars;
	}

	/**
	 * Prepares member data for return as an object.
	 *
	 * @param stdClass        $item    Member 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']['name'] ) ) {
			$data['name'] = bp_core_get_user_displayname( $item->ID );
		}

		if ( ! empty( $schema['properties']['group_role'] ) ) {
			if ( 1 === $item->is_admin ) {
				$group_role = 'admin';
			} elseif ( 1 === $item->is_mod ) {
				$group_role = 'mod';
			} elseif ( 1 === $item->is_banned ) {
				$group_role = 'banned';
			} else {
				$group_role = 'member';
			}
			$data['group_role'] = $group_role;
		}

		if ( 'invite' === $request['context'] ) {
			if ( ! empty( $schema['properties']['is_confirmed'] ) ) {
				$data['is_confirmed'] = 1 === $item->is_confirmed ? true : false;
			}

			if ( ! empty( $schema['properties']['is_banned'] ) ) {
				$data['is_banned'] = 1 === $item->is_banned ? true : false;
			}

			if ( ! empty( $schema['properties']['invite_sent'] ) ) {
				$data['invite_sent'] = 1 === $item->invite_sent ? true : false;
			}

			if ( ! empty( $schema['properties']['is_admin'] ) ) {
				$data['is_admin'] = 1 === $item->is_admin ? true : false;
			}

			if ( ! empty( $schema['properties']['is_mod'] ) ) {
				$data['is_mod'] = 1 === $item->is_mod ? true : false;
			}
		}

		$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 an activity 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_group_member_value', $response, $item, $request );
	}

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

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

		return $links;
	}

	/**
	 * Convert the input date to RFC3339 format.
	 *
	 * @param string      $date_gmt Date gmt.
	 * @param string|null $date     Optional. Date object.
	 * @return string|null ISO8601/RFC3339 formatted datetime.
	 */
	protected function prepare_date_response( $date_gmt, $date = null ) {
		if ( isset( $date ) ) {
			return mysql_to_rfc3339( $date );
		}

		if ( '0000-00-00 00:00:00' === $date_gmt ) {
			return null;
		}

		return mysql_to_rfc3339( $date_gmt );
	}

	/**
	 * Get the plugin schema, conforming to JSON Schema.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = [
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'group-member',
			'type'       => 'object',
			'properties' => [
				'id'           => [
					'context'     => [ 'view', 'edit', 'embed', 'invite' ],
					'description' => esc_html__( 'A unique alphanumeric ID for the object.', 'nisje' ),
					'readonly'    => true,
					'type'        => 'integer',
				],
				'name'         => [
					'context'     => [ 'view', 'edit', 'embed', 'invite' ],
					'description' => esc_html__( 'Display name for the resource.', 'nisje' ),
					'type'        => 'string',
					'readonly'    => true,
				],
				'group_role'   => [
					'context'     => [ 'view', 'embed' ],
					'description' => esc_html__( 'Group role for the resource.', 'nisje' ),
					'type'        => 'string',
					'readonly'    => true,
				],
				'inviter_id'   => [
					'context'     => [ 'view', 'invite' ],
					'description' => esc_html__( 'Id of the inviter', 'nisje' ),
					'type'        => 'integer',
					'default'     => 0,
				],
				'is_confirmed' => [
					'context'     => [ 'view', 'invite' ],
					'description' => esc_html__( 'Is user confirmed?.', 'nisje' ),
					'type'        => 'boolean',
					'default'     => true,
				],
				'is_banned'    => [
					'context'     => [ 'invite' ],
					'description' => esc_html__( 'Is user banned?', 'nisje' ),
					'type'        => 'boolean',
					'readonly'    => true,
				],
				'invite_sent'  => [
					'context'     => [ 'invite' ],
					'description' => esc_html__( 'Is invite sent to the user?', 'nisje' ),
					'type'        => 'boolean',
					'readonly'    => true,
				],
				'is_mod'       => [
					'context'     => [ 'invite' ],
					'description' => esc_html__( 'Is mod?', 'nisje' ),
					'type'        => 'boolean',
					'readonly'    => true,
				],
				'is_admin'     => [
					'context'     => [ 'invite' ],
					'description' => esc_html__( 'Is admin?', 'nisje' ),
					'type'        => 'boolean',
					'readonly'    => true,
				],
			],
		];

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

	/**
	 * Get the query params for collections of plugins.
	 *
	 * @return array
	 */
	public function get_collection_params() {
		$params = parent::get_collection_params();

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

		$params['type'] = [
			'description'       => esc_html__( 'Shorthand for certain orderby/order combinations', 'nisje' ),
			'type'              => 'string',
			'default'           => 'last_joined',
			'enum'              => [ 'last_joined', 'first_joined', 'group_activity', 'active', 'newest', 'alphabetical', 'random', 'popular' ],
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['exclude'] = [
			'description'       => esc_html__( 'Ensure result set excludes specific IDs.', 'nisje' ),
			'type'              => 'array',
			'default'           => [],
			'sanitize_callback' => 'wp_parse_id_list',
		];

		$params['search'] = [
			'description'       => esc_html__( 'Limit result set to items that match this search query.', 'nisje' ),
			'default'           => '',
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_text_field',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['exclude_admins_mods'] = [
			'description'       => esc_html__( 'Exclude mods', 'nisje' ),
			'default'           => true,
			'type'              => 'boolean',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['exclude_admins_banned'] = [
			'description'       => esc_html__( 'Exclude admins.', 'nisje' ),
			'default'           => true,
			'type'              => 'boolean',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['per_page'] = [
			'description'       => esc_html__( 'Maximum number of results returned per result set.', 'nisje' ),
			'default'           => 20,
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['page'] = [
			'description'       => esc_html__( 'Offset the result set by a specific number of pages of results.', 'nisje' ),
			'default'           => 1,
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['exclude_from_search_log'] = [
			'description'       => esc_html__( 'Do not track this search.', 'nisje' ),
			'default'           => false,
			'type'              => 'boolean',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['inviter_id'] = [
			'description'       => esc_html__( 'Id of the inviter.', 'nisje' ),
			'type'              => 'integer',
			'sanitize_callback' => 'absint',
			'validate_callback' => 'rest_validate_request_arg',
		];

		$params['is_confirmed'] = [
			'description'       => esc_html__( 'Is user confirmed?', 'nisje' ),
			'default'           => true,
			'type'              => 'boolean',
			'validate_callback' => 'rest_validate_request_arg',
		];

		return $params;
	}
}
