<?php
/**
 * Gateway Manager
 *
 * Manages the registering and activation of gateways.
 *
 * @package WP_Ultimo
 * @subpackage Managers/Gateway
 * @since 2.0.0
 */

namespace WP_Ultimo\Managers;

use WP_Ultimo\Dependencies\Psr\Log\LogLevel;
use \WP_Ultimo\Managers\Base_Manager;
use \WP_Ultimo\Gateways\Ignorable_Exception;

use \WP_Ultimo\Gateways\Free_Gateway;
use \WP_Ultimo\Gateways\Stripe_Gateway;
use \WP_Ultimo\Gateways\Stripe_Checkout_Gateway;
use \WP_Ultimo\Gateways\PayPal_Gateway;
use \WP_Ultimo\Gateways\Manual_Gateway;

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

/**
 * Manages the registering and activation of gateways.
 *
 * @since 2.0.0
 */
class Gateway_Manager extends Base_Manager {

	use \WP_Ultimo\Traits\Singleton;

	/**
	 * Lists the registered gateways.
	 *
	 * @since 2.0.0
	 * @var array
	 */
	protected $registered_gateways = array();

	/**
	 * Lists the gateways that are enabled.
	 *
	 * @since 2.0.0
	 * @var array
	 */
	protected $enabled_gateways = array();

	/**
	 * Keeps a list of the gateways with auto-renew.
	 *
	 * @since 2.0.0
	 * @var array
	 */
	protected $auto_renewable_gateways = array();

	/**
	 * Instantiate the necessary hooks.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function init() {

		add_action('plugins_loaded', array($this, 'on_load'));

	} // end init;

	/**
	 * Runs after all plugins have been loaded to allow for add-ons to hook into it correctly.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function on_load() {
		/*
		 * Adds our own default gateways.
		 */
		add_action('wu_register_gateways', array($this, 'add_default_gateways'), 5);

		/*
		 * Adds the Gateway selection fields
		 */
		add_action('init', array($this, 'add_gateway_selector_field'));

		/*
		 * Handle gateway confirmations.
		 * We need it both on the front-end and the back-end.
		 */
		add_action('template_redirect', array($this, 'process_gateway_confirmations'), -99999);
		add_action('load-admin_page_wu-checkout', array($this, 'process_gateway_confirmations'), -99999);

		/*
		 * Waits for webhook signals and deal with them.
		 */
		add_action('init', array($this, 'maybe_process_webhooks'), 1);

		/*
		 * Waits for webhook signals and deal with them.
		 */
		add_action('admin_init', array($this, 'maybe_process_v1_webhooks'), 1);

