<?php
/**
 * A class that handles integration with the Stripe 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 stdClass;
use WP_Error;
use WP_REST_Request;
use WP_User;

defined( 'ABSPATH' ) || exit;

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

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

	/**
	 * Encoding function for the payload.
	 *
	 * @since 3.0
	 * @var string
	 */
	protected static string $encoder = 'http_build_query';

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

	/**
	 * Array of webhook events we are subscribed to.
	 *
	 * @since 3.0.0
	 * @var array
	 */
	private const ALLOWED_EVENTS = array(
		'charge.dispute.created',
		'charge.refunded',
		'checkout.session.async_payment_failed',
		'checkout.session.async_payment_succeeded',
		'checkout.session.expired',
		'payment_intent.canceled',
		'payment_intent.payment_failed',
		'payment_intent.succeeded',
	);


	/**
	 * Installation activities of the payment provider. It can be called multiple times!
	 * Install the webhooks for Stripe.
	 *
	 * @since 3.0.0
	 *
	 * @return bool|WP_Error Returns true if the installation was successful.
	 */
	public function install(): bool|WP_Error {
		$info = $this->options->get_provider_data( self::$provider_name );

		// Get the configuration of the payment provider.
		$options = $this->options->get_plugin_settings();
		$payment = $this->settings->payment_config();

		// Set the payload for the webhook creation (live and test).
		$this->payload = array(
			'description'    => 'Fast Events ticketing',
			'url'            => home_url() . '/wp-json/fast-events/v1/payment/webhook/',
			'connect'        => 'false',
			'enabled_events' => self::ALLOWED_EVENTS,
		);
		$test_key      = $this->settings->payment_property( 'test_key' );
		$this->headers = array( 'Authorization' => 'Bearer ' . $test_key );

		// Set the test webhook events.
		if ( empty( $info['self']['webhook_test_id'] ) ) {
			$test_hook = $this->api_call( self::BASE_URL . 'webhook_endpoints', 'POST' );
			if ( ! is_wp_error( $test_hook ) ) {
				$info['self']['webhook_test_id']     = $test_hook['id'];
				$payment->provider->webhook_test_key = $test_hook['secret'];
				$this->options->set_provider_data( self::$provider_name, $info );
				$options->payment_provider = wp_json_encode( $payment );
				$this->options->set_plugin_settings( $options );
			}
		}

		// Set the test webhook events for connected accounts if SaaS is enabled.
		if ( empty( $info['saas']['webhook_test_id'] ) && $this->settings->saas_mode() ) {
			$this->payload['connect'] = 'true';
			$test_hook                = $this->api_call( self::BASE_URL . 'webhook_endpoints', 'POST' );
			if ( ! is_wp_error( $test_hook ) ) {
				$info['saas']['webhook_test_id']          = $test_hook['id'];
				$payment->provider->saas_webhook_test_key = $test_hook['secret'];
				$this->options->set_provider_data( self::$provider_name, $info );
				$options->payment_provider = wp_json_encode( $payment );
				$this->options->set_plugin_settings( $options );
			}
		}

		// Check if the keys are the same.
		$live_key      = $this->settings->payment_property( 'live_key' );
		$this->headers = array( 'Authorization' => 'Bearer ' . $live_key );
		if ( $test_key === $live_key ) {
			return new WP_Error( 'invalid_request', __( 'The Stripe test and live keys are the same', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Set the live webhook events.
		if ( empty( $info['self']['webhook_live_id'] ) ) {
			$this->payload['connect'] = 'false';
			$live_hook                = $this->api_call( self::BASE_URL . 'webhook_endpoints', 'POST' );
			if ( ! is_wp_error( $live_hook ) ) {
				$info['self']['webhook_live_id']     = $live_hook['id'];
				$payment->provider->webhook_live_key = $live_hook['secret'];
				$this->options->set_provider_data( self::$provider_name, $info );
				$options->payment_provider = wp_json_encode( $payment );
				$this->options->set_plugin_settings( $options );
			}
		}

		// Set the live webhook events for connected accounts if SaaS is enabled.
		if ( empty( $info['saas']['webhook_live_id'] ) && $this->settings->saas_mode() ) {
			$this->payload['connect'] = 'true';
			$live_hook                = $this->api_call( self::BASE_URL . 'webhook_endpoints', 'POST' );
			if ( ! is_wp_error( $live_hook ) ) {
				$info['saas']['webhook_live_id']          = $live_hook['id'];
				$payment->provider->saas_webhook_live_key = $live_hook['secret'];
				$this->options->set_provider_data( self::$provider_name, $info );
				$options->payment_provider = wp_json_encode( $payment );
				$this->options->set_plugin_settings( $options );
			}
		}

		return true;
	}

	/**
	 * Uninstallation activities of the payment provider.
	 * Uninstall the webhooks for Stripe.
	 *
	 * @since 3.0.0
	 */
	public function uninstall(): void {
		$info = $this->options->get_provider_data( self::$provider_name );

		// Delete the webhooks of the live environment.
		$this->headers = array( 'Authorization' => 'Bearer ' . $this->settings->payment_property( 'live_key' ) );
		if ( ! empty( $info['self']['webhook_live_id'] ) ) {
			$this->api_call( self::BASE_URL . 'webhook_endpoints/' . $info['self']['webhook_live_id'], 'DELETE' );
		}
		if ( ! empty( $info['saas']['webhook_live_id'] ) ) {
			$this->api_call( self::BASE_URL . 'webhook_endpoints/' . $info['saas']['webhook_live_id'], 'DELETE' );
		}

		// Delete the webhooks of the test environment.
		$this->headers = array( 'Authorization' => 'Bearer ' . $this->settings->payment_property( 'test_key' ) );
		if ( ! empty( $info['self']['webhook_test_id'] ) ) {
			$this->api_call( self::BASE_URL . 'webhook_endpoints/' . $info['self']['webhook_test_id'], 'DELETE' );
		}
		if ( ! empty( $info['saas']['webhook_test_id'] ) ) {
			$this->api_call( self::BASE_URL . 'webhook_endpoints/' . $info['saas']['webhook_test_id'], 'DELETE' );
		}

		// Delete the options.
		$this->options->delete_provider_data( self::$provider_name );
	}

	/**
	 * 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(
			'customer_email'      => $events_helper->get_email(),
			'line_items'          => array(
				array(
					'price_data' => array(
						'currency'     => $this->settings->currency(),
						'unit_amount'  => round( $events_helper->get_amount() * 100 ),
						'product_data' => array(
							'name' => 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() . ')',
						),
					),
					'quantity'   => 1,
				),
			),
			'payment_intent_data' => array(
				'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() . ')',
				'metadata'    => array( // This can be used by the charge.dispute.created webhook event.
					'order_ids'    => implode( ',', $events_helper->get_order_ids() ), // Stripe only accepts strings.
					'event_ids'    => implode( ',', $events_helper->get_event_ids() ), // Stripe only accepts strings.
					'fe_order_uid' => $events_helper->get_order_uid(),
					'test'         => $test ? 'true' : 'false', // Stripe only accepts strings.
					'fe_version'   => $GLOBALS['fast_events']::FAST_EVENTS_VERSION,
				),
			),
			'mode'                => 'payment',
			'metadata'            => array(
				'order_ids'    => implode( ',', $events_helper->get_order_ids() ), // Stripe only accepts strings.
				'event_ids'    => implode( ',', $events_helper->get_event_ids() ), // Stripe only accepts strings.
				'fe_order_uid' => $events_helper->get_order_uid(),
				'test'         => $test ? 'true' : 'false', // Stripe only accepts strings.
				'fe_version'   => $GLOBALS['fast_events']::FAST_EVENTS_VERSION,
			),
			'cancel_url'          => home_url() . '/wp-json/fast-events/v1/payment/cancel/' . $events_helper->get_order_uid(),
			'success_url'         => home_url() . '/wp-json/fast-events/v1/payment/redirect/' . $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 . 'checkout/sessions', '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['url'],
		);
	}

	/**
	 * 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', 'uid' and 'status').
	 */
	public function get_uid( WP_REST_Request $data ): array|WP_Error {
		global $wpdb;

		$params = $data->get_params();
		$object = $params['data']['object'] ?? array();

		// Check if the Stripe signature header is present.
		$value = $data->get_header( 'Stripe-Signature' );
		if ( empty( $value ) ) {
			$this->logger->warning(
				'The Stripe-Signature header is missing from the webhook',
				array(
					'type'      => 'Stripe webhook',
					'ip'        => PluginUtil::get_ip(),
					'provider'  => self::$provider_name,
					'order_uid' => $object['metadata']['fe_order_uid'] ?? '',
					'event'     => $params['type'] ?? __( 'Stripe event type not found', 'fast-events' ),
				)
			);
			return new WP_Error( 'invalid_request', __( 'The Stripe-Signature header is missing from the webhook', 'fast-events' ) );
		}

		// Check if 't' and 'v1' are present in the signature.
		parse_str( str_replace( ',', '&', $value ), $sig ); // This now holds every key/value pair.
		if ( ! isset( $sig['t'] ) || ! isset( $sig['v1'] ) ) {
			$this->logger->warning(
				'Malformed Stripe-Signature header',
				array(
					'type'      => 'Stripe webhook',
					'ip'        => PluginUtil::get_ip(),
					'provider'  => self::$provider_name,
					'order_uid' => $object['metadata']['fe_order_uid'] ?? '',
					'event'     => $params['type'] ?? __( 'Stripe event type not found', 'fast-events' ),
				)
			);
			return new WP_Error( 'invalid_request', __( 'Malformed Stripe-Signature header', 'fast-events' ) );
		}

		// Check the time tolerance.
		if ( abs( time() - (int) $sig['t'] ) > 300 ) {
			$this->logger->warning(
				'Timestamp outside the allowed tolerance',
				array(
					'type'      => 'Stripe webhook',
					'ip'        => PluginUtil::get_ip(),
					'provider'  => self::$provider_name,
					'order_uid' => $object['metadata']['fe_order_uid'] ?? '',
					'event'     => $params['type'] ?? __( 'Stripe event type not found', 'fast-events' ),
				)
			);
			return new WP_Error( 'invalid_request', __( 'Timestamp outside the allowed tolerance', 'fast-events' ) );
		}

		// Check if the mandatory parameters are present.
		if ( empty( $params['type'] ) || ! array_key_exists( 'livemode', $params ) ) {
			$this->logger->warning(
				'Mandatory parameters (livemode or type) missing from webhook',
				array(
					'type'           => 'Stripe webhook',
					'ip'             => PluginUtil::get_ip(),
					'provider'       => self::$provider_name,
					'request_params' => $params,
				)
			);
			return new WP_Error( 'invalid_request', __( 'Mandatory parameters (livemode or type) missing from webhook', 'fast-events' ) );
		}

		// Which signing key do we use?
		$signing_key = empty( $params['account'] ) // This property is only present in the webhook of connected accounts.
			? $this->settings->payment_property( true === $params['livemode'] ? 'webhook_live_key' : 'webhook_test_key' )
			: $this->settings->payment_property( true === $params['livemode'] ? 'saas_webhook_live_key' : 'saas_webhook_test_key' );

		// Check the signature.
		$expected_sig = hash_hmac( 'sha256', $sig['t'] . '.' . $data->get_body(), $signing_key );
		if ( ! hash_equals( $expected_sig, $sig['v1'] ) ) {
			$this->logger->warning(
				'Invalid signature',
				array(
					'type'      => 'Stripe webhook',
					'ip'        => PluginUtil::get_ip(),
					'provider'  => self::$provider_name,
					'order_uid' => $object['metadata']['fe_order_uid'] ?? '',
					'event'     => $params['type'] ?? __( 'Stripe event type not found', 'fast-events' ),
				)
			);
			return new WP_Error( 'invalid_request', __( 'Invalid signature', 'fast-events' ) );
		}

		// Check if this is an event we are subscribed to.
		if ( ! in_array( $params['type'], self::ALLOWED_EVENTS, true ) ) {
			$this->logger->warning(
				'This event is ignored: ' . $params['type'],
				array(
					'type'      => 'Stripe webhook',
					'ip'        => PluginUtil::get_ip(),
					'provider'  => self::$provider_name,
					'order_uid' => $object['metadata']['fe_order_uid'] ?? '',
					'event'     => $params['type'],
				)
			);
			return new WP_Error( 'invalid_event', __( 'Invalid webhook event', 'fast-events' ) );
		}

		$uid    = '';
		$status = '';
		switch ( $params['type'] ) {
			case 'charge.dispute.created':
				$status = 'chargebacks';

				// Look up the payment intent which has the metadata.fe_order_uid.
				$this->set_headers( $params['account'] ?? 0, ! $object['livemode'] );
				$response = $this->api_call( self::BASE_URL . 'payment_intents/' . $object['payment_intent'], 'POST' );
				if ( is_wp_error( $response ) ) {
					$this->logger->warning(
						'Unable to locate the payment_intent for the dispute',
						array(
							'type'           => 'Stripe webhook',
							'provider'       => self::$provider_name,
							'payment_intent' => $object['payment_intent'],
							'dispute_id'     => $object['id'],
							'event'          => $params['type'],
							'event_id'       => $params['id'],
						)
					);
					return $response;
				}

				// Save the dispute creation or failed date in the metadata.
				$uid    = $response['metadata']['fe_order_uid'];
				$date   = wp_date( 'Y-m-d H:i:s', $object['created'] );
				$orders = array_map( 'intval', explode( ',', $response['metadata']['order_ids'] ) );
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', " .
					"JSON_OBJECT(), '$.metadata.has_chargebacks', '1', '$.metadata.dispute_created_at', '$date' ) " . // phpcs:ignore
					'WHERE id IN (' . implode( ',', $orders ) . ')' // phpcs:ignore
				);
				break;
			case 'charge.refunded':
				$status = 'refunded';

				// Set the refunds flag in the metadata. If Fast Events did the refund, the flag is already set.
				// But if the refund was initiated by Stripe itself, we need to set it manually.
				$orders = array_map( 'intval', explode( ',', $object['metadata']['order_ids'] ) );
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.has_refunds', '1' ) " . // phpcs:ignore
					'WHERE id IN (' . implode( ',', $orders ) . ')' // phpcs:ignore
				);
				$uid = $object['metadata']['fe_order_uid'];
				break;
			// This one can be triggered by the expiry timer of Fast Events, which will already set the expired_at date in the checkout session metadata.
			// So the MySql Update won't do anything.
			// But Stripe itself can also initiate checkout session expiry, so we need to handle that case as well.
			case 'checkout.session.expired':
				$uid    = $object['metadata']['fe_order_uid'];
				$status = 'expired';
				$date   = wp_date( 'Y-m-d H:i:s', $params['created'] );
				$orders = array_map( 'intval', explode( ',', $object['metadata']['order_ids'] ) );
				$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( ',', $orders ) . ')' // phpcs:ignore
				);
				break;
				// Same logic as 'checkout.session.expired', Fast Events already did set it, but Stripe can also initiate cancelling.
			case 'payment_intent.canceled':
				$uid    = $object['metadata']['fe_order_uid'];
				$status = 'canceled';
				$date   = wp_date( 'Y-m-d H:i:s', $object['canceled_at'] );
				$orders = array_map( 'intval', explode( ',', $object['metadata']['order_ids'] ) );
				$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( ',', $orders ) . ')' // phpcs:ignore
				);
				break;
			case 'checkout.session.async_payment_failed':
			case 'payment_intent.failed':
				$uid    = $object['metadata']['fe_order_uid'];
				$status = 'failed';
				$date   = wp_date( 'Y-m-d H:i:s', $object['created'] );
				$orders = array_map( 'intval', explode( ',', $object['metadata']['order_ids'] ) );
				$wpdb->query(
					"UPDATE {$wpdb->prefix}fe_event_orders SET input_fields = JSON_INSERT(input_fields, '$.metadata', JSON_OBJECT(), '$.metadata.failed_at', '$date' ) " . // phpcs:ignore
					'WHERE id IN (' . implode( ',', $orders ) . ')' // phpcs:ignore
				);
				break;
			case 'checkout.session.async_payment_succeeded':
			case 'payment_intent.succeeded':
				$uid    = $object['metadata']['fe_order_uid'];
				$status = 'paid';
				break;

		}

		$this->payload = array();

		return array(
			'id'     => '',
			'uid'    => $uid,
			'status' => $status,
		);
	}

	/**
	 * 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;

		$this->set_headers( $user_id, $test );
		$endpoint = self::BASE_URL . 'checkout/sessions/' . $payment_id . '?expand[]=line_items&expand[]=payment_intent.latest_charge.refunds&expand[]=payment_intent.payment_method';
		$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 );
		$result           = array();
		$result['status'] = match ( $response['status'] ) {
			'complete' => 'paid',
			default    => $response['status'],
		};
		$result['description'] = $response['line_items']['data'][0]['description'] ?? '';
		$result['amount']      = (float) $response['amount_total'] / 100;
		$result['currency']    = strtoupper( $response['currency'] );
		$result['created']     = $order->created_at;

		// Convert the metadata to an array; Stripe only accepts strings in the metadata.
		$result['metadata']              = $response['metadata'];
		$result['metadata']['event_ids'] = array_map( 'intval', explode( ',', $response['metadata']['event_ids'] ) );
		$result['metadata']['order_ids'] = array_map( 'intval', explode( ',', $response['metadata']['order_ids'] ) );
		$result['metadata']['test']      = ( 'true' === $response['metadata']['test'] );

		// Are there any customer details?
		if ( ! empty( $response['payment_intent']['payment_method'] ) ) {
			$method = $response['payment_intent']['payment_method'];
			if ( ! empty( $method['billing_details']['name'] ) ) {
				$result['customer'][ __( 'Customer', 'fast-events' ) ] = $method['billing_details']['name'];
			}
			if ( ! empty( $method['type'] ) ) {
				$result['customer'][ __( 'Payment method', 'fast-events' ) ] = $method['type'];
				if ( ! empty( $method[ $method['type'] ]['brand'] ) ) {
					$result['customer'][ __( 'Payment method', 'fast-events' ) ] = $method[ $method['type'] ]['brand'];
				}
				if ( ! empty( $method[ $method['type'] ]['last4'] ) ) {
					$result['customer'][ __( 'Card number', 'fast-events' ) ] = '**********' . $method[ $method['type'] ]['last4'];
				} elseif ( ! empty( $response['payment_intent']['latest_charge']['payment_method_details'] ) ) {
					$details = $response['payment_intent']['latest_charge']['payment_method_details'];
					if ( ! empty( $details[ $method['type'] ] ) ) {
						if ( ! empty( $details[ $method['type'] ]['last4'] ) ) {
							$result['customer'][ __( 'Card number', 'fast-events' ) ] = '**********' . $details[ $method['type'] ]['last4'];
						} elseif ( ! empty( $details[ $method['type'] ]['iban_last4'] ) ) {
							$result['customer'][ __( 'Card number', 'fast-events' ) ] = '**********' . $details[ $method['type'] ]['iban_last4'];
						}
					}
				}
			}
		}
		$result['customer'][ __( 'Transaction ID', 'fast-events' ) ] = $response['id'];

		if ( ! empty( $response['created'] ) ) {
			$result['actions'][ __( 'Created', 'fast-events' ) ] = wp_date( 'Y-m-d H:i:s', $response['created'] );
		}

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

		if ( ! empty( $response['payment_intent']['payment_method']['created'] ) && empty( $json['metadata']['expired_at'] ) &&
			empty( $json['metadata']['canceled_at'] ) && empty( $json['metadata']['failed_at'] ) ) {
			$result['actions'][ __( 'Paid', 'fast-events' ) ] = wp_date( 'Y-m-d H:i:s', $response['payment_intent']['payment_method']['created'] );
		}

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

		// Has it failed?
		if ( ! empty( $json['metadata']['failed_at'] ) ) {
			$result['status']                                   = 'failed';
			$result['actions'][ __( 'Failed', 'fast-events' ) ] = $json['metadata']['failed_at'];
		}

		// Is there an application fee?
		if ( ! empty( $response['payment_intent']['application_fee_amount'] ) && empty( $json['metadata']['expired_at'] ) &&
			empty( $json['metadata']['canceled_at'] ) && empty( $json['metadata']['failed_at'] ) ) {
			$result['actions'][ __( 'Application fee', 'fast-events' ) ] =
				$this->settings->currency() . ' ' . number_format_i18n( (float) $response['payment_intent']['application_fee_amount'] / 100, 2 );
		}
		if ( ! empty( $response['metadata']['saas_merchant'] ) ) {
			$result['actions'][ __( 'Merchant', 'fast-events' ) ] = $response['metadata']['saas_merchant'];
		}

		// Are there refunds?
		$result['total_refunds'] = 0.0;
		$result['has_refunds']   = false;
		if ( ! empty( $response['payment_intent']['latest_charge']['refunds']['data'] ) ) {
			foreach ( $response['payment_intent']['latest_charge']['refunds']['data'] as $refund ) {
				$result['has_refunds']    = true;
				$result['status']         = 'refunded';
				$result['refunds'][]      = array(
					'amount'   => (string) ( $refund['amount'] / 100 ),
					'currency' => strtoupper( $refund['currency'] ),
					'date'     => wp_date( 'Y-m-d H:i:s', $refund['created'] ),
					'order_id' => empty( $refund['metadata']['order_id'] ) ? '' : (int) $refund['metadata']['order_id'],
				);
				$result['total_refunds'] += (float) $response['payment_intent']['latest_charge']['amount_refunded'] / 100;
			}
		}

		// Are there any chargebacks?
		$result['has_chargebacks'] = $response['payment_intent']['latest_charge']['disputed'] ?? false;
		if ( $result['has_chargebacks'] ) {
			// Is there a dispute creation date?
			if ( ! empty( $json['metadata']['dispute_created_at'] ) ) {
				$result['actions'][ __( 'Dispute', 'fast-events' ) ] = $json['metadata']['dispute_created_at'];
			}
			$result['status'] = 'chargeback';
		}

		if ( ! empty( $response['payment_intent']['status'] ) && 'succeeded' !== $response['payment_intent']['status'] ) {
			if ( ! in_array( $result['status'], array( 'canceled', 'failed', 'expired', 'refunded', 'chargeback' ), true ) ) {
				$result['status'] = 'processing';
				unset( $result['actions'][ __( 'Paid', 'fast-events' ) ] );
				$result['actions'][ __( 'Detailed status', 'fast-events' ) ] = $response['payment_intent']['status'];
			}
		}

		// Is the dashboard link already stored?
		if ( empty( $json['metadata']['dashboard_link'] ) && ! empty( $response['payment_intent']['id'] ) ) {
			$environment = $response['livemode'] ? '' : 'test/';
			$connect     = empty( $response['metadata']['saas_account_id'] ) ? '' : 'connect/accounts/' . $response['metadata']['saas_account_id'] . '/';
			$link        = 'https://dashboard.stripe.com/' . $environment . $connect . 'payments/' . $response['payment_intent']['id'];
			$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( ',', $json['orders'] ) . ')', // phpcs:ignore
					$link
				)
			);
			$json['metadata']['dashboard_link'] = $link;
		}
		$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 {
		return array();
	}

	/**
	 * Cancel 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 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 {
		$endpoint = self::BASE_URL . 'checkout/sessions/' . $payment_id . '/expire';
		$this->set_headers( $user_id, $test );
		$response = $this->api_call( $endpoint, 'POST' );
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		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 {

		// Find the payment intent ID.
		$endpoint = self::BASE_URL . 'checkout/sessions/' . $order->payment_id;
		$this->set_headers( $user_id, $test );
		$response = $this->api_call( $endpoint, 'GET' );
		if ( is_wp_error( $response ) ) {
			return $response;
		}
		if ( empty( $response['payment_intent'] ) ) {
			$this->logger->warning(
				'Payment ID not found',
				array(
					'type'     => 'Stripe refund',
					'provider' => self::$provider_name,
					'id'       => $order->payment_id,
					'order_id' => (int) ( $metadata['order_id'] ?? 0 ),
					'event_id' => (int) ( $metadata['event_id'] ?? 0 ),
				)
			);
			return new WP_Error( 'no_payment_id', __( 'Payment ID not found', 'fast-events' ) );
		}

		// Create the refund.
		$this->payload = array(
			'amount'         => round( $amount * 100 ),
			'payment_intent' => $response['payment_intent'],
			'metadata'       => $metadata,
		);
		$response      = $this->api_call( self::BASE_URL . '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(
			'response_type' => 'code',
			'scope'         => 'read_write',
			'client_id'     => $this->settings->saas_client_id(),
			'state'         => $this->get_state( $user ),
			'redirect_uri'  => home_url( '/wp-json/fast-events/v1/saas/authorize' ),
		);
		return add_query_arg( $args, 'https://connect.stripe.com/oauth/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 {
		$this->payload = array(
			'client_id'      => $this->settings->saas_client_id(),
			'client_secret'  => $this->settings->payment_property( $this->settings->saas_test() ? 'test_key' : 'live_key' ),
			'stripe_user_id' => $this->user_meta->get_saas_profile_id( $user_id ),
		);
		$this->headers = array();
		$config        = $this->api_call( 'https://connect.stripe.com/oauth/deauthorize', 'POST' );
		if ( is_wp_error( $config ) ) {
			return $config;
		}
		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',
			'client_id'     => $this->settings->saas_client_id(),
			'client_secret' => $this->settings->payment_property( $this->settings->saas_test() ? 'test_key' : 'live_key' ),
			'code'          => $params['code'],
		);
		$this->headers = array();
		$config        = $this->api_call( 'https://connect.stripe.com/oauth/token', 'POST' );
		if ( is_wp_error( $config ) ) {
			return $config;
		}

		// Save the SaaS config.
		$this->user_meta->set_saas_config( $user_id, $config );

		// Store the sub-merchant strip account id as profile ID.
		$this->user_meta->set_saas_profile_id( $user_id, $config['stripe_user_id'] );

		// Get the merchant name.
		$this->payload = array();
		$this->headers = array(
			'Authorization'  => 'Bearer ' . $this->settings->payment_property( $this->settings->saas_test() ? 'test_key' : 'live_key' ), // phpcs:ignore
			'Stripe-Account' => $config['stripe_user_id'],
		);
		$account       = $this->api_call( self::BASE_URL . 'accounts/' . $config['stripe_user_id'], 'GET' );
		if ( is_wp_error( $account ) ) {
			$this->revoke_authorization( $user_id );
			return $account;
		}
		$name = '';
		if ( ! empty( $account['business_profile']['name'] ) ) {
			$name = $account['business_profile']['name'];
		} elseif ( ! empty( $account['individual']['first_name'] ) && ! empty( $account['individual']['last_name'] ) ) {
			$name = trim( $account['individual']['first_name'] . ' ' . $account['individual']['last_name'] );
		}
		$this->user_meta->set_saas_merchant_name( $user_id, $name );
		$this->logger->info(
			'Stripe merchant name: ' . $name,
			array(
				'type'  => 'Stripe merchant name',
				'name'  => $name,
				'email' => $account['email'] ?? '',
			)
		);
		return '';
	}

	/**
	 * Get an access token from auth code.
	 * Stripe doesn't use this method any more. The sub-merchant stripe account ID is used in the HTTP headers instead.
	 *
	 * @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 {
		return 'dummy_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/x-www-form-urlencoded',
			'Stripe-Version' => '2025-12-15.clover',
		);
	}

	/**
	 * 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 {
		return ( $error['error']['type'] ?? '' ) . ': ' . ( $error['error']['message'] ?? '' );
	}

	/**
	 * If we are operating in SaaS mode, we need to set additional fields for the 'create_payment' endpoint.
	 *
	 * @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 {

		// 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;
		}

		// 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() ) { // Fee per order.
			$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 per ticket.
			$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' );
		}

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

		// Metadata for the Stripe payment intent.
		$this->payload['payment_intent_data']['metadata']['saas_fee']        = $this->settings->currency() . ' ' . number_format_i18n( $fee, 2 ) . ' ' . $per;
		$this->payload['payment_intent_data']['metadata']['saas_desc']       = $total . ' ' . $meta_desc;
		$this->payload['payment_intent_data']['metadata']['saas_amount']     = $this->settings->currency() . ' ' . number_format_i18n( $fee_total, 2 );
		$this->payload['payment_intent_data']['metadata']['saas_account_id'] = $this->user_meta->get_saas_profile_id( $events_helper->get_saas_user_id() );
		$this->payload['payment_intent_data']['metadata']['saas_merchant']   = $org_name;

		// Metadata for the Stripe checkout session.
		$this->payload['metadata']['saas_fee']        = $this->settings->currency() . ' ' . number_format_i18n( $fee, 2 ) . ' ' . $per;
		$this->payload['metadata']['saas_desc']       = $total . ' ' . $meta_desc;
		$this->payload['metadata']['saas_amount']     = $this->settings->currency() . ' ' . number_format_i18n( $fee_total, 2 );
		$this->payload['metadata']['saas_account_id'] = $this->user_meta->get_saas_profile_id( $events_helper->get_saas_user_id() );
		$this->payload['metadata']['saas_merchant']   = $org_name;

		// Set the application fee.
		$this->payload['payment_intent_data']['application_fee_amount'] = round( $fee_total * 100 );
	}

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