<?php
/**
 * A class that handles integration with the Mollie API for payments.
 *
 * @since 3.0.0
 */

declare( strict_types=1 );
namespace FeData\FastEvents\Payment\Provider;

use FeData\FastEvents\Helpers\EventsHelper;
use FeData\FastEvents\Payment\AbstractPayment;
use FeData\FastEvents\Utilities\PluginUtil;
use FeData\FastEvents\Utilities\TimeUtil;
use stdClass;
use WP_Error;
use WP_REST_Request;
use WP_User;

defined( 'ABSPATH' ) || exit;

/**
 * A class that handles integration with the Mollie API for payments.
 */
final class MollieProvider extends AbstractPayment {

	/**
	 * Provider name.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected static string $provider_name = 'Mollie';

	/**
	 * Name of the body encoder function.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	protected static string $encoder = 'wp_json_encode';

	/**
	 * Provider base URL for API calls.
	 *
	 * @since 3.0.0
	 * @var string
	 */
	private const BASE_URL = 'https://api.mollie.com/v2/';


	/**
	 * Installation activities of the payment provider. It can be called multiple times!
	 * There is nothing to install for Mollie.
	 *
	 * @since 3.0.0
	 *
	 * @return bool|WP_Error Returns true if the installation was successful.
	 */
	public function install(): bool|WP_Error {
		return true;
	}

	/**
	 * Uninstallation activities of the payment provider.
	 * There is nothing to uninstall for Mollie.
	 *
	 * @since 3.0.0
	 */
	public function uninstall(): void {}

	/**
	 * Create a payment.
	 *
	 * @since 3.0.0
	 *
	 * @param EventsHelper $events_helper The events helper.
	 *                                    It contains all the methods to set up a payment.
	 * @param bool         $test          Use test mode or not (default: false).
	 * @return array|WP_Error Returns an array with the payment details ('id' and 'url').
	 */
	public function create_payment( EventsHelper $events_helper, bool $test = false ): array|WP_Error {
		$this->payload = array(
			'amount'      => array(
				'currency' => $this->settings->currency(),
				'value'    => number_format( $events_helper->get_amount(), 2, '.', '' ),
			),
			'description' => count( $events_helper->get_order_ids() ) > 1
						? 'Order #' . implode( ',#', $events_helper->get_order_ids() ) . ' (' . $events_helper->get_event_group() . ')'
						: 'Order #' . $events_helper->get_order_ids()[0] . ' (' . $events_helper->get_event_name() . ')',
			// Saas mode requires application fees, which is not supported in 'manual' capture mode.
			'captureMode' => $this->settings->saas_mode() ? 'automatic' : 'manual',
			'metadata'    => array(
				'order_ids'    => $events_helper->get_order_ids(),
				'event_ids'    => $events_helper->get_event_ids(),
				'fe_order_uid' => $events_helper->get_order_uid(),
				'test'         => $test,
				'fe_version'   => $GLOBALS['fast_events']::FAST_EVENTS_VERSION,
			),
			'cancelUrl'   => home_url() . '/wp-json/fast-events/v1/payment/cancel/' . $events_helper->get_order_uid(),
			'redirectUrl' => home_url() . '/wp-json/fast-events/v1/payment/redirect/' . $events_helper->get_order_uid(),
			'webhookUrl'  => home_url() . '/wp-json/fast-events/v1/payment/webhook/' . $events_helper->get_order_uid(),
		);
		$this->set_headers( $events_helper->get_saas_user_id(), $test );
		if ( $this->settings->saas_mode() && 0 !== $events_helper->get_saas_user_id() ) {
			$this->set_saas_fields( $events_helper, $test );
		}
		$response = $this->api_call( self::BASE_URL . 'payments', 'POST' );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Look up the payment ID and redirect URL from the response.
		return array(
			'id'  => $response['id'],
			'url' => $response['_links']['checkout']['href'],
		);
	}

