<?php
/**
 * The Register API endpoint.
 *
 * @package WP_Ultimo
 * @subpackage API
 * @since 2.0.0
 */

namespace WP_Ultimo\API;

use WP_Ultimo\Checkout\Cart;
use WP_Ultimo\Database\Sites\Site_Type;
use WP_Ultimo\Database\Payments\Payment_Status;
use WP_Ultimo\Database\Memberships\Membership_Status;
use WP_Ultimo\Objects\Billing_Address;

// Exit if accessed directly
defined('ABSPATH') || exit;

/**
 * The Register API endpoint.
 *
 * @since 2.0.0
 */
class Register_Endpoint {

	use \WP_Ultimo\Traits\Singleton;

	/**
	 * Loads the initial register route hooks.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function init(): void {

		add_action('wu_register_rest_routes', [$this, 'register_route']);
	}

	/**
	 * Adds a new route to the wu namespace, for the register endpoint.
	 *
	 * @since 2.0.0
	 *
	 * @param \WP_Ultimo\API $api The API main singleton.
	 * @return void
	 */
	public function register_route($api): void {

		$namespace = $api->get_namespace();

		register_rest_route(
			$namespace,
			'/register',
			[
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => [$this, 'handle_get'],
				'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']),
			]
		);

		register_rest_route(
			$namespace,
			'/register',
			[
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => [$this, 'handle_endpoint'],
				'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']),
				'args'                => $this->get_rest_args(),
			]
		);
	}

	/**
	 * Handle the register endpoint get for zapier integration reasons.
	 *
	 * @since 2.0.0
	 *
	 * @param \WP_REST_Request $request WP Request Object.
	 * @return array
	 */
	public function handle_get($request) {

		return [
			'registration_status' => wu_get_setting('enable_registration', true) ? 'open' : 'closed',
		];
	}

	/**
	 * Handle the register endpoint logic.
	 *
	 * @since 2.0.0
	 *
	 * @param \WP_REST_Request $request WP Request Object.
	 * @return array
	 */
	public function handle_endpoint($request) {

		global $wpdb;

		$params = json_decode($request->get_body(), true);

		if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) {
			wu_log_add('api-calls', json_encode($params, JSON_PRETTY_PRINT));
		}

		$validation_errors = $this->validate($params);

		if (is_wp_error($validation_errors)) {
			$validation_errors->add_data(
				[
					'status' => 400,
				]
			);

			return $validation_errors;
		}

		$wpdb->query('START TRANSACTION');

		try {
			$customer = $this->maybe_create_customer($params);

			if (is_wp_error($customer)) {
				return $this->rollback_and_return($customer);
			}

			$customer->update_last_login(true, true);

			$customer->add_note(
				[
					'text'      => __('Created via REST API', 'wp-ultimo'),
					'author_id' => $customer->get_user_id(),
				]
			);

			/*
			 * Payment Method defaults
			 */
			$payment_method = wp_parse_args(
				wu_get_isset($params, 'payment_method'),
				[
					'gateway'                 => '',
					'gateway_customer_id'     => '',
					'gateway_subscription_id' => '',
					'gateway_payment_id'      => '',
				]
			);

			/*
			 * Cart params and creation
			 */
			$cart_params = $params;

			$cart_params = wp_parse_args(
				$cart_params,
				[
					'type' => 'new',
				]
			);

			$cart = new Cart($cart_params);

			/*
			 * Validates if the cart is valid.
			 */
			if ($cart->is_valid() && count($cart->get_line_items()) === 0) {
				return new \WP_Error(
					'invalid_cart',
					__('Products are required.', 'wp-ultimo'),
					array_merge(
						(array) $cart->done(),
						[
							'status' => 400,
						]
					)
				);
			}

			/*
			 * Get Membership data
			 */
			$membership_data = $cart->to_membership_data();

			$membership_data = array_merge(
				$membership_data,
				wu_get_isset(
					$params,
					'membership',
					[
						'status' => Membership_Status::PENDING,
					]
				)
			);

			$membership_data['customer_id']             = $customer->get_id();
			$membership_data['gateway']                 = wu_get_isset($payment_method, 'gateway');
			$membership_data['gateway_subscription_id'] = wu_get_isset($payment_method, 'gateway_subscription_id');
			$membership_data['gateway_customer_id']     = wu_get_isset($payment_method, 'gateway_customer_id');
			$membership_data['auto_renew']              = wu_get_isset($params, 'auto_renew');

			/*
			 * Unset the status because we are going to transition it later.
			 */
			$membership_status = $membership_data['status'];

			unset($membership_data['status']);

			$membership = wu_create_membership($membership_data);

			if (is_wp_error($membership)) {
				return $this->rollback_and_return($membership);
			}

			$membership->add_note(
				[
					'text'      => __('Created via REST API', 'wp-ultimo'),
					'author_id' => $customer->get_user_id(),
				]
			);

			$payment_data = $cart->to_payment_data();

			$payment_data = array_merge(
				$payment_data,
				wu_get_isset(
					$params,
					'payment',
					[
						'status' => Payment_Status::PENDING,
					]
				)
			);

			/*
			 * Unset the status because we are going to transition it later.
			 */
			$payment_status = $payment_data['status'];

			unset($payment_data['status']);

			$payment_data['customer_id']        = $customer->get_id();
			$payment_data['membership_id']      = $membership->get_id();
			$payment_data['gateway']            = wu_get_isset($payment_method, 'gateway');
			$payment_data['gateway_payment_id'] = wu_get_isset($payment_method, 'gateway_payment_id');

			$payment = wu_create_payment($payment_data);

			if (is_wp_error($payment)) {
				return $this->rollback_and_return($payment);
			}

			$payment->add_note(
				[
					'text'      => __('Created via REST API', 'wp-ultimo'),
					'author_id' => $customer->get_user_id(),
				]
			);

			$site = false;

			/*
			 * Site creation.
			 */
			if (wu_get_isset($params, 'site')) {
				$site = $this->maybe_create_site($params, $membership);

				if (is_wp_error($site)) {
					return $this->rollback_and_return($site);
				}
			}

			/*
			 * Deal with status changes.
			 */
			if ($membership->get_status() !== $membership_status) {
				$membership->set_status($membership_status);

				$membership->save();

				/*
				 * The above change might trigger a site publication
				 * to take place, so we need to try to fetch the site
				 * again, this time as a WU Site object.
				 */
				if ($site) {
					$wp_site = get_site_by_path($site['domain'], $site['path']);

					if ($wp_site) {
						$site['id'] = $wp_site->blog_id;
					}
				}
			}

			if ($payment->get_status() !== $payment_status) {
				$payment->set_status($payment_status);

				$payment->save();
			}
		} catch (\Throwable $e) {
			$wpdb->query('ROLLBACK');

			return new \WP_Error('registration_error', $e->getMessage(), ['status' => 500]);
		}

		$wpdb->query('COMMIT');

		/*
		 * We have everything we need now.
		 */
		return [
			'membership' => $membership->to_array(),
			'customer'   => $customer->to_array(),
			'payment'    => $payment->to_array(),
			'site'       => $site ?: ['id' => 0],
		];
	}

	/**
	 * Returns the list of arguments allowed on to the endpoint.
	 *
	 * This is also used to build the documentation page for the endpoint.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	public function get_rest_args() {
		/*
		 * Billing Address Fields
		 */
		$billing_address_fields = Billing_Address::fields_for_rest(false);

		$customer_args = [
			'customer_id' => [
				'description' => __('The customer ID, if the customer already exists. If you also need to create a customer/wp user, use the "customer" property.', 'wp-ultimo'),
				'type'        => 'integer',
			],
			'customer'    => [
				'description' => __('Customer data. Needs to be present when customer id is not.', 'wp-ultimo'),
				'type'        => 'object',
				'properties'  => [
					'user_id'         => [
						'description' => __('Existing WordPress user id to attach this customer to. If you also need to create a WordPress user, pass the properties "username", "password", and "email".', 'wp-ultimo'),
						'type'        => 'integer',
					],
					'username'        => [
						'description' => __('The customer username. This is used to create the WordPress user.', 'wp-ultimo'),
						'type'        => 'string',
						'minLength'   => 4,
					],
					'password'        => [
						'description' => __('The customer password. This is used to create the WordPress user. Note that no validation is performed here to enforce strength.', 'wp-ultimo'),
						'type'        => 'string',
						'minLength'   => 6,
					],
					'email'           => [
						'description' => __('The customer email address. This is used to create the WordPress user.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'email',
					],
					'billing_address' => [
						'type'       => 'object',
						'properties' => $billing_address_fields,
					],
				],
			],
		];

		$membership_args = [
			'membership' => [
				'description' => __('The membership data is automatically generated based on the cart info passed (e.g. products) but can be overridden with this property.', 'wp-ultimo'),
				'type'        => 'object',
				'properties'  => [
					'status'                      => [
						'description' => __('The membership status.', 'wp-ultimo'),
						'type'        => 'string',
						'enum'        => array_values(Membership_Status::get_allowed_list()),
						'default'     => Membership_Status::PENDING,
					],
					'date_expiration'             => [
						'description' => __('The membership expiration date. Must be a valid PHP date format.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'date-time',
					],
					'date_trial_end'              => [
						'description' => __('The membership trial end date. Must be a valid PHP date format.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'date-time',
					],
					'date_activated'              => [
						'description' => __('The membership activation date. Must be a valid PHP date format.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'date-time',
					],
					'date_renewed'                => [
						'description' => __('The membership last renewed date. Must be a valid PHP date format.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'date-time',
					],
					'date_cancellation'           => [
						'description' => __('The membership cancellation date. Must be a valid PHP date format.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'date-time',
					],
					'date_payment_plan_completed' => [
						'description' => __('The membership completion date. Used when the membership is limited to a limited number of billing cycles. Must be a valid PHP date format.', 'wp-ultimo'),
						'type'        => 'string',
						'format'      => 'date-time',
					],
				],
			],
		];

		$payment_args = [
			'payment'        => [
				'description' => __('The payment data is automatically generated based on the cart info passed (e.g. products) but can be overridden with this property.', 'wp-ultimo'),
				'type'        => 'object',
				'properties'  => [
					'status' => [
						'description' => __('The payment status.', 'wp-ultimo'),
						'type'        => 'string',
						'enum'        => array_values(Payment_Status::get_allowed_list()),
						'default'     => Payment_Status::PENDING,
					],
				],
			],
			'payment_method' => [
				'description' => __('Payment method information. Useful when using the REST API to integrate other payment methods.', 'wp-ultimo'),
				'type'        => 'object',
				'properties'  => [
					'gateway'                 => [
						'description' => __('The gateway name. E.g. stripe.', 'wp-ultimo'),
						'type'        => 'string',
					],
					'gateway_customer_id'     => [
						'description' => __('The customer ID on the gateway system.', 'wp-ultimo'),
						'type'        => 'string',
					],
					'gateway_subscription_id' => [
						'description' => __('The subscription ID on the gateway system.', 'wp-ultimo'),
						'type'        => 'string',
					],
					'gateway_payment_id'      => [
						'description' => __('The payment ID on the gateway system.', 'wp-ultimo'),
						'type'        => 'string',
					],
				],
			],
		];

		$site_args = [
			'site' => [
				'type'       => 'object',
				'properties' => [
					'site_url'    => [
						'type'        => 'string',
						'description' => __('The site subdomain or subdirectory (depending on your Multisite install). This would be "test" in "test.your-network.com".', 'wp-ultimo'),
						'minLength'   => 4,
						'required'    => true,
					],
					'site_title'  => [
						'type'        => 'string',
						'description' => __('The site title. E.g. My Amazing Site', 'wp-ultimo'),
						'minLength'   => 4,
						'required'    => true,
					],
					'publish'     => [
						'description' => __('If we should publish this site regardless of membership/payment status. Sites are created as pending by default, and are only published when a payment is received or the status of the membership changes to "active". This flag allows you to bypass the pending state.', 'wp-ultimo'),
						'type'        => 'boolean',
						'default'     => false,
					],
					'template_id' => [
						'description' => __('The template ID we should copy when creating this site. If left empty, the value dictated by the products will be used.', 'wp-ultimo'),
						'type'        => 'integer',
					],
					'site_meta'   => [
						'description' => __('An associative array of key values to be saved as site_meta.', 'wp-ultimo'),
						'type'        => 'object',
					],
					'site_option' => [
						'description' => __('An associative array of key values to be saved as site_options. Useful for changing plugin settings and other site configurations.', 'wp-ultimo'),
						'type'        => 'object',
					],
				],
			],
		];

		$cart_args = [
			'products'      => [
				'description' => __('The products to be added to this membership. Takes an array of product ids or slugs.', 'wp-ultimo'),
				'uniqueItems' => true,
				'type'        => 'array',
			],
			'duration'      => [
				'description' => __('The membership duration.', 'wp-ultimo'),
				'type'        => 'integer',
				'required'    => false,
			],
			'duration_unit' => [
				'description' => __('The membership duration unit.', 'wp-ultimo'),
				'type'        => 'string',
				'default'     => 'month',
				'enum'        => [
					'day',
					'week',
					'month',
					'year',
				],
			],
			'discount_code' => [
				'description' => __('A discount code. E.g. PROMO10.', 'wp-ultimo'),
				'type'        => 'string',
			],
			'auto_renew'    => [
				'description' => __('The membership auto-renew status. Useful when integrating with other payment options via this REST API.', 'wp-ultimo'),
				'type'        => 'boolean',
				'default'     => false,
				'required'    => true,
			],
			'country'       => [
				'description' => __('The customer country. Used to calculate taxes and check if registration is allowed for that country.', 'wp-ultimo'),
				'type'        => 'string',
				'default'     => '',
			],
			'currency'      => [
				'description' => __('The currency to be used.', 'wp-ultimo'),
				'type'        => 'string',
			],
		];

		$args = array_merge($customer_args, $membership_args, $cart_args, $payment_args, $site_args);

		return apply_filters('wu_rest_register_endpoint_args', $args, $this);
	}

	/**
	 * Maybe create a customer, if needed.
	 *
	 * @since 2.0.0
	 *
	 * @param array $p The request parameters.
	 * @return \WP_Ultimo\Models\Customer|\WP_Error
	 */
	public function maybe_create_customer($p) {

		$customer_id = wu_get_isset($p, 'customer_id');

		if ($customer_id) {
			$customer = wu_get_customer($customer_id);

			if ( ! $customer) {
				return new \WP_Error('customer_not_found', __('The customer id sent does not correspond to a valid customer.', 'wp-ultimo'));
			}
		} else {
			$customer = wu_create_customer($p['customer']);
		}

		return $customer;
	}

	/**
	 * Undocumented function
	 *
	 * @since 2.0.0
	 *
	 * @param array                        $p The request parameters.
	 * @param \WP_Ultimo\Models\Membership $membership The membership created.
	 * @return array|\WP_Ultimo\Models\Site\|\WP_Error
	 */
	public function maybe_create_site($p, $membership) {

		$site_data = $p['site'];

		/*
		 * Let's get a list of membership sites.
		 * This list includes pending sites as well.
		 */
		$sites = $membership->get_sites();

		/*
		 * Decide if we should create a new site or not.
		 *
		 * When should we create a new pending site?
		 * There are a couple of rules:
		 * - The membership must not have a pending site;
		 * - The membership must not have an existing site;
		 *
		 * The get_sites method already includes pending sites,
		 * so we can safely rely on it.
		 */
		if ( ! empty($sites)) {
			/*
			 * Returns the first site on that list.
			 * This is not ideal, but since we'll usually only have
			 * one site here, it's ok. for now.
			 */
			return current($sites);
		}

		$site_url = wu_get_isset($site_data, 'site_url');

		$d = wu_get_site_domain_and_path($site_url);

		/*
		 * Validates the site url.
		 */
		$results = wpmu_validate_blog_signup($site_url, wu_get_isset($site_data, 'site_title'), $membership->get_customer()->get_user());

		if ($results['errors']->has_errors()) {
			return $results['errors'];
		}

		/*
		 * Get the transient data to save with the site
		 * that way we can use it when actually registering
		 * the site on WordPress.
		 */
		$transient = array_merge(
			wu_get_isset($site_data, 'site_meta', []),
			wu_get_isset($site_data, 'site_option', [])
		);

		$template_id = apply_filters('wu_checkout_template_id', (int) wu_get_isset($site_data, 'template_id'), $membership, $this);

		$site_data = [
			'domain'         => $d->domain,
			'path'           => $d->path,
			'title'          => wu_get_isset($site_data, 'site_title'),
			'template_id'    => $template_id,
			'customer_id'    => $membership->get_customer()->get_id(),
			'membership_id'  => $membership->get_id(),
			'transient'      => $transient,
			'signup_meta'    => wu_get_isset($site_data, 'site_meta', []),
			'signup_options' => wu_get_isset($site_data, 'site_option', []),
			'type'           => Site_Type::CUSTOMER_OWNED,
		];

		$membership->create_pending_site($site_data);

		$site_data['id'] = 0;

		if (wu_get_isset($site_data, 'publish')) {
			$membership->publish_pending_site();

			$wp_site = get_site_by_path($site_data['domain'], $site_data['path']);

			if ($wp_site) {
				$site_data['id'] = $wp_site->blog_id;
			}
		}

		return $site_data;
	}

	/**
	 * Set the validation rules for this particular model.
	 *
	 * To see how to setup rules, check the documentation of the
	 * validation library we are using: https://github.com/rakit/validation
	 *
	 * @since 2.0.0
	 * @link https://github.com/rakit/validation
	 * @return array
	 */
	public function validation_rules() {

		return [
			'customer_id'       => 'required_without:customer',
			'customer'          => 'required_without:customer_id',
			'customer.username' => 'required_without_all:customer_id,customer.user_id',
			'customer.password' => 'required_without_all:customer_id,customer.user_id',
			'customer.email'    => 'required_without_all:customer_id,customer.user_id',
			'customer.user_id'  => 'required_without_all:customer_id,customer.username,customer.password,customer.email',
			'site.site_url'     => 'required_with:site|alpha_num|min:4|lowercase|unique_site',
			'site.site_title'   => 'required_with:site|min:4',
		];
	}

	/**
	 * Validates the rules and make sure we only save models when necessary.
	 *
	 * @since 2.0.0
	 * @param array $args The params to validate.
	 * @return mixed[]|\WP_Error
	 */
	public function validate($args) {

		$validator = new \WP_Ultimo\Helpers\Validator();

		$validator->validate($args, $this->validation_rules());

		if ($validator->fails()) {
			return $validator->get_errors();
		}

		return true;
	}

	/**
	 * Rolls back database changes and returns the error passed.
	 *
	 * @since 2.0.0
	 *
	 * @param \WP_Error $error The error to return.
	 * @return \WP_Error
	 */
	protected function rollback_and_return($error) {

		global $wpdb;

		$wpdb->query('ROLLBACK');

		return $error;
	}
}