<?php

/**
 * Handles responses from PayPal IPN.
 *
 * @package Masteriyo\PayPal
 * @version 3.3.0
 */

namespace Masteriyo\Gateways\Paypal;

defined( 'ABSPATH' ) || exit;

use Masteriyo\Enums\OrderStatus;
use Masteriyo\Gateways\Paypal\Response;
use Masteriyo\Pro\Enums\SubscriptionStatus;
use Masteriyo\Models\Order\Order;

/**
 * Paypal_IPN_Handler class.
 */
class IpnHandler extends Response {

	/**
	 * Receiver email address to validate.
	 *
	 * @since 1.0.0
	 *
	 * @var string Receiver email address.
	 */
	protected $receiver_email;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param bool   $sandbox Use sandbox or not.
	 * @param string $receiver_email Email to receive IPN from.
	 */
	public function __construct( $sandbox = false, $receiver_email = '' ) {
		add_action( 'masteriyo_api_gateway_paypal', array( $this, 'check_response' ) );
		add_action( 'masteriyo_valid_paypal_standard_ipn_request', array( $this, 'valid_response' ) );

		$this->receiver_email = $receiver_email;
		$this->sandbox        = $sandbox;
	}

	/**
	 * Check for PayPal IPN Response.
	 *
	 * @since 1.0.0
	 */
	public function check_response() {
		masteriyo_get_logger()->info( 'Checking for PayPal IPN Response.', array( 'source' => 'payment-paypal' ) );
		if ( ! empty( $_POST ) && $this->validate_ipn() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			$posted = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
			masteriyo_get_logger()->info( 'Found PayPal IPN Response.', array( 'source' => 'payment-paypal' ) );
			/**
			 * Fires if PayPal IPN request is valid.
			 *
			 * @since 1.0.0
			 *
			 * @param array $posted Posted data.
			 */
			do_action( 'masteriyo_valid_paypal_standard_ipn_request', $posted );
			exit;
		}
		masteriyo_get_logger()->info( 'No PayPal IPN Response found.', array( 'source' => 'payment-paypal' ) );
		// wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) );
	}

	/**
	 * There was a valid response.
	 *
	 * @since 1.0.0
	 *
	 * @param  array $posted Post data after wp_unslash.
	 */
	public function valid_response( $posted ) {
		masteriyo_get_logger()->info( 'Validating PayPal IPN Response.', array( 'source' => 'payment-paypal' ) );
		$order = ! empty( $posted['custom'] ) ? $this->get_paypal_order( $posted['custom'] ) : false;

		if ( $order ) {
			$payment_status           = strtolower( $posted['payment_status'] ?? '' );
			$posted['payment_status'] = $payment_status;
			$txn_type                 = masteriyo_strtolower( $posted['txn_type'] ?? '' );

			Paypal::log( 'Found order #' . $order->get_id() );
			Paypal::log( 'Payment status: ' . $payment_status );

			masteriyo_get_logger()->info( 'Found order #' . $order->get_id(), array( 'source' => 'payment-paypal' ) );
			masteriyo_get_logger()->info( 'Payment status: ' . $payment_status, array( 'source' => 'payment-paypal' ) );

			if ( method_exists( $this, 'payment_status_' . $payment_status ) ) {
				call_user_func( array( $this, 'payment_status_' . $payment_status ), $order, $posted );
			}

			if (
				masteriyo_starts_with( $txn_type, 'subscr_' ) ||
				masteriyo_starts_with( $txn_type, 'recurring_' )
			) {
				$this->update_subscription( $order, $posted );
			}

			masteriyo_get_logger()->info( 'Finished validating PayPal IPN Response.', array( 'source' => 'payment-paypal' ) );
		}
	}