	/**
	 * Retrieve the unique order uid and id from the webhook request parameters.
	 *
	 * @since 3.0.0
	 *
	 * @param WP_REST_Request $data The webhook request.
	 * @return string|WP_Error Returns an array with the payment details ('id' and 'uid').
	 */
	public function get_uid( WP_REST_Request $data ): array|WP_Error {
		$params = $data->get_params();
		if ( empty( $params['uid'] ) || ! is_string( $params['uid'] ) || 40 !== strlen( $params['uid'] ) || empty( $params['id'] ) ) {
			$this->logger->warning(
				'Invalid order "uid" or missing "id" in Mollie webhook request',
				array(
					'type'           => 'Mollie webhook',
					'ip'             => PluginUtil::get_ip(),
					'provider'       => self::$provider_name,
					'request_params' => $params,
				)
			);
			return new WP_Error( 'invalid_request', __( 'Invalid order "uid" or missing "id"', 'fast-events' ) );
		}
		return array(
			'id'  => $params['id'],
			'uid' => $params['uid'],
		);
	}

	/**
	 * Retrieve a single payment object by its payment ID.
	 *
	 * @since 3.0.0
	 *
	 * @param string   $payment_id The unique ID of the payment.
	 * @param int      $user_id    The ID of the user.
	 * @param stdClass $order      The order object.
	 * @param bool     $test       Use test mode or not (default: false).
	 * @return array|WP_Error    Returns an array with the payment details.
	 */
	public function get_payment( string $payment_id, int $user_id, stdClass $order, bool $test = false ): array|WP_Error {
		global $wpdb;

		$endpoint = self::BASE_URL . 'payments/' . $payment_id . '?embed=refunds,chargebacks';
		$this->set_headers( $user_id, $test );
		if ( $this->settings->saas_mode() && 0 !== $user_id && $test ) {
			$endpoint .= '&testmode=true';
		}
		$response = $this->api_call( $endpoint, 'GET' );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Fill the array with the payment details.
		$json                  = json_decode( $order->input_fields, true );
		$ids                   = empty( $json['orders'] ) ? array( (int) $order->id ) : array_map( 'intval', $json['orders'] );
		$result                = array();
		$result['status']      = $response['status'];
		$result['description'] = $response['description'];
		$result['amount']      = (float) $response['amount']['value'];
		$result['currency']    = $response['amount']['currency'];
		$result['created']     = $order->created_at;
		$result['metadata']    = $response['metadata'];

		// Are there any customer details?
		if ( ! empty( $response['details'] ) ) {
			if ( ! empty( $response['details']['consumerName'] ) ) {
				$result['customer'][ __( 'Customer', 'fast-events' ) ] = $response['details']['consumerName'];
			}
			if ( ! empty( $response['details']['cardHolder'] ) ) {
				$result['customer'][ __( 'Customer', 'fast-events' ) ] = $response['details']['cardHolder'];
			}
			if ( ! empty( $response['details']['consumerAccount'] ) ) {
				$result['customer'][ __( 'Card number', 'fast-events' ) ] = '**********' . substr( $response['details']['consumerAccount'], -4 );
			}
			if ( ! empty( $response['details']['cardNumber'] ) ) {
				$result['customer'][ __( 'Card number', 'fast-events' ) ] = '**********' . substr( $response['details']['cardNumber'], -4 );
			}
		}
		$result['customer'][ __( 'Transaction ID', 'fast-events' ) ] = $response['id'];
		if ( ! empty( $response['method'] ) ) {
			$result['customer'][ __( 'Payment method', 'fast-events' ) ] = $response['method'];
		}

		$result['actions'][ __( 'Created', 'fast-events' ) ] = iso8601_to_datetime( $response['createdAt'] );

		// Is the metadata expired set?
		if ( ! empty( $json['metadata']['expired_at'] ) ) {
			$result['actions'][ __( 'Expired', 'fast-events' ) ] = $json['metadata']['expired_at'];
		}

		// Was the payment authorized before?
		if ( ! empty( $json['metadata']['authorized_at'] ) ) {
			$result['actions'][ __( 'Authorized', 'fast-events' ) ] = $json['metadata']['authorized_at'];
		}

		// Is it authorized?
		if ( 'authorized' === $response['status'] ) {
			if ( empty( $json['metadata']['authorized_at'] ) ) {
				$date = iso8601_to_datetime( $response['authorizedAt'] );
				$result['actions'][ __( 'Authorized', 'fast-events' ) ] = $date;

				// Update metadata.
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.authorized_at', '$date' ) " .  // phpcs:ignore
					'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
				);
			} else {
				$result['actions'][ __( 'Authorized', 'fast-events' ) ] = $json['metadata']['authorized_at'];
			}
		}

