<?php
/**
 * WP Courseware Gateway Stripe - Api.
 *
 * @package WPCW
 * @subpackage Gateways\Stripe
 * @since 4.3.0
 */
namespace WPCW\Gateways\Stripe;

use Stripe\Charge;
use Stripe\Event;
use WPCW\Gateways\Gateway_Stripe;

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

/**
 * Class Stripe_Webhooks
 *
 * @since 4.3.0
 */
class Stripe_Webhooks {

	/**
	 * @var Gateway_Stripe
	 * @since 4.3.0
	 */
	protected $gateway;

	/**
	 * @var int Retry Interval.
	 * @since 4.3.0
	 */
	protected $retry_interval = 2;

	/**
	 * Stripe_Webhooks constructor.
	 *
	 * @since 4.3.0
	 *
	 * @param Gateway_Stripe The Stripe Gateway parent object.
	 */
	public function __construct( Gateway_Stripe $gateway ) {
		$this->gateway = $gateway;
	}

	/**
	 * Load Stripe Webhooks.
	 *
	 * @since 4.3.0
	 */
	public function load() {
		add_action( 'wpcw_api_gateway-stripe', array( $this, 'webhook_handler' ) );
	}

	/**
	 * Webhook Handler.
	 *
	 * @since 4.3.0
	 */
	public function webhook_handler() {
		$request_body    = file_get_contents( 'php://input' );
		$request_headers = array_change_key_case( $this->get_request_headers(), CASE_UPPER );

		if ( $this->is_valid_request( $request_headers, $request_body ) ) {
			status_header( 200 );
			$this->process_webhook( $request_body );
			exit;
		} else {
			status_header( 400 );
			$this->log( sprintf( 'Incoming webhook failed validation: Headers: %1$s - Body: %2$s', wpcw_print_r( $request_headers, true ), wpcw_print_r( $request_body, true ) ) );
			exit;
		}
	}

	/**
	 * Get Request Headers.
	 *
	 * @since 4.3.0
	 *
	 * @return array Requested headers.
	 */
	protected function get_request_headers() {
		if ( ! function_exists( 'getallheaders' ) ) {
			$headers = [];
			foreach ( $_SERVER as $name => $value ) {
				if ( 'HTTP_' === substr( $name, 0, 5 ) ) {
					$headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value;
				}
			}

			return $headers;
		} else {
			return getallheaders();
		}
	}