	/**
	 * Check PayPal IPN validity.
	 *
	 * @since 1.0.0
	 */
	public function validate_ipn() {
		masteriyo_get_logger()->info( 'Validating PayPal IPN.', array( 'source' => 'payment-paypal' ) );
		Paypal::log( 'Checking IPN response is valid' );

		// Get received values from post data.
		$validate_ipn        = wp_unslash( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$validate_ipn['cmd'] = '_notify-validate';

		// Send back post vars to paypal.
		$params = array(
			'body'        => $validate_ipn,
			'timeout'     => 60,
			'httpversion' => '1.1',
			'compress'    => false,
			'decompress'  => false,
			'user-agent'  => 'Masteriyo/' . masteriyo_get_version(),
		);

		// Post back to get a response.
		$response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params );
		masteriyo_get_logger()->info( 'Finished validating PayPal IPN.', array( 'source' => 'payment-paypal' ) );
		Paypal::log( 'IPN Response: ' . masteriyo_print_r( $response, true ) );

		// Check to see if the request was valid.
		if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) {
			masteriyo_get_logger()->info( 'Received valid response from PayPal IPN', array( 'source' => 'payment-paypal' ) );
			Paypal::log( 'Received valid response from PayPal IPN' );
			return true;
		}

		masteriyo_get_logger()->info( 'Received invalid response from PayPal IPN', array( 'source' => 'payment-paypal' ) );
		Paypal::log( 'Received invalid response from PayPal IPN' );

		if ( is_wp_error( $response ) ) {
			masteriyo_get_logger()->error( 'Error response: ' . $response->get_error_message(), array( 'source' => 'payment-paypal' ) );
			Paypal::log( 'Error response: ' . $response->get_error_message() );
		}

