<?php

namespace MPHB\Payments\Gateways\Stripe;

use MPHB\Entities\Payment;
use MPHB\Payments\Gateways\{ AbstractNotificationListener, StripeGateway };
use MPHB\PostTypes\PaymentCPT\Statuses as PaymentStatuses;
use MPHB\Stripe\{ Event, Webhook };

/**
 * @since 3.6.0
 */
class WebhookListener extends AbstractNotificationListener {
	/**
	 * @var StripeGateway
	 */
	protected $gateway;

	private $eventType = '';
	private $eventObject = null; // Charge|PaymentIntent|Refund

	private $isEventProcessingMustBeOk = false;

	public function fireExit( $succeed ) {
		if ( $succeed || $this->isEventProcessingMustBeOk ) {
			http_response_code( 200 );
		} else {
			http_response_code( 400 );
		}

		parent::fireExit( $succeed );
	}

	protected function initUrlIdentificationValue() {
		return 'stripe';
	}

	/**
	 * @return string
	 */
	protected function parseInput() {
		$payload = @file_get_contents( 'php://input' );

		return $payload;
	}

	protected function paymentCancelled( $log ): bool {
		return MPHB()->paymentManager()->cancelPayment( $this->payment, $log );
	}

	protected function paymentCompleted( $log ) {
		return MPHB()->paymentManager()->completePayment( $this->payment, $log );
	}

	protected function paymentFailed( $log ) {
		return MPHB()->paymentManager()->failPayment( $this->payment, $log );
	}

	protected function paymentOnHold( $log ) {
		return MPHB()->paymentManager()->holdPayment( $this->payment, $log );
	}

	protected function paymentRefunded( $log ) {
		return MPHB()->paymentManager()->refundPayment( $this->payment, $log );
	}

	protected function process() {
		switch ( $this->eventType ) {
			case 'charge.captured': // Manual capture only
				// Update captured funds' data
				if ( $this->payment->hasPendingAuthedFunds() ) {
					$currency       = $this->payment->getCurrency();
					$capturedAmount = $this->stripeAmountToFloat( $this->eventObject->amount_captured, $currency );

					$this->payment->addLog(
						sprintf(
							// Translators: %1: Stripe PaymentIntent ID. %2: Payment amount.
							__( 'Webhook received. Payment %1$s of %2$s was successfully captured.', 'motopress-hotel-booking' ),
							$this->eventObject->payment_intent,
							mphb_format_price( $capturedAmount, array( 'as_html' => false ) )
						)
					);

					$this->payment->setAmount( $capturedAmount - $this->payment->getPaymentFee() );
					$this->payment->getAuthedFunds()->capture( $capturedAmount );

					MPHB()->getPaymentRepository()->save( $this->payment );
				}

				// Update fee amount
				if ( ! empty( $this->eventObject->balance_transaction ) ) {
					$transactionId = $this->eventObject->balance_transaction;

					try {
						$transaction = $this->gateway->getApi()->retrieveBalanceTransaction( $transactionId );
						$feeAmount   = $this->stripeAmountToFloat( $transaction->fee, $this->eventObject->currency );

						update_post_meta( $this->payment->getId(), '_mphb_fee', $feeAmount );
					} catch ( \Exception $e ) {}
				}

				break;

			case 'charge.succeeded': // Automatic & manual capture
				// Hold payment with captured funds after 3D Secure. For 3D
				// Secure + manual capture, there is no other event to put the
				// payment into "On Hold" status. For 3D Secure + automatic
				// capture, there is "payment_intent.succeeded".
				if ( $this->payment->getStatus() === PaymentStatuses::STATUS_PENDING
					&& $this->payment->hasPendingAuthedFunds()
				) {
					try {
						$paymentIntent = $this->gateway->getApi()->retrievePaymentIntent( $this->eventObject->payment_intent );

						if ( $paymentIntent->status === 'requires_capture' ) {
							$currency       = $this->payment->getCurrency();
							$capturedAmount = $this->stripeAmountToFloat( $paymentIntent->amount_capturable, $currency );

							$this->paymentOnHold(
								sprintf(
									// Translators: %1: Stripe PaymentIntent ID. %2: Payment amount.
									__( 'Webhook received. Payment %1$s with uncaptured amount %2$s authorized by customer.', 'motopress-hotel-booking' ),
									$this->eventObject->payment_intent,
									mphb_format_price( $capturedAmount, array( 'as_html' => false ) )
								)
							);
						}
					} catch ( \Exception $e ) {
						$this->paymentOnHold(
							sprintf(
								// Translators: %s: Stripe PaymentIntent ID.
								__( 'Webhook received. Payment %s confirmed by customer.', 'motopress-hotel-booking' ),
								$this->eventObject->payment_intent
							)
						);
					}
				}

				// Update "Payment Type" field
				$paymentType = $this->eventObject->payment_method_details['type'] ?? null;

				if ( $paymentType !== null ) {
					update_post_meta( $this->payment->getId(), '_mphb_payment_type', $paymentType );
				}

				break;

			case 'charge.updated': // Atomatic capture only
				// Update fee amount
				if ( ! empty( $this->eventObject->balance_transaction ) ) {
					$transactionId = $this->eventObject->balance_transaction;

					try {
						$transaction = $this->gateway->getApi()->retrieveBalanceTransaction( $transactionId );
						$feeAmount   = $this->stripeAmountToFloat( $transaction->fee, $this->eventObject->currency );

						update_post_meta( $this->payment->getId(), '_mphb_fee', $feeAmount );
					} catch ( \Exception $e ) {}
				}

				break;

			case 'payment_intent.canceled':
				if ( $this->payment->hasPendingAuthedFunds() ) {
					// Cancelled payment intent = cancelled authorized funds
					// (don't trigger releaseAuthorizedFunds() of the gateway)
					$this->payment->getAuthedFunds()->release();
				}

				$this->paymentCancelled(
					sprintf(
						// Translators: %s: Stripe event object ID.
						__( 'Webhook received. Payment %s was cancelled by the customer.', 'motopress-hotel-booking' ),
						$this->eventObject->id
					)
				);
				break;

			case 'payment_intent.payment_failed':
				$this->paymentFailed(
					sprintf(
						// Translators: %s: Stripe event object ID.
						__( "Webhook received. Payment %s failed and couldn't be processed.", 'motopress-hotel-booking' ),
						$this->eventObject->id
					)
				);
				break;

			case 'payment_intent.succeeded':
				// This event tries to race with "charge.captured", so don't
				// accidently override the captured amount with the old value
				$this->payment->setAuthedFunds( null );

				$amount = $this->stripeAmountToFloat(
					$this->eventObject->amount_received,
					$this->payment->getCurrency()
				);

				$this->paymentCompleted(
					sprintf(
						// Translators: %1: Stripe event object ID. %2: Payed amount.
						__( 'Webhook received. Payment %1$s of %2$s was successfully processed.', 'motopress-hotel-booking' ),
						$this->eventObject->id,
						mphb_format_price( $amount, array( 'as_html' => false ) )
					)
				);

				break;

			case 'refund.created':
				$this->payment->addLog(
					sprintf(
						// Translators: %s: Stripe refund ID.
						__( 'Webhook received. A refund %s is created.', 'motopress-hotel-booking' ),
						$this->eventObject->id
					)
				);
				break;

			case 'refund.updated':
				$status = $this->eventObject->status;
				$reason = $this->eventObject->reason ?? '';

				if ( $status === 'succeeded' && $reason !== 'expired_uncaptured_charge' ) {
					$this->paymentRefunded(
						sprintf(
							// Translators: %1: Stripe PaymentIntent ID. %2: Event object ID. %3: Refund reason.
							__( 'Webhook received. Payment %1$s was refunded. Refund ID: %2$s. Reason: %3$s', 'motopress-hotel-booking' ),
							$this->eventObject->payment_intent,
							$this->eventObject->id,
							$reason
						)
					);
				}

				break;
		}
	}

