<?php
/**
 * Ultimate Multisite faker
 *
 * @package WP_Ultimo
 * @subpackage Helper
 * @since 2.0.0
 */

namespace WP_Ultimo;

use Exception;
use Faker as Lib_Faker;
use WP_Ultimo\Database\Domains\Domain_Stage;
use WP_Ultimo\Models\Membership;
use WP_Ultimo\Models\Product;

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

/**
 * Ultimate Multisite faker
 *
 * @since 2.0.0
 */
class Faker {

	/**
	 * Hold the fake data
	 *
	 * @since 2.0.0
	 * @var array|null
	 */
	private $fake_data_generated;

	/**
	 * Hold the faker object
	 *
	 * @since 2.2.0
	 * @var Lib_Faker\Generator
	 */
	private $faker;

	/**
	 * Constructor
	 *
	 * @since 2.0.0
	 */
	public function __construct() {

		$this->faker = Lib_Faker\Factory::create();
	}

	/**
	 * Get the faker generator.
	 *
	 * @since 2.0.0
	 * @return \Faker\Generator faker object.
	 */
	private function get_faker() {

		return $this->faker;
	}

	/**
	 * Get the faker generator.
	 *
	 * @since 2.0.0
	 * @return \Faker\Generator faker object.
	 */
	public function generate() {

		return $this->get_faker();
	}

	/**
	 * Get the fake data generated.
	 *
	 * @since 2.0.0
	 * @param string $model The model name to get.
	 * @return array The fake data generated.
	 */
	public function get_fake_data_generated($model = '') {

		if (empty($this->fake_data_generated)) {
			$this->fake_data_generated = [
				'customers'      => [],
				'products'       => [],
				'memberships'    => [],
				'domains'        => [],
				'events'         => [],
				'discount_codes' => [],
				'checkout_forms' => [],
				'emails'         => [],
				'broadcasts'     => [],
				'webhooks'       => [],
				'payments'       => [],
				'sites'          => [],
			];
		}

		if (empty($model)) {
			return $this->fake_data_generated;
		}

		if (isset($this->fake_data_generated[ $model ])) {
			return $this->fake_data_generated[ $model ];
		} else {
			return [];
		}
	}

	/**
	 * Set the fake data generated.
	 *
	 * @since 2.0.0
	 * @param string $model The model name.
	 * @param mixed  $value The value to identify the fake data generated.
	 */
	public function set_fake_data_generated($model, $value): void {

		$this->get_fake_data_generated();

		$this->fake_data_generated[ $model ][] = $value;
	}

	/**
	 * Get the option "debug_faker" with the data generated by faker.
	 *
	 * @since 2.0.0
	 * @return array The ids of the fake data generated.
	 */
	public function get_option_debug_faker() {

		return wu_get_option(
			'debug_faker',
			[
				'customers'      => [],
				'products'       => [],
				'memberships'    => [],
				'domains'        => [],
				'events'         => [],
				'discount_codes' => [],
				'checkout_forms' => [],
				'emails'         => [],
				'broadcasts'     => [],
				'webhooks'       => [],
				'payments'       => [],
				'sites'          => [],
			]
		);
	}

	/**
	 * Get random data.
	 *
	 * @since 2.0.0
	 * @param string $model The name of model.
	 * @return object|false The id of the data.
	 */
	private function get_random_data($model) {

		if ($model) {
			$faker = $this->get_faker();

			$data_saved = wu_get_isset($this->get_option_debug_faker(), $model, []);

			$data_in_memory = $this->get_fake_data_generated($model);

			if ( ! empty($data_saved) && ! empty($data_in_memory)) {
				$data_saved_or_in_memory = $faker->randomElement(['data_saved', 'data_in_memory']);

				$data_index = $faker->numberBetween(0, count(${$data_saved_or_in_memory}) - 1);

				return ${$data_saved_or_in_memory}[ $data_index ];
			} elseif ( ! empty($data_saved)) {
				$data_index = $faker->numberBetween(0, count($data_saved) - 1);

				return $data_saved[ $data_index ];
			} elseif ( ! empty($data_in_memory)) {
				$data_index = $faker->numberBetween(0, count($data_in_memory) - 1);

				return $data_in_memory[ $data_index ];
			} else {
				return false;
			}
		}
		return false;
	}