		// Is it paid?
		if ( 'paid' === $response['status'] ) {
			$result['actions'][ __( 'Paid', 'fast-events' ) ] = iso8601_to_datetime( $response['paidAt'] );

			// Check if the order was already expired. The payment method did not support manual capture, so the order was not released.
			// So we immediately refund the order and set the 'refunded' status in the order.
			if ( ! empty( $json['metadata']['expired_at'] ) ) {
				$result['status'] = 'refunded';
				if ( empty( $json['metadata']['refunded_msg'] ) ) {
					$wpdb->query(
						$wpdb->prepare(
							"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status ='refunded', " .
							"input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.refunded_msg', %s ) " .
							'WHERE id IN (' . implode( ',', $ids ) . ')', // phpcs:ignore
							__( 'After the order had expired, the user still paid → the amount was immediately refunded in full', 'fast-events' )
						)
					);

					// Log the error for each order.
					foreach ( $ids as $key => $order_id ) {
						$this->logger->warning(
							esc_html__( 'After the order had expired, the user still paid → the amount was immediately refunded in full', 'fast-events' ),
							array(
								'type'     => 'Mollie webhook',
								'order_id' => (int) $order_id,
								'event_id' => (int) $json['events'][ $key ],
								'provider' => self::$provider_name,
							)
						);
					}
					$this->refund( $order, __( 'Order refunded', 'fast-events' ), $result['amount'], array(), $user_id, $test );
				}
			}
		}

		// Was the payment canceled before?
		if ( ! empty( $json['metadata']['canceled_at'] ) ) {
			$result['actions'][ __( 'Canceled', 'fast-events' ) ] = $json['metadata']['canceled_at'];
		}