	/**
	 * @return Payment|null
	 */
	protected function retrievePayment() {
		// To find payment we need to use the PaymentItent ID
		if ( $this->eventObject->object === 'payment_intent' ) {
			$payment = MPHB()->getPaymentRepository()->findByTransactionId( $this->eventObject->id );
		} else {
			// Charge|Refund
			$payment = MPHB()->getPaymentRepository()->findByTransactionId( $this->eventObject->payment_intent );
		}

		// Stripe triggers webhook too early for payment intents and  we did not
		// create the payment yet. So we skip the event and don't mark it as
		// "Failed" in Stripe Dashboard. Otherwise the customer will have
		// "Failed" for each first try of the webhook.
		$this->isEventProcessingMustBeOk = is_null( $payment );

		return $payment;
	}

	/**
	 * @param string $input
	 * @return bool
	 */
	protected function validate( $input ) {
		if ( ! isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ) {
			return false;
		}

		try {
			$this->gateway->getApi()->setApp();

			$webhookSecret = $this->gateway->getWebhookKey();

			if ( $webhookSecret !== '' ) {
				// Verify webhook signature
				// https://docs.stripe.com/webhooks#verify-official-libraries
				$signatureHeader = sanitize_text_field( wp_unslash( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) );

				$event = Webhook::constructEvent( $input, $signatureHeader, $webhookSecret );
			} else {
				// https://docs.stripe.com/webhooks#example-endpoint
				$event = Event::constructFrom( json_decode( $input, true ) );
			}

			if ( ! empty( $event->data->object->id ) ) {
				$this->eventType   = $event->type;
				$this->eventObject = $event->data->object;
			}

		} catch ( \Throwable $e ) {
			return false;
		}

		return ! empty( $this->eventType )
			&& ! empty( $this->eventObject )
			&& (
				// Has payment intent ID
				$this->eventObject->object === 'payment_intent'
				|| ! empty( $this->eventObject->payment_intent )
			);
	}

	private function stripeAmountToFloat( $amount, ?string $currency ): float {
		return $this->gateway->getApi()->convertFromSmallestUnit( $amount, strtoupper( $currency ) );
	}
}