		/*
		 * Allow developers to add new gateways.
		 */
		do_action('wu_register_gateways');

	} // end on_load;

	/**
	 * Checks if we need to process webhooks received by gateways.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function maybe_process_webhooks() {

		$gateway = wu_request('wu-gateway');

		if ($gateway && !is_admin() && is_main_site()) {
			/*
			 * Do not cache this!
			 */
			!defined('DONOTCACHEPAGE') && define('DONOTCACHEPAGE', true); // phpcs:ignore

			try {
				/*
				 * Passes it down to Gateways.
				 *
				 * Gateways will hook into here
				 * to handle their respective webhook
				 * calls.
				 *
				 * We also wrap it inside a try/catch
				 * to make sure we log errors,
				 * tell network admins, and make sure
				 * the gateway tries again by sending back
				 * a non-200 HTTP code.
				 */
				do_action("wu_{$gateway}_process_webhooks");

				http_response_code(200);

				die('Thanks!');

			} catch (Ignorable_Exception $e) {

				$message = sprintf('We failed to handle a webhook call, but in this case, no further action is necessary. Message: %s', $e->getMessage());

				wu_log_add("wu-{$gateway}-webhook-errors", $message);

				/*
				 * Send the error back, but with a 200.
				 */
				wp_send_json_error(new \WP_Error('webhook-error', $message), 200);

			} catch (\Throwable $e) {

				$file = $e->getFile();
				$line = $e->getLine();

				$message = sprintf('We failed to handle a webhook call. Error: %s', $e->getMessage());

				$message .= PHP_EOL . "Location: {$file}:{$line}";
				$message .= PHP_EOL . $e->getTraceAsString();

				wu_log_add("wu-{$gateway}-webhook-errors", $message, LogLevel::ERROR);

				/*
				 * Force a 500.
				 *
				 * Most gateways will try again later when
				 * a non-200 code is returned.
				 */
				wp_send_json_error(new \WP_Error('webhook-error', $message), 500);

			} // end try;

		} // end if;

	} // end maybe_process_webhooks;

	/**
	 * Checks if we need to process webhooks received by legacy gateways.
	 *
	 * @since 2.0.4
	 * @return void
	 */
	public function maybe_process_v1_webhooks() {

		$action = wu_request('action', '');

		if ($action && strpos((string) $action, 'notify_gateway_') !== false) {
			/*
			 * Get the gateway id from the action.
			 */
			$gateway_id = str_replace(array('nopriv_', 'notify_gateway_'), '', (string) $action);

			$gateway = wu_get_gateway($gateway_id);

			if ($gateway) {

				$gateway->before_backwards_compatible_webhook();

				/*
				 * Do not cache this!
				 */
				!defined('DONOTCACHEPAGE') && define('DONOTCACHEPAGE', true); // phpcs:ignore

				try {
					/*
					 * Passes it down to Gateways.
					 *
					 * Gateways will hook into here
					 * to handle their respective webhook
					 * calls.
					 *
					 * We also wrap it inside a try/catch
					 * to make sure we log errors,
					 * tell network admins, and make sure
					 * the gateway tries again by sending back
					 * a non-200 HTTP code.
					 */
					do_action("wu_{$gateway_id}_process_webhooks");

					http_response_code(200);

					die('Thanks!');

				} catch (Ignorable_Exception $e) {

					$message = sprintf('We failed to handle a webhook call, but in this case, no further action is necessary. Message: %s', $e->getMessage());

					wu_log_add("wu-{$gateway_id}-webhook-errors", $message);

					/*
					* Send the error back, but with a 200.
					*/
					wp_send_json_error(new \WP_Error('webhook-error', $message), 200);

				} catch (\Throwable $e) {

					$message = sprintf('We failed to handle a webhook call. Error: %s', $e->getMessage());

					wu_log_add("wu-{$gateway_id}-webhook-errors", $message, LogLevel::ERROR);

					/*
					 * Force a 500.
					 *
					 * Most gateways will try again later when
					 * a non-200 code is returned.
					 */
					http_response_code(500);

					wp_send_json_error(new \WP_Error('webhook-error', $message));

				} // end try;

			} // end if;

		} // end if;

	} // end maybe_process_v1_webhooks;

	/**
	 * Let gateways deal with their confirmation steps.
	 *
	 * This is the case for PayPal Express.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function process_gateway_confirmations() {
		/*
		 * First we check for the confirmation parameter.
		 */
		if (!wu_request('wu-confirm') || (wu_request('status') && wu_request('status') === 'done')) {

			return;

		} // end if;

		ob_start();

		add_filter('body_class', fn($classes) => array_merge($classes, array('wu-process-confirmation')));

		$gateway_id = sanitize_text_field(wu_request('wu-confirm'));

		$gateway = wu_get_gateway($gateway_id);

		if (!$gateway) {

			$error = new \WP_Error('missing_gateway', __('Missing gateway parameter.', 'wp-ultimo'));

			wp_die($error, __('Error', 'wp-ultimo'), array('back_link' => true, 'response' => '200'));

		} // end if;

		try {

			$payment_hash = wu_request('payment');

			$payment = wu_get_payment_by_hash($payment_hash);

			if ($payment) {

				$gateway->set_payment($payment);

			} // end if;

			/*
			 * Pass it down to the gateway.
			 *
			 * Here you can throw exceptions, that
			 * we will catch it and throw it as a wp_die
			 * message.
			 */
			$results = $gateway->process_confirmation();

			if (is_wp_error($results)) {

				wp_die($results, __('Error', 'wp-ultimo'), array('back_link' => true, 'response' => '200'));

			} // end if;

		} catch (\Throwable $e) {

			$error = new \WP_Error('confirm-error-' . $e->getCode(), $e->getMessage());

			wp_die($error, __('Error', 'wp-ultimo'), array('back_link' => true, 'response' => '200'));

		} // end try;

		$output = ob_get_clean();

		if (!empty($output)) {
			/*
			 * Add a filter to bypass the checkout form.
			 * This is used for PayPal confirmation page.
			 */
			add_action('wu_bypass_checkout_form', fn($bypass, $atts) => $output, 10, 2);

		} // end if;

	} // end process_gateway_confirmations;

	/**
	 * Adds the field that enabled and disables Payment Gateways on the settings.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function add_gateway_selector_field() {

		wu_register_settings_field('payment-gateways', 'active_gateways', array(
			'title'   => __('Active Payment Gateways', 'wp-ultimo'),
			'desc'    => __('Payment gateways are what your customers will use to pay.', 'wp-ultimo'),
			'type'    => 'multiselect',
			'columns' => 2,
			'options' => array($this, 'get_gateways_as_options'),
			'default' => array(),
		));

	} // end add_gateway_selector_field;

	/**
	 * Returns the list of registered gateways as options for the gateway selector setting.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	public function get_gateways_as_options() {
		/*
		 * We use this to order the options.
		 */
		$active_gateways = wu_get_setting('active_gateways', array());

		$gateways = $this->get_registered_gateways();

		$gateways = array_filter($gateways, fn($item) => $item['hidden'] === false);

		return $gateways;

	} // end get_gateways_as_options;

	/**
	 * Loads the default gateways.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function add_default_gateways() {
		/*
		 * Free Payments
		 */
		wu_register_gateway('free', __('Free', 'wp-ultimo'), '', Free_Gateway::class, true);

		/*
		 * Stripe Payments
		 */
		$stripe_desc = __('Stripe is a suite of payment APIs that powers commerce for businesses of all sizes, including subscription management.', 'wp-ultimo');
		wu_register_gateway('stripe', __('Stripe', 'wp-ultimo'), $stripe_desc, Stripe_Gateway::class);

		/*
		 * Stripe Checkout Payments
		 */
		$stripe_checkout_desc = __('Stripe Checkout is the hosted solution for checkouts using Stripe.', 'wp-ultimo');
		wu_register_gateway('stripe-checkout', __('Stripe Checkout', 'wp-ultimo'), $stripe_checkout_desc, Stripe_Checkout_Gateway::class);

		/*
		 * PayPal Payments
		 */
		$paypal_desc = __('PayPal is the leading provider in checkout solutions and it is the easier way to get your network subscriptions going.', 'wp-ultimo');
		wu_register_gateway('paypal', __('PayPal', 'wp-ultimo'), $paypal_desc, PayPal_Gateway::class);

		/*
		 * Manual Payments
		 */
		$manual_desc = __('Use the Manual Gateway to allow users to pay you directly via bank transfers, checks, or other channels.', 'wp-ultimo');
		wu_register_gateway('manual', __('Manual', 'wp-ultimo'), $manual_desc, Manual_Gateway::class);

	} // end add_default_gateways;

	/**
	 * Checks if a gateway was already registered.
	 *
	 * @since 2.0.0
	 * @param string $id The id of the gateway.
	 * @return boolean
	 */
	public function is_gateway_registered($id) {

		return is_array($this->registered_gateways) && isset($this->registered_gateways[$id]);

	} // end is_gateway_registered;

	/**
	 * Returns a list of all the registered gateways
	 *
	 * @since 2.0.0
	 * @return array
	 */
	public function get_registered_gateways() {

		return $this->registered_gateways;

	} // end get_registered_gateways;

	/**
	 * Returns a particular Gateway registered
	 *
	 * @since 2.0.0
	 * @param string $id The id of the gateway.
	 * @return array
	 */
	public function get_gateway($id) {

		return $this->is_gateway_registered($id) ? $this->registered_gateways[$id] : false;

	} // end get_gateway;

	/**
	 * Adds a new Gateway to the System. Used by gateways to make themselves visible.
	 *
	 * @since 2.0.0
	 *
	 * @param string $id ID of the gateway. This is how we will identify the gateway in the system.
	 * @param string $title Name of the gateway.
	 * @param string $desc A description of the gateway to help super admins understand what services they integrate with.
	 * @param string $class_name Gateway class name.
	 * @param bool   $hidden If we need to hide this gateway publicly.
	 * @return bool
	 */
	public function register_gateway($id, $title, $desc, $class_name, $hidden = false) {

		// Checks if gateway was already added
		if ($this->is_gateway_registered($id)) {

			return;

		} // end if;

		$active_gateways = (array) wu_get_setting('active_gateways', array());

		// Adds to the global
		$this->registered_gateways[$id] = array(
			'id'         => $id,
			'title'      => $title,
			'desc'       => $desc,
			'class_name' => $class_name,
			'active'     => in_array($id, $active_gateways, true),
			'active'     => in_array($id, $active_gateways, true),
			'hidden'     => (bool) $hidden,
			'gateway'    => $class_name, // Deprecated.
		);

		$this->install_hooks($class_name);

		// Return the value
		return true;

	} // end register_gateway;

	/**
	 * Adds additional hooks for each of the gateway registered.
	 *
	 * @since 2.0.0
	 *
	 * @param string $class_name Gateway class name.
	 * @return void
	 */
	public function install_hooks($class_name) {

		$gateway = new $class_name();

		$gateway_id = $gateway->get_id();

		/*
		 * If the gateway supports recurring
		 * payments, add it to the list.
		 */
		if ($gateway->supports_recurring()) {

			$this->auto_renewable_gateways[] = $gateway_id;

		} // end if;

		add_action('wu_checkout_scripts', array($gateway, 'register_scripts'));

		add_action('init', array($gateway, 'hooks'));

		add_action('init', array($gateway, 'settings'));

		add_action("wu_{$gateway_id}_process_webhooks", array($gateway, 'process_webhooks'));

		add_action("wu_{$gateway_id}_remote_payment_url", array($gateway, 'get_payment_url_on_gateway'));

		add_action("wu_{$gateway_id}_remote_subscription_url", array($gateway, 'get_subscription_url_on_gateway'));

		add_action("wu_{$gateway_id}_remote_customer_url", array($gateway, 'get_customer_url_on_gateway'));

		/*
		 * Renders the gateway fields.
		 */
		add_action('wu_checkout_gateway_fields', function($checkout) use ($gateway) {

			$field_content = call_user_func(array($gateway, 'fields'));

			ob_start();

			?>

			<div v-cloak v-show="gateway == '<?php echo esc_attr($gateway->get_id()); ?>' && order && order.should_collect_payment" class="wu-overflow">

				<?php echo $field_content; ?>

			</div>

			<?php

			echo ob_get_clean();

		});

	} // end install_hooks;

	/**
	 * Returns an array with the list of gateways that support auto-renew.
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_auto_renewable_gateways() {

		return (array) $this->auto_renewable_gateways;

	} // end get_auto_renewable_gateways;

} // end class Gateway_Manager;