	/**
	 * Get random customer.
	 *
	 * @since 2.0.0
	 * @param boolean $create_if_not_exist Create the data if there's none.
	 * @return object The customer object.
	 */
	private function get_random_customer($create_if_not_exist = false) {

		$faker = $this->get_faker();

		$customer = $this->get_random_data('customers');

		if ( ! $customer) {
			if ($create_if_not_exist) {
				$this->generate_fake_customers();

				$customer = $this->get_random_data('customers');
			} else {
				return false;
			}
		}

		if (is_object($customer)) {
			return $customer;
		} else {
			return wu_get_customer($customer);
		}
	}

	/**
	 * Get random product.
	 *
	 * @since 2.0.0
	 * @param boolean $create_if_not_exist Create the data if there's none.
	 * @return Product|false The product object.
	 */
	private function get_random_product($create_if_not_exist = false) {

		$faker = $this->get_faker();

		$product = $this->get_random_data('products');

		if ( ! $product) {
			if ($create_if_not_exist) {
				$this->generate_fake_products();

				$product = $this->get_random_data('products');
			} else {
				return false;
			}
		}

		if (is_object($product)) {
			return $product;
		} else {
			return wu_get_product($product);
		}
	}

	/**
	 * Get random membership.
	 *
	 * @since 2.0.0
	 * @return Membership|false The membership object.
	 */
	private function get_random_membership() {

		$faker = $this->get_faker();

		$membership = $this->get_random_data('memberships');

		if ( ! $membership) {
			return false;
		}

		if (is_object($membership)) {
			return $membership;
		} else {
			return wu_get_membership($membership);
		}
	}

	/**
	 * Get random site.
	 *
	 * @since 2.0.0
	 * @return object|false The site object.
	 */
	private function get_random_site() {

		$faker = $this->get_faker();

		$site = $this->get_random_data('sites');

		if ( ! $site) {
			return false;
		}

		if (is_object($site)) {
			return $site;
		} else {
			return wu_get_site($site);
		}
	}

	/**
	 * Get random payment.
	 *
	 * @since 2.0.0
	 * @return object|false The payment object.
	 */
	private function get_random_payment() {

		$faker = $this->get_faker();

		$payment = $this->get_random_data('payments');

		if ( ! $payment) {
			return false;
		}

		if (is_object($payment)) {
			return $payment;
		} else {
			return wu_get_payment($payment);
		}
	}