		return false;
	}

	/**
	 * Check for a valid transaction type.
	 *
	 * @since 1.0.0
	 *
	 * @param string $txn_type Transaction type.
	 */
	protected function validate_transaction_type( $txn_type ) {
		masteriyo_get_logger()->info( 'Checking transaction type: ' . $txn_type, array( 'source' => 'payment-paypal' ) );
		$accepted_types = array(
			'cart',
			'instant',
			'express_checkout',
			'web_accept',
			'masspay',
			'send_money',
			'paypal_here',
			'subscr_payment',
			'subscr_signup',
			'subscr_cancel',
			'subscr_eot',
			'recurring_payment',
			'recurring_payment_profile_created',
			'recurring_payment_profile_cancel',
			'recurring_payment_failed',
			'recurring_payment_suspended',
			'recurring_payment_suspended_due_to_max_failed_payment',
			'recurring_payment_expired',
		);

		if ( ! in_array( strtolower( $txn_type ), $accepted_types, true ) ) {
			masteriyo_get_logger()->error( 'Aborting, Invalid type:' . $txn_type, array( 'source' => 'payment-paypal' ) );
			Paypal::log( 'Aborting, Invalid type:' . $txn_type );
			exit;
		}

		masteriyo_get_logger()->info( 'Transaction type accepted: ' . $txn_type, array( 'source' => 'payment-paypal' ) );
	}

	/**
	 * Check currency from IPN matches the order.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order    Order object.
	 * @param string   $currency Currency code.
	 */
	protected function validate_currency( $order, $currency ) {
		masteriyo_get_logger()->info( 'Checking currency: ' . $currency, array( 'source' => 'payment-paypal' ) );
		if ( $order->get_currency() !== $currency ) {
			masteriyo_get_logger()->error( 'Aborting, Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")', array( 'source' => 'payment-paypal' ) );
			Paypal::log( 'Payment error: Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")' );

			/* translators: %s: currency code. */
			$order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'learning-management-system' ), $currency ) );
			exit;
		}
	}

	/**
	 * Check payment amount from IPN matches the order.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param int      $amount Amount to validate.
	 */
	protected function validate_amount( $order, $amount ) {
		masteriyo_get_logger()->info( 'Checking amount: ' . $amount, array( 'source' => 'payment-paypal' ) );
		if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
			masteriyo_get_logger()->error( 'Aborting, Amounts do not match (gross ' . $amount . ')', array( 'source' => 'payment-paypal' ) );
			Paypal::log( 'Payment error: Amounts do not match (gross ' . $amount . ')' );

			/* translators: %s: Amount. */
			$order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'learning-management-system' ), $amount ) );
			exit;
		}
	}

	/**
	 * Check receiver email from PayPal. If the receiver email in the IPN is different than what is stored in.
	 * Masteriyo -> Settings -> Checkout -> PayPal, it will log an error about it.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order          Order object.
	 * @param string   $receiver_email Email to validate.
	 */
	protected function validate_receiver_email( $order, $receiver_email ) {
		masteriyo_get_logger()->info( 'Checking receiver email: ' . $receiver_email, array( 'source' => 'payment-paypal' ) );
		if ( strcasecmp( trim( $receiver_email ), trim( $this->receiver_email ) ) !== 0 ) {
			masteriyo_get_logger()->error( 'Aborting, IPN Response is for another account: ' . $receiver_email, array( 'source' => 'payment-paypal' ) );
			Paypal::log( "IPN Response is for another account: {$receiver_email}. Your email is {$this->receiver_email}" );

			/* translators: %s: email address . */
			$order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'learning-management-system' ), $receiver_email ) );
			exit;
		}
	}

	/**
	 * Handle a completed payment.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_completed( $order, $posted ) {
		masteriyo_get_logger()->info( 'Payment status completed', array( 'source' => 'payment-paypal' ) );
		if ( $order->has_status( masteriyo_get_is_paid_statuses() ) ) {
			masteriyo_get_logger()->error( 'Aborting, Order #' . $order->get_id() . ' is already complete.', array( 'source' => 'payment-paypal' ) );
			Paypal::log( 'Aborting, Order #' . $order->get_id() . ' is already complete.' );
			exit;
		}

		$this->validate_transaction_type( $posted['txn_type'] );
		$this->validate_currency( $order, $posted['mc_currency'] );
		$this->validate_amount( $order, $posted['mc_gross'] );
		$this->validate_receiver_email( $order, $posted['receiver_email'] );
		$this->save_paypal_meta_data( $order, $posted );

		if ( OrderStatus::COMPLETED === $posted['payment_status'] ) {
			if ( $order->has_status( OrderStatus::CANCELLED ) ) {
				$this->payment_status_paid_cancelled_order( $order, $posted );
			}

			if ( ! empty( $posted['mc_fee'] ) ) {
				$order->add_meta_data( 'PayPal Transaction Fee', masteriyo_clean( $posted['mc_fee'] ) );
			}

			$this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? masteriyo_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed.', 'learning-management-system' ) );
		} else {
			if ( 'authorization' === $posted['pending_reason'] ) {
				$this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'learning-management-system' ) );
			} else {
				/* translators: %s: Pending reason. */
				$this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'learning-management-system' ), $posted['pending_reason'] ) );
			}
		}
	}

	/**
	 * Handle a pending payment.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_pending( $order, $posted ) {
		$this->payment_status_completed( $order, $posted );
	}

	/**
	 * Handle a failed payment.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_failed( $order, $posted ) {
		/* translators: %s: payment status. */
		$order->update_status( OrderStatus::FAILED, sprintf( __( 'Payment %s via IPN.', 'learning-management-system' ), masteriyo_clean( $posted['payment_status'] ) ) );
	}

	/**
	 * Handle a denied payment.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_denied( $order, $posted ) {
		$this->payment_status_failed( $order, $posted );
	}

	/**
	 * Handle an expired payment.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_expired( $order, $posted ) {
		$this->payment_status_failed( $order, $posted );
	}

	/**
	 * Handle a voided payment.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_voided( $order, $posted ) {
		$this->payment_status_failed( $order, $posted );
	}

	/**
	 * When a user cancelled order is marked paid.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_paid_cancelled_order( $order, $posted ) {
		$this->send_ipn_email_notification(
		/* translators: %s: order link. */
			sprintf( __( 'Payment for cancelled order %s received.', 'learning-management-system' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
			/* translators: %s: order ID. */
			sprintf( __( 'Order #%s has been marked paid by PayPal IPN, but was previously cancelled. Admin handling required.', 'learning-management-system' ), $order->get_order_number() )
		);
	}

	/**
	 * Handle a refunded order.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_refunded( $order, $posted ) {
		// Only handle full refunds, not partial.
		if ( $order->get_total() === masteriyo_format_decimal( $posted['mc_gross'] * -1, masteriyo_get_price_decimals() ) ) {

			/* translators: %s: payment status. */
			$order->update_status( OrderStatus::REFUNDED, sprintf( __( 'Payment %s via IPN.', 'learning-management-system' ), strtolower( $posted['payment_status'] ) ) );

			$this->send_ipn_email_notification(
			/* translators: %s: order link. */
				sprintf( __( 'Payment for order %s refunded.', 'learning-management-system' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
				/* translators: %1$s: order ID, %2$s: reason code. */
				sprintf( __( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'learning-management-system' ), $order->get_order_number(), $posted['reason_code'] )
			);
		}
	}

	/**
	 * Handle a reversal.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_reversed( $order, $posted ) {
		/* translators: %s: payment status. */
		$order->update_status( OrderStatus::ON_HOLD, sprintf( __( 'Payment %s via IPN.', 'learning-management-system' ), masteriyo_clean( $posted['payment_status'] ) ) );

		$this->send_ipn_email_notification(
		/* translators: %s: order link. */
			sprintf( __( 'Payment for order %s reversed.', 'learning-management-system' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
			/* translators: %1$s: order ID, %2$s: reason code. */
			sprintf( __( 'Order #%1$s has been marked on-hold due to a reversal - PayPal reason code: %2$s', 'learning-management-system' ), $order->get_order_number(), masteriyo_clean( $posted['reason_code'] ) )
		);
	}

	/**
	 * Handle a cancelled reversal.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function payment_status_canceled_reversal( $order, $posted ) {
		$this->send_ipn_email_notification(
			/* translators: %s: order link. */
			sprintf( __( 'Reversal cancelled for order #%s.', 'learning-management-system' ), $order->get_order_number() ),
			sprintf(
				/* translators: %1$s: order ID, %2$s: order link. */
				__( 'Order #%1$s has had a reversal cancelled. Please check the status of payment and update the order status accordingly here: %2$s', 'learning-management-system' ),
				$order->get_order_number(),
				esc_url( $order->get_edit_order_url() )
			)
		);
	}

	/**
	 * Save important data from the IPN to the order.
	 *
	 * @since 1.0.0
	 *
	 * @param Order $order  Order object.
	 * @param array    $posted Posted data.
	 */
	protected function save_paypal_meta_data( $order, $posted ) {
		if ( ! empty( $posted['payment_type'] ) ) {
			update_post_meta( $order->get_id(), 'Payment type', masteriyo_clean( $posted['payment_type'] ) );
		}
		if ( ! empty( $posted['txn_id'] ) ) {
			update_post_meta( $order->get_id(), '_transaction_id', masteriyo_clean( $posted['txn_id'] ) );
		}
		if ( ! empty( $posted['payment_status'] ) ) {
			update_post_meta( $order->get_id(), '_paypal_status', masteriyo_clean( $posted['payment_status'] ) );
		}
	}

	/**
	 * Send a notification to the user handling orders.
	 *
	 * @since 1.0.0
	 *
	 * @param string $subject Email subject.
	 * @param string $message Email message.
	 */
	protected function send_ipn_email_notification( $subject, $message ) {
		$new_order_settings        = $this->get_new_order_settings();
		$masteriyo_paypal_settings = $this->get_paypal_settings();

		if ( ! isset( $masteriyo_paypal_settings['ipn_notification'] ) ) {
			return;
		}
		$mailer     = new \Masteriyo\Emails\Admin\NewOrderEmailToAdmin();
		$recipients = $new_order_settings['recipients'];

		if ( empty( $recipients ) ) {
			$recipients = (array) get_bloginfo( 'admin_email' );
		}

		$mailer->set_recipients( $recipients );
		$mailer->set( 'subject', $subject );
		$mailer->set( 'content', $message );

		$mailer->send(
			$mailer->get_recipients(),
			$mailer->get_subject(),
			$mailer->get_content(),
			$mailer->get_headers(),
			$mailer->get_attachments()
		);
	}

	/**
	 * Get email new order settings.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	protected function get_new_order_settings() {
		return array(
			'enable'     => masteriyo_get_setting( 'emails.new_order.enable' ),
			'recipients' => masteriyo_get_setting( 'emails.new_order.recipients' ),
			'subject'    => masteriyo_get_setting( 'emails.new_order.subject' ),
			'heading'    => masteriyo_get_setting( 'emails.new_order.heading' ),
			'content'    => masteriyo_get_setting( 'emails.new_order.content' ),
		);
	}

	/**
	 * Get paypal settings.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	protected function get_paypal_settings() {
		return array(
			'enable'                 => masteriyo_get_setting( 'payments.paypal.enable' ),
			'title'                  => masteriyo_get_setting( 'payments.paypal.title' ),
			'description'            => masteriyo_get_setting( 'payments.paypal.description' ),
			'ipn_email_notification' => masteriyo_get_setting( 'payments.paypal.ipn_email_notification' ),
			'sandbox'                => masteriyo_get_setting( 'payments.paypal.sandbox' ),
			'email'                  => masteriyo_get_setting( 'payments.paypal.email' ),
			'receiver_email'         => masteriyo_get_setting( 'payments.paypal.receiver_email' ),
			'identity_token'         => masteriyo_get_setting( 'payments.paypal.indentity_token' ),
			'invoice_prefix'         => masteriyo_get_setting( 'payments.paypal.invoice_prefix' ),
			'payment_action'         => masteriyo_get_setting( 'payments.paypal.payment_action' ),
			'image_url'              => masteriyo_get_setting( 'payments.paypal.image_url' ),
			'debug'                  => masteriyo_get_setting( 'payments.paypal.debug' ),
			'sandbox_api_username'   => masteriyo_get_setting( 'payments.paypal.sandbox_api_username' ),
			'sandbox_api_password'   => masteriyo_get_setting( 'payments.paypal.sandbox_api_password' ),
			'sandbox_api_signature'  => masteriyo_get_setting( 'payments.paypal.sandbox_api_signature' ),
			'live_api_username'      => masteriyo_get_setting( 'payments.paypal.live_api_username' ),
			'live_api_password'      => masteriyo_get_setting( 'payments.paypal.live_api_password' ),
			'live_api_signature'     => masteriyo_get_setting( 'payments.paypal.live_api_signature' ),
		);
	}

	/**
	 * Update subscription.
	 *
	 * @since 2.6.10
	 * @param Order $order Order object.
	 * @param array $posted Post data.
	 */
	protected function update_subscription( $order, $posted ) {
		if ( ! masteriyo_order_contains_recurring_courses( $order ) ) {
			Paypal::log( "Order: {$order->get_id()} does not contain any recurring courses." );
			exit;
		}
		$subscription = masteriyo_get_subscription_from_order( $order );

		if ( ! $subscription ) {
			Paypal::log( "Order: {$order->get_id()} does not have a subscription." );
			exit;
		}

		$txn_type       = $posted['txn_type'];
		$payment_status = $posted['payment_status'];

		switch ( $txn_type ) {
			case 'subscr_payment':
				$subscription->set_props(
					array(
						'status'          => SubscriptionStatus::ACTIVE,
						'transaction_id'  => $posted['txn_id'],
						'subscription_id' => $posted['subscr_id'],
					)
				);
				Paypal::log( "Subscription: #{$subscription->get_id()} has been updated." );
				break;
			case 'recurring_payment':
			case 'recurring_payment_outstanding_payment':
				switch ( $payment_status ) {
					case 'failing':
					case 'failed':
						$subscription->set_status( SubscriptionStatus::FAILED );
						Paypal::log( "Subscription: #{$subscription->get_id()} has failed." );
						break;
					default:
						$subscription->set_status( SubscriptionStatus::ACTIVE );
						Paypal::log( "Subscription: #{$subscription->get_id()} has been updated." );
				}
				break;
			case 'recurring_payment_profile_cancel':
			case 'recurring_payment_suspended_due_to_max_failed_payment':
			case 'recurring_payment_suspended':
				$subscription->set_status( SubscriptionStatus::CANCELLED );
				Paypal::log( "Subscription: #{$subscription->get_id()} has been cancelled." );
				break;
			case 'recurring_payment_failed':
				$subscription->set_status( SubscriptionStatus::FAILED );
				Paypal::log( "Subscription: #{$subscription->get_id()} has failed." );
				break;
			case 'recurring_payment_expired':
				$subscription->set_status( SubscriptionStatus::EXPIRED );
				Paypal::log( "Subscription: #{$subscription->get_id()} has expired." );
				break;
		}

		$subscription->save();
	}
}
