<?php
/**
 * FE Admin Sub Accounts controller.
 *
 * @since 2.0
 */

declare( strict_types=1 );
namespace FeData\FastEvents\RestAPI\Public;

defined( 'ABSPATH' ) || exit;

use FeData\FastEvents\Accounts\FeAccount;
use FeData\FastEvents\Accounts\FeSubAccount;
use FeData\FastEvents\Helpers\AuthHelper;
use FeData\FastEvents\Utilities\UniqIds;
use stdClass;
use WP_Application_Passwords;
use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * FE Admin Sub Accounts class.
 */
class SubAccountsController extends WP_REST_Controller {

	/**
	 * The account.
	 *
	 * @since 2.0
	 * @var FeAccount
	 */
	private FeAccount $account;

	/**
	 * The application password.
	 *
	 * @since 2.0
	 * @var string
	 */
	private string $application_pwd;

	/**
	 * Key to the subaccount array.
	 *
	 * @since 2.0
	 * @var bool|int $key False if key not found, else the index in the subaccount array.
	 */
	private bool|int $key;

	/**
	 * Admin Sub Accounts constructor.
	 *
	 * @param AuthHelper $auth Auth helper instance.
	 * @since 2.0
	 */
	public function __construct( private readonly AuthHelper $auth ) {
		$this->namespace = 'fast-events/v1';
		$this->rest_base = 'admin/accounts';
	}