	/**
	 * Generate a faker customer.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_customers($number = 1): void {

		for ($i = 0; $i < $number; $i++) {
			$user_name  = $this->get_faker()->userName;
			$user_email = $this->get_faker()->safeEmail;

			if ( ! username_exists($user_name) && ! email_exists($user_email)) {
				$password = wp_generate_password(12, false);

				$user_id = wp_create_user($user_name, $password, $user_email);

				remove_user_from_blog($user_id);

				$customer = wu_create_customer(
					[
						'user_id'            => $user_id,
						'vip'                => $this->get_faker()->boolean,
						'date_registered'    => $this->get_faker()->dateTimeThisYear()->format('Y-m-d H:i:s'),
						'email_verification' => $this->get_faker()->randomElement(
							[
								'none',
								'pending',
								'verified',
							]
						),
						'meta'               => [
							'ip_country' => $this->get_faker()->countryCode,
						],
					]
				);

				if (is_wp_error($customer)) {
					throw new Exception(esc_html($customer->get_error_message()));
				} else {
					$this->set_fake_data_generated('customers', $customer);
				}
			}
		}
	}

	/**
	 * Generate a faker product.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_products($number = 1): void {

		$faker                 = $this->get_faker();
		$product_type_options  = [
			'plan',
			'package',
			'service',
		];
		$pricing_type_options  = [
			'paid',
			'free',
			'contact_us',
		];
		$duration_unit_options = [
			'day',
			'week',
			'month',
			'year',
		];

		for ($i = 0; $i < $number; $i++) {
			$product_data = [];

			$type         = $faker->optional(0.5, $product_type_options[0])->randomElement($product_type_options);
			$pricing_type = $faker->optional(0.2, $pricing_type_options[0])->randomElement($pricing_type_options);
			$amount       = 'free' === $pricing_type ? 0 : $faker->numberBetween(10, 55);
			$name         = $faker->sentence(2);

			$product_data['type']                = $type;
			$product_data['name']                = $name;
			$product_data['description']         = $faker->sentence();
			$product_data['pricing_type']        = $pricing_type;
			$product_data['amount']              = $amount;
			$product_data['trial_duration']      = $faker->numberBetween(0, 5);
			$product_data['trial_duration_unit'] = $faker->randomElement($duration_unit_options);
			$product_data['duration']            = $faker->numberBetween(1, 3);
			$product_data['duration_unit']       = $faker->randomElement($duration_unit_options);
			$product_data['active']              = $faker->boolean(75);
			$product_data['currency']            = 'USD';
			$product_data['slug']                = strtolower(str_replace(' ', '-', (string) $name));
			$product_data['recurring']           = $faker->boolean(75);

			$product = wu_create_product($product_data);

			if (is_wp_error($product)) {
				throw new Exception(esc_html($product->get_error_message()));
			} else {
				$this->set_fake_data_generated('products', $product);
			}
		}
	}

	/**
	 * Generate a faker membership.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_memberships($number = 1): void {

		$faker = $this->get_faker();

		for ($i = 0; $i < $number; $i++) {
			$customer = $this->get_random_customer(true);
			$product  = $this->get_random_product(true);

			$status_options = [
				'pending',
				'active',
				'on-hold',
				'expired',
				'cancelled',
			];

			$membership_data = [];

			$membership_data['customer_id']   = $customer ? $customer->get_id() : 0;
			$membership_data['plan_id']       = $product ? $product->get_id() : 0;
			$membership_data['amount']        = $product ? $product->get_amount() : 0;
			$membership_data['status']        = $faker->optional(0.6, $status_options[1])->randomElement($status_options);
			$membership_data['disabled']      = $faker->boolean(75);
			$membership_data['signup_method'] = 'network_admin';
			$membership_data['date_created']  = $this->get_faker()->dateTimeThisYear()->format('Y-m-d H:i:s');

			$membership_data = array_merge(
				$membership_data,
				$product ? $product->to_array() : [],
				$customer ? $customer->to_array() : []
			);

			$membership = wu_create_membership($membership_data);

			if (is_wp_error($membership)) {
				throw new Exception(esc_html($membership->get_error_message()));
			} else {
				$this->set_fake_data_generated('memberships', $membership);
			}
		}
	}

	/**
	 * Generate a fake domain.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_domain($number = 1): void {

		$faker         = $this->get_faker();
		$stage_options = [
			Domain_Stage::CHECKING_DNS,
			Domain_Stage::CHECKING_SSL,
			Domain_Stage::DONE,
		];

		$stage_checking_dns = $stage_options[0];

		$stage = $faker->optional(0.35, $stage_checking_dns)->randomElement($stage_options);

		for ($i = 0; $i < $number; $i++) {
			$site = $this->get_random_site();

			$domain = wu_create_domain(
				[
					'domain'     => $faker->domainName, // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				'stage'          => $stage,
				'blog_id'        => $site ? $site->get_blog_id() : 0,
				'primary_domain' => $faker->boolean(25),
				'active'         => $faker->boolean(75),
				'secure'         => $faker->boolean(25),
				]
			);

			if (is_wp_error($domain)) {
				throw new Exception(esc_html($domain->get_error_message()));
			} else {
				$this->set_fake_data_generated('domains', $domain);
			}
		}
	}

	/**
	 * Generate a fake event.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_events($number = 1): void {

		$faker = $this->get_faker();

		$initiator_options = [
			'system',
			'manual',
		];

		$payload = [
			'key'       => '1234',
			'old_value' => 'None',
			'new_value' => 'created',
		];

		for ($i = 0; $i < $number; $i++) {
			$membership = $this->get_random_membership();

			$author_id = $membership->get_id();

			$event_data = [
				'severity'     => 3,
				'initiator'    => $faker->randomElement($initiator_options),
				'author_id'    => $author_id > 0 ? $author_id : 1,
				'object_id'    => 6,
				'object_type'  => 'customer',
				'slug'         => 'created',
				'payload'      => $payload,
				'date_created' => $faker->dateTimeThisYear()->format('Y-m-d H:i:s'),
			];

			$event_data = wu_create_event($event_data);

			if (is_wp_error($event_data)) {
				throw new Exception(esc_html($event_data->get_error_message()));
			} else {
				$this->set_fake_data_generated('events', $event_data);
			}
		}
	}

	/**
	 * Generate a fake discount code.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_discount_code($number = 1): void {

		$faker                   = $this->get_faker();
		$type_options            = [
			'percentage',
			'absolute',
		];
		$type_options_percentage = $type_options[0];

		for ($i = 0; $i < $number; $i++) {
			$name            = rtrim((string) $faker->sentence(2), '.');
			$value           = $faker->numberBetween(1, 25);
			$code            = strtoupper(substr(implode('', explode(' ', $name)), 0, 15)) . $value . 'OFF';
			$type            = $faker->optional(0.2, $type_options_percentage)->randomElement($type_options);
			$setup_fee_type  = $faker->optional(0.2, $type_options_percentage)->randomElement($type_options);
			$start_date      = $faker->dateTimeBetween('-1 weeks', 'now', 'UTC');
			$expiration_date = $faker->dateTimeBetween('now', '+4 weeks', 'UTC');

			$discount_code = wu_create_discount_code(
				[
					'name'            => $name,
					'description'     => $faker->sentence(),
					'code'            => $code,
					'max_uses'        => $faker->numberBetween(1, 50),
					'type'            => $type,
					'value'           => $value,
					'setup_fee_type'  => $setup_fee_type,
					'setup_fee_value' => $faker->numberBetween(1, 20),
					'date_start'      => $start_date->format('Y-m-d H:i:s'),
					'date_expiration' => $expiration_date->format('Y-m-d H:i:s'),
					'active'          => true,
				]
			);

			if (is_wp_error($discount_code)) {
				throw new Exception(esc_html($discount_code->get_error_message()));
			} else {
				$this->set_fake_data_generated('discount_codes', $discount_code);
			}
		}
	}

	/**
	 * Generate a fake checkout form.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_checkout_form($number = 1): void {

		$faker = $this->get_faker();

		$checkout_form_name = rtrim((string) $faker->sentence(2), '.');
		$checkout_form_slug = str_replace(' ', '-', $checkout_form_name);

		for ($i = 0; $i < $number; $i++) {
			$checkout_form_data = [
				'name'              => $checkout_form_name,
				'slug'              => strtolower($checkout_form_slug),
				'active'            => true,
				'settings'          => [],
				'custom_css'        => '',
				'allowed_countries' => '',
				'date_created'      => $faker->dateTimeThisYear()->format('Y-m-d H:i:s'),
				'date_modified'     => $faker->dateTimeThisYear()->format('Y-m-d H:i:s'),
			];

			$checkout_form = wu_create_checkout_form($checkout_form_data);

			if (is_wp_error($checkout_form)) {
				throw new Exception(esc_html($checkout_form->get_error_message()));
			} else {
				$this->set_fake_data_generated('checkout_forms', $checkout_form);
			}
		}
	}

	/**
	 * Generate a fake email.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_email($number = 1): void {

		$faker = $this->get_faker();

		$schedule_type_options = [
			'days',
			'hours',
		];

		$target_options = [
			'customer',
			'admin',
		];

		for ($i = 0; $i < $number; $i++) {
			$email_title = rtrim((string) $faker->sentence(2), '.');
			$email_slug  = strtolower(str_replace(' ', '-', $email_title));
			$user_name   = $this->get_faker()->userName;
			$user_email  = $this->get_faker()->safeEmail;

			$email_data = [
				'schedule'            => 0,
				'type'                => 'system_email',
				'event'               => $faker->sentence(2),
				'send_hours'          => '',
				'send_days'           => 1,
				'schedule_type'       => $faker->randomElement($schedule_type_options),
				'name'                => $email_title,
				'title'               => $email_title,
				'slug'                => $email_slug,
				'custom_sender'       => 0,
				'custom_sender_name'  => $user_name,
				'custom_sender_email' => $user_email,
				'target'              => $faker->randomElement($target_options),
				'send_copy_to_admin'  => 0,
				'active'              => 1,
				'legacy'              => 0,
			];

			$email = wu_create_email($email_data);

			if (is_wp_error($email)) {
				throw new Exception(esc_html($email->get_error_message()));
			} else {
				$this->set_fake_data_generated('emails', $email);
			}
		}
	}

	/**
	 * Generate a fake broadcast.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_broadcast($number = 1): void {

		$faker = $this->get_faker();

		$notice_type_optinos = [
			'info',
			'success',
			'warning',
			'error',
		];

		$type_optinos = [
			'broadcast_email',
			'broadcast_notice',
		];

		for ($i = 0; $i < $number; $i++) {
			$broadcast_data = [
				'notice_type' => $faker->randomElement($notice_type_optinos),
				'status'      => 'publish',
				'name'        => rtrim((string) $faker->sentence(3), '.'),
				'title'       => rtrim((string) $faker->sentence(3), '.'),
				'content'     => rtrim((string) $faker->sentence(8), '.'),
				'type'        => $faker->randomElement($type_optinos),
			];

			$broadcast = wu_create_broadcast($broadcast_data);

			if (is_wp_error($broadcast)) {
				throw new Exception(esc_html($broadcast->get_error_message()));
			} else {
				$this->set_fake_data_generated('broadcasts', $broadcast);
			}
		}
	}

	/**
	 * Generate a fake webhook.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_webhook($number = 1): void {

		$faker         = $this->get_faker();
		$event_options = [
			'account_created',
			'account_deleted',
			'new_domain_mapping',
			'payment_received',
			'payment_successful',
			'payment_failed',
			'refund_issued',
			'plan_change',
		];

		for ($i = 0; $i < $number; $i++) {
			$webhook_data = [
				'name'             => rtrim((string) $faker->sentence(2), '.'),
				'webhook_url'      => 'https://' . $faker->domainName,
				'event'            => $faker->randomElement($event_options),
				'event_count'      => 0,
				'active'           => $faker->boolean(75),
				'hidden'           => 0,
				'integration'      => rtrim((string) $faker->sentence(3), '.'),
				'date_last_failed' => '',
			];

			$webhook = wu_create_webhook($webhook_data);

			if (is_wp_error($webhook)) {
				throw new Exception(esc_html($webhook->get_error_message()));
			} else {
				$this->set_fake_data_generated('webhooks', $webhook);
			}
		}
	}

	/**
	 * Generate a fake payment.
	 *
	 * @since 2.0.0
	 * @param int $number The number of fake data that will be generated.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_payment($number = 1): void {

		$faker          = $this->get_faker();
		$type_options   = [
			'percentage',
			'absolute',
		];
		$status_options = [
			'pending',
			'completed',
			'refunded',
			'partially-refunded',
			'partially-paid',
			'failed',
			'cancelled',
		];

		$type_options_percentage = $type_options[0];
		$status_options_pending  = $status_options[0];
		$memberships             = $this->get_fake_data_generated('memberships');

		for ($i = 0; $i < $number; $i++) {
			$membership = $this->get_random_membership();

			$payment_data = [
				'parent_id'          => 0,
				'status'             => $faker->randomElement($status_options),
				'customer_id'        => $membership ? $membership->get_customer_id() : false,
				'membership_id'      => $membership ? $membership->get_id() : false,
				'product_id'         => $membership ? $membership->get_plan_id() : false,
				'currency'           => $membership ? $membership->get_currency() : false,
				'tax'                => 0.00,
				'credits'            => 0.00,
				'fees'               => 0.00,
				'discounts'          => 0.00,
				'discount_code'      => '',
				'gateway'            => '',
				'gateway_payment_id' => '',
				'date_created'       => $faker->dateTimeThisYear()->format('Y-m-d H:i:s'),
			];

			$payment = wu_create_payment($payment_data);

			if (is_wp_error($payment)) {
				throw new Exception(esc_html($payment->get_error_message()));
			} else {
				$payment->recalculate_totals()->save();

				$this->set_fake_data_generated('payments', $payment);
			}
		}
	}

	/**
	 * Generate a faker site.
	 *
	 * @since 2.0.0
	 * @param int    $number The number of fake data that will be generated.
	 * @param string $type The type of site to favor.
	 * @throws Exception In case of failures, an exception is thrown.
	 */
	public function generate_fake_site($number = 1, $type = 'customer_owned'): void {

		$faker = $this->get_faker();

		$type_options = [
			'default',
			'site_template',
			'customer_owned',
		];

		$type_customer_owned = $type_options[2];

		for ($i = 0; $i < $number; $i++) {
			$site_data = [];

			$title = rtrim((string) $faker->sentence(2), '.');
			$path  = strtolower(implode('-', explode(' ', $title)));
			$type  = $faker->optional(0.2, $type)->randomElement($type_options);

			$site_data['title']  = $title;
			$site_data['path']   = $path;
			$site_data['type']   = $type;
			$site_data['public'] = $faker->boolean(75);

			if ($type_customer_owned === $type) {
				$membership = $this->get_random_membership();

				if ($membership) {
					$site_data['customer_id']   = $membership->get_customer_id();
					$site_data['membership_id'] = $membership->get_id();
				}
			}

			$site = wu_create_site($site_data);

			if (is_wp_error($site)) {
				throw new Exception(esc_html($site->get_error_message()));
			} else {
				$this->set_fake_data_generated('sites', $site);
			}
		}
	}
}