	/**
	 * Is Valid Request?
	 *
	 * @since 4.3.0
	 *
	 * Todo: Implement Stripe Signatures: https://stripe.com/docs/webhooks/signatures.
	 *
	 * @param string $request_headers The request headers from Stripe.
	 * @param string $request_body The request body from Stripe.
	 *
	 * @return bool
	 */
	protected function is_valid_request( $request_headers = null, $request_body = null ) {
		if ( null === $request_headers || null === $request_body ) {
			return false;
		}

		if ( ! empty( $request_headers['USER-AGENT'] ) && ! preg_match( '/Stripe/', $request_headers['USER-AGENT'] ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get Stripe Api.
	 *
	 * @since 4.3.0
	 *
	 * @throws Stripe_Exception
	 */
	protected function get_api() {
		$api = $this->gateway->get_api();

		if ( ! empty( $api->error ) ) {
			throw new Stripe_Exception( $api->error->message, $api->error->localized );
		}

		return $api->is_available() ? $api : false;
	}

	/**
	 * Log Message.
	 *
	 * @since 4.3.0
	 *
	 * @param string $message The message to log.
	 */
	protected function log( $message = '' ) {
		$this->gateway->log( $message );
	}

	/**
	 * Process Webhook.
	 *
	 * @since 4.0.0
	 * @version 4.0.0
	 *
	 * @param string $request_body
	 */
	protected function process_webhook( $request_body ) {
		$webhook = json_decode( $request_body );

		try {
			$this->gateway->setup();
			if ( isset( $webhook->id ) && $webhook->livemode ) {
				$api     = $this->get_api();
				$webhook = $api->get_event( $webhook->id );

				if ( ! empty( $webhook->error ) ) {
					throw new Stripe_Exception( $webhook->error->message, $webhook->error->localized );
				}
			}
		} catch ( Stripe_Exception $exception ) {
			$this->log( $exception->getMessage() );
			return;
		}

		$this->log( sprintf( 'About to process webhook type: %s', $webhook->type ) );
		// $this->log( wpcw_print_r( $webhook, true ) );

		// Define Webhook Data.
		$object = $webhook->data->object;

		// Check to see if there is any data.
		if ( empty( $object ) ) {
			$this->log( sprintf( 'Webhook data object is empty. Webhook Object: %s', wpcw_print_r( $webhook, true ) ) );
			return;
		}

		/** @var Event The webook object. */
		switch ( $webhook->type ) {
			case 'charge.refunded':
				$this->webhook_charge_refunded( $object, $webhook );
				break;
			case 'review.opened':
				$this->webhook_review_opened( $object, $webhook );
				break;
			case 'review.closed':
				$this->webhook_review_closed( $object, $webhook );
				break;
			case 'invoice.payment_failed' :
				$this->webhook_invoice_payment_failed( $object, $webhook );
				break;
			case 'invoice.payment_succeeded' :
				$this->webhook_invoice_payment_succeeded( $object, $webhook );
				break;
			case 'customer.subscription.deleted' :
				$this->webhook_customer_subscription_deleted( $object, $webhook );
				break;
			case 'customer.subscription.updated' :
				$this->webhook_customer_subscription_updated( $object, $webhook );
				break;
		}

		/**
		 * Action: Stripe Process Webhook.
		 *
		 * @since 4.3.0
		 *
		 * @param object The webhook data object.
		 * @param string The webhook type.
		 * @param Event The stripe event.
		 * @param Stripe_Webhooks The stripe webhooks object.
		 */
		do_action( 'wpcw_stripe_process_webhook', $object, $webhook->type, $webhook, $this );
	}

	/** Stripe Webhooks -------------------------------------------------- */

	/**
	 * Stripe Webhook: Charge Refunded.
	 *
	 * Occurs whenever a charge is refunded, including partial refunds.
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_charge_refunded( $object, $webhook ) {
		$order = wpcw()->orders->get_order_by_transaction_id( $object->id );

		if ( ! $order ) {
			$this->log( sprintf( 'Could not find order via transaction id: %s', $object->id ) );
			return;
		}

		if ( 'stripe' !== $order->get_payment_method() || $order->has_status( 'refunded' ) ) {
			$this->log( sprintf( 'Order #%1$s cannot or has already been refunded: Refund Id: %2$s', $order->get_order_id(), $object->id ) );
			return;
		}

		$refund        = $object->refunds->data[0];
		$refund_id     = $refund->id;
		$refund_amount = $this->gateway->format_stripe_amount( $refund->amount );
		$refund_reason = esc_html__( 'Refunded via Stripe Dashboard', 'wp-courseware' );

		$order->add_meta( '_stripe_refund_id', $refund_id );
		$order->add_meta( '_stripe_refund_amount', $refund_amount );
		$order->update_status( 'refunded', sprintf( __( 'Refunded %1$s - Refund ID: %2$s - %3$s', 'wp-courseware' ), wpcw_price( $refund_amount ), $refund_id, $refund_reason ) );
	}

	/**
	 * Stripe Webhook: Review Opened.
	 *
	 * Occurs whenever a review is opened.
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_review_opened( $object, $webhook ) {
		$order = wpcw()->orders->get_order_by_transaction_id( $object->charge );

		if ( ! $order ) {
			$this->log( sprintf( 'Could not find order via transaction id: %s', $object->charge ) );
			return;
		}

		/* translators: %1$s The stripe transaction url %2$s The stripe review reason. */
		$message = sprintf(
			__( 'A review has been opened for this order. Action is needed. Please go to your <a href="%1$s" title="Stripe Dashboard" target="_blank">Stripe Dashboard</a> to review the issue. Reason: (%2$s)', 'wp-courseware' ),
			$this->get_transaction_url( $order ),
			$object->reason
		);

		if ( apply_filters( 'wpcw_stripe_webhook_review_change_order_status', true, $order, $object, $webhook ) ) {
			$order->update_status( 'on-hold', $message );
		} else {
			$order->add_order_note( $message );
		}
	}

	/**
	 * Stripe Webhook: Review Closed.
	 *
	 * Occurs whenever a review is closed. The review's reason field indicates why:
	 * - approved
	 * - disputed
	 * - refunded
	 * - refunded_as_fraud
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_review_closed( $object, $webhook ) {
		$order = wpcw()->orders->get_order_by_transaction_id( $object->charge );

		if ( ! $order ) {
			$this->log( sprintf( 'Could not find order via transaction id: %s', $object->charge ) );
			return;
		}

		/* translators: %s: The reason type. */
		$message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'wp-courseware' ), $object->reason );