	/**
	 * Register the routes for the objects of the controller.
	 *
	 * @since 2.0
	 */
	public function register_routes(): void {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/sub_accounts',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'fast_events_admin_permissions' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'fast_events_admin_permissions' ),
					'args'                => $this->get_endpoint_args_for_item_schema(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/sub_accounts/(?P<uuid>[\w\-]+)',
			array(
				'args'   => array(
					'id'   => array(
						'description'       => __( 'The unique id of the user', 'fast-events' ),
						'type'              => 'integer',
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
					'uuid' => array(
						'description'       => __( 'The uuid of the sub-account.', 'fast-events' ),
						'type'              => 'string',
						'format'            => 'uuid',
						'required'          => true,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'fast_events_admin_permissions' ),
					'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, 'fast_events_admin_permissions' ),
					'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, 'fast_events_admin_permissions' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/sub_accounts/(?P<uuid>[\w\-]+)/reset_password',
			array(
				'args'   => array(
					'id'   => array(
						'description'       => __( 'The unique id of the user', 'fast-events' ),
						'type'              => 'integer',
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
					'uuid' => array(
						'description'       => __( 'The uuid of the sub-account.', 'fast-events' ),
						'type'              => 'string',
						'format'            => 'uuid',
						'required'          => true,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'reset_password' ),
					'permission_callback' => array( $this, 'fast_events_admin_permissions' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}


	/**
	 * Only 'fe_account' users with USER_APW_ADD are allowed.
	 *
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_Error|bool
	 *
	 * @since 2.0
	 */
	public function fast_events_admin_permissions( WP_REST_Request $request ): WP_Error|bool {
		if ( ! current_user_can( 'list_app_passwords', $request['id'] ) ) {
			return new WP_Error(
				'rest_cannot_list_sub_accounts',
				__( 'Sorry, you are not allowed to list sub-accounts for this user.', 'fast-events' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return $this->auth->authorize( $this->auth::USER_SUB_ACCOUNTS );
	}


	/**
	 * Get all 'fe_account' sub_accounts.
	 *
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_REST_Response|WP_Error|array
	 *
	 * @since 2.0
	 */
	public function get_items( $request ): WP_Error|WP_REST_Response|array {
		$result = $this->get_account( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$sub_accounts_info = array();
		foreach ( $this->account->sub_accounts as $sub_account ) {
			$sub_accounts_info[] = $this->prepare_response_for_collection(
				$this->prepare_item_for_response( $sub_account, $request )
			);
		}

		$response = rest_ensure_response( $sub_accounts_info );

		// Generate the totals headers.
		$response->header( 'X-WP-Total', count( $sub_accounts_info ) );
		$response->header( 'X-WP-TotalPages', 1 );

		return $response;
	}


	/**
	 * Retrieve a single 'fe_account' subaccount.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|array Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.0
	 */
	public function get_item( $request ): WP_Error|WP_REST_Response|array {
		$result = $this->get_sub_account( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return rest_ensure_response( $this->prepare_item_for_response( $this->account->sub_accounts[ $this->key ], $request ) );
	}


	/**
	 * Create a new 'fe_account' subaccount.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.0
	 */
	public function create_item( $request ): WP_Error|WP_REST_Response|bool|array|stdClass {

		// Get the account.
		$result = $this->get_account( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Has the maximum nr. of subaccounts already been reached?
		if ( count( $this->account->sub_accounts ) >= $this->account->max_sub_accounts ) {
			return new WP_Error( 'max_sub_accounts', __( 'Maximum number of sub-accounts reached.', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Validate name.
		if ( WP_Application_Passwords::application_name_exists_for_user( $request['id'], $request['name'] ) ) {
			return new WP_Error( 'sub_account_exists', __( 'Sub-account name already exists', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Create the application password.
		$apw                           = WP_Application_Passwords::create_new_application_password( $request['id'], array( 'name' => $request['name'] ) );
		$this->application_pwd         = $apw[0];
		$sub_account                   = new FeSubAccount();
		$sub_account->uuid             = $apw[1]['uuid'];
		$sub_account->blocked          = isset( $request['blocked'] ) ? $request['blocked'] : false;
		$sub_account->allowed_events   = isset( $request['allowed_events'] ) ? $request['allowed_events'] : '';
		$sub_account->permission_list  = isset( $request['permission_list'] ) ? $request['permission_list'] : str_repeat( '0', 100 );
		$this->account->sub_accounts[] = $sub_account;
		$this->account->save();

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

		return rest_ensure_response( $this->prepare_item_for_response( $sub_account, $request ) );
	}


	/**
	 * Update a single 'fe_account' user.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.0
	 */
	public function update_item( $request ): WP_Error|WP_REST_Response|bool|array|stdClass {
		$result = $this->get_sub_account( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Is the application password still there?
		$sub_account = $this->account->sub_accounts[ $this->key ];
		$result      = WP_Application_Passwords::get_user_application_password( $this->account->get_user_id(), $this->account->sub_accounts[ $this->key ]->uuid );
		if ( is_null( $result ) ) {
			$apw                   = WP_Application_Passwords::create_new_application_password(
				$this->account->get_user_id(),
				array( 'name' => 'Fast Events - ' . UniqIds::uniq_field() )
			);
			$this->application_pwd = $apw[0];
			$sub_account->uuid     = $apw[1]['uuid'];
		}

		// Did the name change?
		$result = WP_Application_Passwords::get_user_application_password( $this->account->get_user_id(), $sub_account->uuid );
		if ( isset( $request['name'] ) && $request['name'] !== $result['name'] ) {
			WP_Application_Passwords::update_application_password( $this->account->get_user_id(), $sub_account->uuid, array( 'name' => $request['name'] ) );
		}

		// Update the other info.
		if ( isset( $request['blocked'] ) ) {
			$sub_account->blocked = $request['blocked'];
		}
		if ( isset( $request['allowed_events'] ) ) {
			$sub_account->allowed_events = $request['allowed_events'];
		}
		if ( isset( $request['permission_list'] ) ) {
			$sub_account->permission_list = $request['permission_list'];
		}
		$this->account->sub_accounts[ $this->key ] = $sub_account;
		$this->account->save();

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

		return rest_ensure_response( $this->prepare_item_for_response( $sub_account, $request ) );
	}


	/**
	 * Reset password of Fast_Events_Sub_Account
	 *
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_Error|WP_REST_Response
	 *
	 * @since 2.0
	 */
	public function reset_password( WP_REST_Request $request ): WP_Error|WP_REST_Response {
		$result = $this->get_sub_account( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Is there still an uuid from the application password?
		$sub_account = $this->account->sub_accounts[ $this->key ];
		if ( ! empty( $sub_account->uuid ) ) {
			$result = WP_Application_Passwords::get_user_application_password( $request['id'], $sub_account->uuid );
			if ( is_null( $result ) ) {
				$name = 'Fast Events - ' . UniqIds::uniq_field();
			} else {
				$name = $result['name'];
			}
			WP_Application_Passwords::delete_application_password( $request['id'], $sub_account->uuid );
		} else {
			$name = 'Fast Events - ' . UniqIds::uniq_field();
		}
		$apw = WP_Application_Passwords::create_new_application_password( $request['id'], array( 'name' => $name ) );

		$sub_account->uuid                         = $apw[1]['uuid'];
		$this->application_pwd                     = $apw[0];
		$this->account->sub_accounts[ $this->key ] = $sub_account;
		$this->account->save();

		return rest_ensure_response( $this->prepare_item_for_response( $sub_account, $request ) );
	}


	/**
	 * Delete a single 'fe_account' user.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.0
	 */
	public function delete_item( $request ): WP_Error|WP_REST_Response|bool|array|stdClass {
		$result = $this->get_sub_account( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

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

		$previous = $this->prepare_item_for_response( $this->account->sub_accounts[ $this->key ], $request );

		// The trigger in fast_events_auth will take care of updating the Fast_Events_Accounts object.
		WP_Application_Passwords::delete_application_password( $this->account->get_user_id(), $request['uuid'] );

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

		return $response;
	}


	/**
	 * Retrieves the query params for the collections.
	 *
	 * @return array Query parameters for the collection.
	 *
	 * @since 2.0
	 */
	public function get_collection_params(): array {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}


	/**
	 * Prepares a single ticket type output for response.
	 *
	 * @param mixed|FeSubAccount $item    FeSubAccount object.
	 * @param WP_REST_Request    $request Request object.
	 * @return WP_REST_Response Response object.
	 *
	 * @since 2.0
	 */
	public function prepare_item_for_response( $item, $request ): WP_REST_Response {

		$data   = array();
		$fields = $this->get_fields_for_response( $request );

		$result = WP_Application_Passwords::get_user_application_password( $this->account->get_user_id(), $item->uuid );

		if ( rest_is_field_included( 'uuid', $fields ) ) {
			$data['uuid'] = $item->uuid;
		}

		if ( rest_is_field_included( 'name', $fields ) && ! is_null( $result ) ) {
			$data['name'] = is_null( $data ) ? '' : $result['name'];
		}

		if ( rest_is_field_included( 'application_pwd', $fields ) && ! empty( $this->application_pwd ) ) {
			$data['application_pwd'] = WP_Application_Passwords::chunk_password( $this->application_pwd );
		}

		if ( rest_is_field_included( 'blocked', $fields ) ) {
			$data['blocked'] = $item->blocked;
		}

		if ( rest_is_field_included( 'created', $fields ) && ! is_null( $result ) ) {
			$data['created'] = $result['created'] ? gmdate( 'Y-m-d\TH:i:s', $result['created'] ) : '';
		}

		if ( rest_is_field_included( 'last_used', $fields ) && ! is_null( $result ) ) {
			$data['last_used'] = $result['last_used'] ? gmdate( 'Y-m-d', $result['last_used'] ) : '';
		}

		if ( rest_is_field_included( 'last_ip', $fields ) && ! is_null( $result ) ) {
			$data['last_ip'] = $result['last_ip'] ?? '';
		}

		if ( rest_is_field_included( 'allowed_events', $fields ) ) {
			$data['allowed_events'] = $item->allowed_events;
		}

		if ( rest_is_field_included( 'permission_list', $fields ) ) {
			$data['permission_list'] = $item->permission_list;
		}

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

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

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $item->uuid ) );
		}

		return $response;
	}


	/**
	 * Look up the account based on the login name.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|bool
	 *
	 * @since 2.0
	 */
	private function get_account( WP_REST_Request $request ): WP_Error|bool {

		// Look up the user.
		$result = get_user_by( 'ID', $request['id'] );
		if ( false === $result ) {
			return new WP_Error( 'not_found', __( 'User not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Get the account.
		$this->account = new FeAccount( $result->ID );
		if ( ! isset( $this->account->api_key ) ) {
			return new WP_Error( 'not_found', __( 'Account structure not found. Update account to create the permissions structure.', 'fast-events' ), array( 'status' => 404 ) );
		}

		return true;
	}


	/**
	 * Look up the subaccount.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Error|bool
	 *
	 * @since 2.0
	 */
	private function get_sub_account( WP_REST_Request $request ): WP_Error|bool {

		// Look up the user.
		$account = $this->get_account( $request );
		if ( is_wp_error( $account ) ) {
			return $account;
		}

		// Get the subaccount.
		$this->key = array_search( $request['uuid'], array_column( $this->account->sub_accounts, 'uuid' ), true );
		if ( false === $this->key ) {
			return new WP_Error( 'not_found', __( 'Sub-account not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		return true;
	}


	/**
	 * Prepares links for the sub-accounts request.
	 *
	 * @param string $uuid The uuid of the subaccount.
	 * @return array Links for the given order.
	 *
	 * @since 2.0
	 */
	protected function prepare_links( string $uuid ): array {
		return array(
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%d/sub_accounts/%s', $this->namespace, $this->rest_base, $this->account->get_user_id(), $uuid ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s/%d/sub_accounts', $this->namespace, $this->rest_base, $this->account->get_user_id() ) ),
			),
		);
	}


	/**
	 * Return the sub-accounts schema.
	 *
	 * @return array
	 *
	 * @since 2.0
	 */
	public function get_item_schema(): array {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'sub-accounts',
			'type'       => 'object',
			'properties' => array(
				'uuid'            => array(
					'description' => __( 'The unique identifier for the sub-account.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'format'      => 'uuid',
					'readonly'    => true,
				),
				'name'            => array(
					'description' => __( 'The name of the sub-account.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'required'    => true,
					'minLength'   => 4,
					'maxLength'   => 50,
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'application_pwd' => array(
					'description' => __( 'The generated password. Only available after adding a sub-account.', 'fast-events' ),
					'context'     => array( 'view', 'edit' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'created'         => array(
					'description' => __( 'The GMT date the application password was created.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'last_used'       => array(
					'description' => __( 'The GMT date the application password was last used.' ),
					'type'        => array( 'string', 'null' ),
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'last_ip'         => array(
					'description' => __( 'The IP address the application password was last used by.' ),
					'type'        => array( 'string', 'null' ),
					'format'      => 'ip',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'blocked'         => array(
					'description' => __( 'Is the user blocked.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'boolean',
					'default'     => false,
				),
				'allowed_events'  => array(
					'description' => __( 'Comma separated list of allowed event ids.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'maxLength'   => 128,
					'pattern'     => '^\d*(,\d+)*$',
				),
				'permission_list' => array(
					'description' => __( 'List of permissions - every byte is a 0 or 1.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'pattern'     => '^[0-1]{100}$',
				),
			),
		);

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