		// Is it cancelled?
		if ( 'canceled' === $response['status'] ) {
			if ( empty( $json['metadata']['canceled_at'] ) ) {
				$date = iso8601_to_datetime( $response['canceledAt'] );
				$result['actions'][ __( 'Canceled', 'fast-events' ) ] = $date;

				// Update metadata.
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.canceled_at', '$date' ) " .  // phpcs:ignore
					'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
				);
			} else {
				$result['actions'][ __( 'Canceled', 'fast-events' ) ] = $json['metadata']['canceled_at'];
			}
		}

		// Is it expired?
		if ( 'expired' === $response['status'] ) {
			if ( empty( $json['metadata']['expired_at'] ) ) {
				$date = iso8601_to_datetime( $response['expiredAt'] );
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.expired_at', '$date' ) " .  // phpcs:ignore
					'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
				);
			} else {
				$result['actions'][ __( 'Expired', 'fast-events' ) ] = $json['metadata']['expired_at'];
			}
		}

		// Has it failed?
		if ( 'failed' === $response['status'] ) {
			$result['actions'][ __( 'Failed', 'fast-events' ) ] = iso8601_to_datetime( $response['failedAt'] );
		}

		// Is there an application fee?
		if ( ! empty( $response['applicationFee'] ) ) {
			if ( empty( $json['metadata']['expired_at'] ) && empty( $json['metadata']['canceled_at'] ) ) {
				$fee = $response['applicationFee'];
				$result['actions'][ __( 'Application fee', 'fast-events' ) ] =
					$fee['amount']['currency'] . ' ' . number_format_i18n( (float) $fee['amount']['value'], 2 );
			}
			if ( ! empty( $response['metadata']['saas']['organization'] ) ) {
				$result['actions'][ __( 'Merchant', 'fast-events' ) ] = $response['metadata']['saas']['organization'];
			}
		}

		// Information messages.
		if ( ! empty( $json['metadata']['expired_msg'] ) ) {
			$result['actions'][ __( 'Expire info', 'fast-events' ) ] = $json['metadata']['expired_msg'];
		}
		if ( ! empty( $json['metadata']['refunded_msg'] ) ) {
			$result['actions'][ __( 'Refund info', 'fast-events' ) ] = $json['metadata']['refunded_msg'];
		}

		// Are there any refunds?
		$total_refunds = 0.0;
		if ( ! empty( $response['_embedded']['refunds'] ) ) {
			foreach ( $response['_embedded']['refunds'] as $refund ) {
				$total_refunds      += (float) $refund['amount']['value'];
				$result['refunds'][] = array(
					'amount'   => $refund['amount']['value'],
					'currency' => $refund['amount']['currency'],
					'date'     => iso8601_to_datetime( $refund['createdAt'] ),
					'order_id' => empty( $refund['metadata']['order_id'] ) ? '' : $refund['metadata']['order_id'],
				);
			}
		}
		$result['total_refunds'] = $total_refunds;
		$result['has_refunds']   = ( 0.0 !== $total_refunds );

		// Set the metadata of the order.
		if ( $result['has_refunds'] && empty( $json['metadata']['has_refunds'] ) ) {
			$wpdb->query(
				"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.has_refunds', '1' ) " .
				'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
			);
		}

		// Are there any chargebacks?
		$result['has_chargebacks'] = ! empty( $response['_embedded']['chargebacks'] );

		// Set the metadata of the order.
		if ( $result['has_chargebacks'] && empty( $json['metadata']['has_chargebacks'] ) ) {
			$wpdb->query(
				"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.has_chargebacks', '1' ) " .
				'WHERE id IN (' . implode( ',', $ids ) . ')' // phpcs:ignore
			);
		}

		// Is the dashboard link already stored?
		if ( empty( $json['metadata']['dashboard_link'] ) && isset( $response['_links']['dashboard']['href'] ) ) {
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = " .
					"JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.dashboard_link', %s ) " .
					'WHERE id IN (' . implode( ',', $ids ) . ')', // phpcs:ignore
					$response['_links']['dashboard']['href']
				)
			);
			$json['metadata']['dashboard_link'] = $response['_links']['dashboard']['href'];
		}
		$result['dashboard_link'] = ! empty( $json['metadata']['dashboard_link'] ) ? $json['metadata']['dashboard_link'] : '';

		return $result;
	}

	/**
	 * Capture the payment.
	 *
	 * @since 3.0.0
	 *
	 * @param string   $payment_id The unique ID of the payment.
	 * @param int      $user_id    The ID of the user.
	 * @param stdClass $order      The order object.
	 * @param bool     $test       Use test mode or not (default: false).
	 * @return array|WP_Error      Returns an array with the payment details.
	 */
	public function capture_payment( string $payment_id, int $user_id, stdClass $order, bool $test = false ): array|WP_Error {
		global $wpdb;

		// Capture the payment if the original status = 'authorized'. If it is 'expired', release the funds again.
		// In the settings an expiry timer is defined, if it fires, it sets the 'expired' status in the order.
		// Unfortunately, in Mollie you can't remove the payment, so in theory the user still can continue with the order after the timer has fired.
		// By setting the 'captureMode' to 'manual' in the 'create_payment' method, the payment is not captured automatically for payment methods supporting this.
		// iDEAL does not support this, so it advances immediately to 'paid' even if the order has 'expired'.
		// This would require manual action in the Mollie dashboard to fully refund the order.
		$json          = json_decode( $order->input_fields, true );
		$ids           = empty( $json['orders'] ) ? array( (int) $order->id ) : array_map( 'intval', $json['orders'] );
		$this->payload = array();
		$this->set_headers( $user_id, $test );
		if ( 'authorized' === $order->payment_status && empty( $json['metadata']['expired_at'] ) ) {
			$this->api_call( self::BASE_URL . 'payments/' . $payment_id . '/captures', 'POST' );
		} elseif ( ! empty( $json['metadata']['expired_at'] ) ) {
			if ( $this->settings->saas_mode() && 0 !== $user_id && $test ) {
				$this->payload['testmode'] = true;
			}
			if ( $this->settings->saas_mode() && 0 !== $user_id ) {
				$this->payload['profileId'] = $this->user_meta->get_saas_profile_id( $user_id );
			}
			$this->api_call( self::BASE_URL . 'payments/' . $payment_id . '/release-authorization', 'POST' );

			// The user approved the order, but it expired. Show in payment details that it was canceled and no charge was made.
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$wpdb->prefix}fe_event_orders SET payment_status ='expired', " .
					"input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.expired_msg', %s ) " .
					'WHERE id IN (' . implode( ',', $ids ) . ')', // phpcs:ignore
					__( 'The user tried, after the order had expired, to approve it → the payment was cancelled and no charge was made', 'fast-events' )
				)
			);

			// Log the error for each order.
			foreach ( $ids as $key => $order_id ) {
				$this->logger->warning(
					esc_html__( 'The user tried, after the order had expired, to approve it → the payment was cancelled and no charge was made', 'fast-events' ),
					array(
						'type'     => 'Mollie webhook',
						'order_id' => (int) $order_id,
						'event_id' => (int) $json['events'][ $key ],
						'provider' => self::$provider_name,
					)
				);
			}
		}

		return array();
	}

	/**
	 * Cancel a payment.
	 * The current Mollie v2 API implementation does not support canceling payments, except for a few isolated cases.
	 * SO this is not implemented here.
	 *
	 * @since 3.0.0
	 *
	 * @param string $payment_id The unique ID of the payment.
	 * @param int    $user_id    The ID of the user.
	 * @param bool   $test       Use test mode or not (default: false).
	 * @return array|WP_Error Returns an array with the payment details ('id' and 'url').
	 */
	public function cancel_payment( string $payment_id, int $user_id, bool $test = false ): array|WP_Error {
		return array();
	}

	/**
	 * Refund a payment.
	 *
	 * @since 3.0.0
	 *
	 * @param stdClass $order       The order object.
	 * @param string   $description The description of the payment.
	 * @param float    $amount      The amount of the payment.
	 * @param array    $metadata    Any metadata you want to store with the payment.
	 * @param int      $user_id     The ID of the user.
	 * @param bool     $test        Use test mode or not (default: false).
	 * @return array|WP_Error       Returns an array with the payment details ('id' and 'url').
	 */
	public function refund( stdClass $order, string $description, float $amount, array $metadata, int $user_id, bool $test = false ): array|WP_Error {
		$this->payload = array(
			'amount'      => array(
				'currency' => $this->settings->currency(),
				'value'    => number_format( $amount, 2, '.', '' ),
			),
			'description' => $description,
			'metadata'    => $metadata,
		);
		$this->set_headers( $user_id, $test );
		if ( $this->settings->saas_mode() && 0 !== $user_id && $test ) {
			$this->payload['testmode'] = true;
		}
		$response = $this->api_call( self::BASE_URL . 'payments/' . $order->payment_id . '/refunds', 'POST' );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		// Return the refund id.
		return array(
			'id' => $response['id'],
		);
	}

	/**
	 * Generate the authorization URL (OAuth url) for the payment provider.
	 *
	 * @since 3.0.0
	 *
	 * @param WP_User $user The user object.
	 * @return string The authorization URL.
	 */
	public function get_oauth_url( WP_User $user ): string {
		$args = array(
			'approval_prompt' => 'force',
			'response_type'   => 'code',
			'scope'           => implode(
				' ',
				array(
					'organizations.read',
					'payments.read',
					'payments.write',
					'profiles.read',
					'refunds.read',
					'refunds.write',
				)
			),
			'client_id'       => $this->settings->saas_client_id(),
			'state'           => $this->get_state( $user ),
		);
		return add_query_arg( $args, 'https://my.mollie.com/oauth2/authorize' );
	}

	/**
	 * Revoke SaaS authorization.
	 *
	 * @since 3.0.0
	 *
	 * @param int $user_id The ID of the user.
	 * @return WP_Error|string
	 */
	public function revoke_authorization( int $user_id ): WP_Error|string {
		$config        = $this->user_meta->get_saas_config( $user_id );
		$this->payload = array(
			'token_type_hint' => 'refresh_token',
			'token'           => $config->refresh_token ?? '',
		);
		$this->headers = array(
			'Authorization' => 'Basic ' . base64_encode( $this->settings->saas_client_id() . ':' . $this->settings->saas_client_secret() ), // phpcs:ignore
		);
		$response      = $this->api_call( 'https://api.mollie.com/oauth2/tokens', 'DELETE' );
		if ( is_wp_error( $response ) ) {
			return $response;
		}
		return '';
	}

	/**
	 * Get an access token from auth code.
	 *
	 * @param int    $user_id The ID of the user.
	 * @param string $state   The state (nonce) parameter from the webhook request.
	 * @param array  $params  The parameters in the webhook request.
	 * @return WP_Error|string The authorization URL.
	 *
	 * @since 3.0.0
	 */
	public function get_access_token_from_auth_token( int $user_id, string $state, array $params ): WP_Error|string {

		// Verify the state parameter (nonce).
		if ( ! $this->verify_state( $state, $user_id ) ) {
			return new WP_Error( 'invalid_state', __( 'Illegal authorization state value', 'fast-events' ) );
		}

		// Exchange it for an access token.
		$this->payload = array(
			'grant_type' => 'authorization_code',
			'code'       => $params['code'],
		);
		$this->headers = array(
			'Authorization' => 'Basic ' . base64_encode( $this->settings->saas_client_id() . ':' . $this->settings->saas_client_secret() ), // phpcs:ignore
		);
		$config        = $this->api_call( 'https://api.mollie.com/oauth2/tokens', 'POST' );
		if ( is_wp_error( $config ) ) {
			return $config;
		}

		// Add the local time to the config, so we know when to refresh the access token.
		$config['local_time'] = TimeUtil::local_time();
		$this->user_meta->set_saas_config( $user_id, $config );

		// Get the merchant name.
		$this->payload = array();
		$this->headers = array(
			'Authorization' => 'Bearer ' . $config['access_token'], // phpcs:ignore
		);
		$response      = $this->api_call( self::BASE_URL . 'organizations/me', 'GET' );
		if ( is_wp_error( $response ) ) {
			$this->revoke_authorization( $user_id );
			return $response;
		}
		$this->user_meta->set_saas_merchant_name( $user_id, $response['name'] );
		$this->logger->info(
			'Mollie merchant name: ' . $response['name'],
			array(
				'type'  => 'Mollie merchant name',
				'id'    => $response['id'],
				'name'  => $response['name'],
				'email' => $response['email'],
			)
		);

		// Get all profiles from the merchant and pick the first one (for now).
		// In the future we may present a choice if multiple profiles are found.
		$profiles = $this->api_call( self::BASE_URL . 'profiles', 'GET' );
		if ( is_wp_error( $profiles ) ) {
			$this->revoke_authorization( $user_id );
			return $profiles;
		}
		foreach ( $profiles['_embedded']['profiles'] as $profile ) {
			$this->user_meta->set_saas_profile_id( $user_id, $profile['id'] );
			return '';
		}

		// No profile found.
		$this->revoke_authorization( $user_id );
		return new WP_Error( 'no_profile', __( 'No Mollie profile found', 'fast-events' ) );
	}

	/**
	 * Get an access token from auth code.
	 *
	 * @param int $user_id The ID of the user.
	 * @return string The access token.
	 *
	 * @since 3.0.0
	 */
	public function get_access_token( int $user_id ): string {

		// Find the saas info.
		$user   = wp_set_current_user( $user_id );
		$config = $this->user_meta->get_saas_config( $user_id );
		if ( null === $config || ! isset( $config->refresh_token ) || ! isset( $config->local_time ) || ! isset( $config->expires_in ) ) {
			$this->logger->warning(
				'SaaS information not found for user: ' . $user_id,
				array(
					'type' => 'Mollie SaaS information',
					'id'   => $user->ID,
					'name' => $user->display_name,
				)
			);
			return '';
		}

		// Is the access token still valid? We use a 60-second safety margin.
		if ( ( $config->local_time + $config->expires_in - 60 ) > TimeUtil::local_time() ) {
			return $config->access_token;
		}

		// Get the new access token.
		$save_payload  = $this->payload;
		$save_headers  = $this->headers;
		$this->payload = array(
			'grant_type'    => 'refresh_token',
			'refresh_token' => $config->refresh_token,
		);
		$this->headers = array(
			'Authorization' => 'Basic ' . base64_encode( $this->settings->saas_client_id() . ':' . $this->settings->saas_client_secret() ), // phpcs:ignore
		);
		$response      = $this->api_call( 'https://api.mollie.com/oauth2/tokens', 'POST' );
		$this->payload = $save_payload;
		$this->headers = $save_headers;
		if ( is_wp_error( $response ) ) {
			$this->revoke_authorization( $user->ID );
			$this->user_meta->delete_saas_config( $user->ID );
			$this->user_meta->delete_saas_merchant_name( $user->ID );
			$this->user_meta->delete_saas_profile_id( $user->ID );
			return 'NOT_FOUND';
		}
		$response['local_time'] = TimeUtil::local_time();
		$this->user_meta->set_saas_config( $user->ID, $response );
		return $response['access_token'];
	}

	/**
	 * The provider-specific default headers.
	 *
	 * @since 3.0.0
	 *
	 * @return array Returns the default headers.
	 */
	protected function default_headers(): array {
		return array(
			'Accept'       => 'application/json',
			'Content-Type' => 'application/json',
		);
	}

	/**
	 * The provider-specific error analysis.
	 *
	 * @since 3.0.0
	 *
	 * @param array $error  The error body payload.
	 * @return string Returns the proper error message.
	 */
	protected function analyse_error( array $error ): string {
		if ( isset( $error['error_description'] ) ) {
			return $error['error_description'];
		}
		return ( $error['title'] ?? '' ) . ': ' . ( $error['detail'] ?? '' );
	}

	/**
	 * If we are operating in SaaS mode, we need to set additional fields for the Mollie API call.
	 *
	 * @since 3.0.0
	 *
	 * @param EventsHelper $events_helper The events helper.
	 * @param bool         $test Use test mode or not (default: false).
	 */
	private function set_saas_fields( EventsHelper $events_helper, bool $test ): void {

		// We only need to set the testmode if we are testing.
		if ( $test ) {
			$this->payload['testmode'] = true;
		}

		// Set profile ID.
		$this->payload['profileId'] = $this->user_meta->get_saas_profile_id( $events_helper->get_saas_user_id() );

		// There are no fees, just return.
		if ( ( is_null( $events_helper->get_saas_fee() ) || 0.00 === $events_helper->get_saas_fee() ) && 0.00 === $this->settings->saas_client_fee() ) {
			return;
		}

		// Get the merchant name.
		$org_name = $this->user_meta->get_saas_merchant_name( $events_helper->get_saas_user_id() );

		// Calculate the fee based on order or per ticket.
		if ( is_null( $events_helper->get_saas_fee() ) || 0.00 === $events_helper->get_saas_fee() ) {
			$fee = $this->settings->saas_client_fee();
		} else {
			$fee = $events_helper->get_saas_fee();
		}
		if ( 1 === $this->settings->saas_client_fee_per() ) {
			$total     = count( $events_helper->get_order_ids() );
			$fee_total = $total * $fee;
			$meta_desc = 1 === $total ? esc_html__( 'order', 'fast-events' ) : esc_html__( 'orders', 'fast-events' );
			$per       = esc_html__( 'per order', 'fast-events' );
		} else {
			$total     = $events_helper->get_num_tickets();
			$fee_total = $total * $fee;
			$meta_desc = 1 === $total ? esc_html__( 'ticket', 'fast-events' ) : esc_html__( 'tickets', 'fast-events' );
			$per       = esc_html__( 'per ticket', 'fast-events' );
		}
		$this->payload['metadata']['saas']['fee']              = $this->settings->currency() . ' ' . number_format( $fee, 2, '.', '' ) . ' ' . $per;
		$this->payload['metadata']['saas']['fee_desc']         = $total . ' ' . $meta_desc;
		$this->payload['metadata']['saas']['fee_amount']       = $this->settings->currency() . ' ' . number_format( $fee_total, 2, '.', '' );
		$this->payload['metadata']['saas']['profileId']        = $this->payload['profileId'];
		$this->payload['metadata']['saas']['organization']     = $org_name;
		$this->payload['applicationFee']['amount']['currency'] = $this->settings->currency();
		$this->payload['applicationFee']['amount']['value']    = number_format( $fee_total, 2, '.', '' );

		// Create the fee description depending on the # of orders.
		$desc = esc_html__( 'App fee order #', 'fast-events' );
		if ( count( $events_helper->get_order_ids() ) > 1 ) {
			$desc .= implode( ',#', $events_helper->get_order_ids() ) . ': ' . $events_helper->get_event_group() . '( ' . $org_name . ') [' . $total . ' ' . $meta_desc . ']';
		} else {
			$desc .= $events_helper->get_order_ids()[0] . ': ' . $events_helper->get_event_name() . ' (' . $org_name . ') [' . $total . ' ' . $meta_desc . ']';
		}
		$this->payload['applicationFee']['description'] = $desc;
	}

	/**
	 * Create the headers for the Mollie API call.
	 *
	 * @since 3.0.0
	 *
	 * @param int  $user_id The ID of the user.
	 * @param bool $test    Use test mode or not (default: false).
	 */
	private function set_headers( int $user_id, bool $test ): void {
		$this->headers['Authorization'] = ( $this->settings->saas_mode() && 0 !== $user_id )
			? 'Bearer ' . $this->get_access_token( $user_id )
			: 'Bearer ' . ( $test
				? $this->settings->payment_property( 'test_key' )
				: $this->settings->payment_property( 'live_key' )
			);
	}
}