		if ( $order->has_order_status( 'on-hold' ) ) {
			if ( apply_filters( 'wpcw_stripe_webhook_review_change_order_status', true, $order, $object, $webhook ) ) {
				$order->update_status( 'processing', $message );
			} else {
				$order->add_order_note( $message );
			}
		} else {
			$order->add_order_note( $message );
		}
	}

	/**
	 * Stripe Webhook: Invoice Payment Failed.
	 *
	 * Occurs whenever an invoice payment fails.
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_invoice_payment_failed( $object, $webhook ) {
		// Data.
		$subscription_id = $object->subscription;
		$transaction_id  = $object->charge;

		// Subscription.
		$subscription = wpcw()->subscriptions->get_subscription_by_profile_id( $subscription_id );

		if ( ! $subscription ) {
			$this->log( sprintf( 'Could not find subscription via subscription id: %s', $subscription_id ) );
			return;
		}

		// Payment Failed
		$subscription->add_note( sprintf( __( 'Stripe Subscription Payment Failed. Transaction Id: %s', 'wp-courseware' ), $transaction_id ) );
		$subscription->payment_failed();
	}

	/**
	 * Stripe Webhook: Invoice Payment Succeeded.
	 *
	 * Occurs whenever an invoice payment succeeds.
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_invoice_payment_succeeded( $object, $webhook ) {
		// Data.
		$subscription_id = $object->subscription;
		$transaction_id  = $object->charge;

		// Subscription.
		$subscription = wpcw()->subscriptions->get_subscription_by_profile_id( $subscription_id );

		if ( ! $subscription ) {
			$this->log( sprintf( 'Could not find subscription via subscription id: %s', $subscription_id ) );
			return;
		}

		// Check for Payments
		$payments = $subscription->get_payments( array( 'transaction_id' => $transaction_id ) );

		// Bail if there are existing payments with the same transaction id.
		if ( ! empty( $payments ) ) {
			$this->log( sprintf( 'Payment already recorded for Subscription %s and Transaction Id %s. Aborting...', $subscription->get_id(), $transaction_id ) );
			return;
		}

		$this->log( sprintf( 'No payments recorded for Subscription %s and Transaction Id %s. Creating new payment order.', $subscription->get_id(), $transaction_id ) );

		// Sub Data.
		$subtotal   = wpcw_round( $object->subtotal / 100 );
		$tax        = wpcw_round( $object->tax / 100 );
		$total      = wpcw_round( $object->total / 100 );
		$period_end = $object->period_end;
		$paid       = $object->paid;
		$bill_times = $subscription->get_bill_times();

		// Stripe Subscription.
		$stripe_subscription = $this->gateway->get_api()->get_subscription( $subscription_id );

		// Record Error.
		if ( ! empty( $stripe_subscription->error ) ) {
			$this->log( $stripe_subscription->error->message );
			return;
		}

		// Check Status.
		if ( in_array( $stripe_subscription->status, array( 'past_due', 'canceled', 'unpaid' ) ) ) {
			switch ( $stripe_subscription->status ) {
				case 'canceled' :
					$subscription->cancel();
					break;
				case 'past_due' :
					$subscription->update_status( 'on-hold', sprintf( __( 'Subscription #%1$s on hold Stripe has reported that the subscription is past due. Stripe Subscription ID: %2$s', 'wp-courseware' ), $subscription->get_id(), $subscription_id ) );
					break;
				case 'unpaid' :
					$subscription->update_status( 'on-hold', sprintf( __( 'Subscription #%1$s on hold Stripe has reported that the subscription is unpaid. Stripe Subscription ID: %2$s', 'wp-courseware' ), $subscription->get_id(), $subscription_id ) );
					break;
			}
			return;
		}

		// Create Payment.
		$payment = $subscription->create_payment();
		$payment->set_props( array(
			'subscription_id' => $subscription->get_id(),
			'transaction_id'  => $transaction_id,
		) );

		// Get Subscription Payment.
		$stripe_charge = $this->gateway->get_api()->get_charge( $transaction_id );

		// Check for the payment and make sure its paid, if not handle it in the webhook.
		if ( empty( $stripe_charge->error ) && false !== $stripe_charge && $stripe_charge instanceof Charge && $stripe_charge->paid ) {
			// Store Fees.
			if ( isset( $stripe_charge->balance_transaction ) && isset( $stripe_charge->balance_transaction->fee ) ) {
				$amount = ! empty( $stripe_charge->balance_transaction->amount ) ? $this->gateway->format_balance_fee( $stripe_charge->balance_transaction, 'amount' ) : 0.00;
				$payment->update_meta( '_stripe_amount', $amount );

				$fee = ! empty( $stripe_charge->balance_transaction->fee ) ? $this->gateway->format_balance_fee( $stripe_charge->balance_transaction, 'fee' ) : 0.00;
				$payment->update_meta( '_stripe_fee', $fee );

				$net = ! empty( $stripe_charge->balance_transaction->net ) ? $this->gateway->format_balance_fee( $stripe_charge->balance_transaction, 'net' ) : 0.00;
				$payment->update_meta( '_stripe_net', $net );

				$currency = ! empty( $stripe_charge->balance_transaction->currency ) ? strtoupper( $stripe_charge->balance_transaction->currency ) : null;
				$payment->update_meta( '_stripe_currency', $currency );

				// Customer Id.
				if ( ! empty( $stripe_charge->customer ) ) {
					$payment->update_meta( '_stripe_customer_id', $stripe_charge->customer );
				}

				// Source Id.
				if ( ! empty( $stripe_charge->source->id ) ) {
					$payment->update_meta( '_stripe_source_id', $stripe_charge->source->id );
				}

				// Add Note.
				$payment->add_order_note( sprintf( __( 'Recorded Stripe Fees. Total Amount: %1$s, Fee: %2$s, Net Amount: %3$s, Currency: %4$s', 'wp-courseware' ), wpcw_price( $amount ), wpcw_price( $fee ), wpcw_price( $net ), $currency ) );
			}
		}

		// Set Properties.
		$subscription->set_prop( 'transaction_id', $transaction_id );
		$subscription->set_prop( 'expiration', date( 'Y-m-d H:i:s', $stripe_subscription->current_period_end ) );

		// Increment Bill times if paid.
		if ( $paid ) {
			$subscription->set_prop( 'bill_times', ( $bill_times + 1 ) );
		}

		// Set Status.
		if ( 'active' === $stripe_subscription->status ) {
			$subscription->set_status( 'active', sprintf( __( 'Subscription #%1$s renewed. Stripe Subscription ID: %2$s', 'wp-courseware' ), $subscription->get_id(), $subscription_id ) );
		} else {
			$subscription->set_status( 'on-hold', sprintf( __( 'Subscription #%1$s on hold. Stripe Subscription ID: %2$s', 'wp-courseware' ), $subscription->get_id(), $subscription_id ) );
		}

		// Save Subscription.
		if ( $subscription->save() ) {
			$this->log( sprintf( __( 'Subscription #%1$s saved successfully. Subscription Data: %2$s', 'wp-courseware' ), $subscription->get_id(), wpcw_print_r( $subscription->get_data(), true ) ) );
		} else {
			$this->log( sprintf( __( 'Subscription #%1$s save error. Subscription Data: %2$s', 'wp-courseware' ), $subscription->get_id(), wpcw_print_r( $subscription->get_data(), true ) ) );
		}

		// Set Status.
		if ( $subscription->has_status( 'active' ) ) {
			$payment->payment_complete( $transaction_id, sprintf( __( 'Payment Order #%s is complete. Stripe Txn Id: %s', 'wp-courseware' ), $payment->get_order_number(), $transaction_id ) );
		} else {
			$payment->update_status( 'on-hold', sprintf( __( 'Order #%s is pending payment. Stripe Txn Id: %s', 'wp-courseware' ), $payment->get_order_number(), $transaction_id ) );
		}
	}

	/**
	 * Stripe Webhook: Customer Subscription Deleted
	 *
	 * Occurs whenever a subscription is deleted.
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_customer_subscription_deleted( $object, $webhook ) {
		$subscription = wpcw()->subscriptions->get_subscription_by_profile_id( $object->id );

		if ( ! $subscription ) {
			$this->log( sprintf( 'Could not find subscription via subscription id: %s', $object->id ) );
			return;
		}

		// Cancel the subscription.
		$subscription->cancel();
	}

	/**
	 * Stripe Webhook: Customer Subscription Updated
	 *
	 * Occurs whenever a subscription is updated.
	 *
	 * @since 4.3.0
	 *
	 * @param object $object The stripe webhook data object.
	 * @param Event $webhook The stripe webhook event object.
	 */
	protected function webhook_customer_subscription_updated( $object, $webhook ) {
		$subscription = wpcw()->subscriptions->get_subscription_by_profile_id( $object->id );

		if ( ! $subscription ) {
			$this->log( sprintf( 'Could not find subscription via subscription id: %s', $object->id ) );
			return;
		}

		$old_amount = $subscription->get_recurring_amount();
		$new_amount = $this->gateway->format_stripe_amount( $object->plan->amount );

		if ( $old_amount !== $new_amount ) {
			$subscription->set_prop( 'recurring_amount', $new_amount );
			$subscription->update_status( sprintf( __( 'Subscription recurring amount changed from %1$s to %2$s in Stripe', 'wp-courseware' ), wpcw_price( $old_amount ), wpcw_price( $new_amount ) ) );
		}
	}
}