<?php
/**
 * SEPA Gateway
 *
 * @package checkout-plugins-stripe-woo
 * @since 1.4.0
 */

namespace CPSW\Gateway\Stripe;

use CPSW\Inc\Helper;
use CPSW\Inc\Logger;
use CPSW\Inc\Traits\Get_Instance;
use CPSW\Inc\Traits\Subscriptions;
use CPSW\Gateway\Local_Gateway;
use CPSW\Gateway\Stripe\Stripe_Api;
use CPSW\Inc\Token;
use Exception;
use WP_Error;
use WC_AJAX;

/**
 * SEPA
 *
 * @since 1.4.0
 */
class Sepa extends Local_Gateway {

	use Get_Instance;
	use Subscriptions;

	/**
	 * Gateway id
	 *
	 * @var string
	 */
	public $id = 'cpsw_sepa';

	/**
	 * Payment method types
	 *
	 * @var string
	 */
	public $payment_method_types = 'sepa_debit';

	/**
	 * Retry interval
	 *
	 * @var int
	 */
	public $retry_interval = 1;

	/**
	 * Constructor
	 *
	 * @since 1.4.0
	 */
	public function __construct() {
		parent::__construct();

		$this->method_title       = __( 'SEPA', 'checkout-plugins-stripe-woo' );
		$this->method_description = $this->method_description();
		$this->has_fields         = true;
		$this->init_supports();
		$this->maybe_init_subscriptions();

		$this->init_form_fields();
		$this->init_settings();
		// get_option should be called after init_form_fields().
		$this->title                = $this->get_option( 'title' );
		$this->description          = $this->get_option( 'description' );
		$this->order_button_text    = $this->get_option( 'order_button_text' );
		$this->enable_saved_cards   = $this->get_option( 'enable_saved_card' );
		$this->company_name         = $this->get_option( 'company_name' );
		$this->statement_descriptor = $this->clean_statement_descriptor( $this->get_option( 'statement_descriptor' ) );
		$this->payment_conform      = true;

		add_action( 'wc_ajax_' . $this->id . '_verify_payment_intent', [ $this, 'verify_intent' ] );
		add_action( 'woocommerce_payment_token_class', [ $this, 'modify_token_class' ], 15, 2 );
		add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_saved_payment_methods_list' ], 10, 2 );
		add_filter( 'woocommerce_payment_successful_result', [ $this, 'modify_successful_payment_result' ], 999, 2 );
	}

	/**
	 * Controls the output on the my account page.
	 *
	 * @since 1.4.0
	 *
	 * @param  array            $item  Individual list item from woocommerce_saved_payment_methods_list.
	 * @param  WC_Payment_Token $token The payment token associated with this method entry.
	 *
	 * @return array $item
	 */
	public function get_saved_payment_methods_list( $item, $token ) {
		if ( 'cpsw_sepa' === strtolower( $token->get_type() ) ) {
			$item['method']['last4'] = $token->get_last4();
			$item['method']['brand'] = esc_html__( 'SEPA IBAN', 'checkout-plugins-stripe-woo' );
		}

		return $item;
	}

	/**
	 * Added token class
	 *
	 * @since 1.4.0
	 *
	 * @param string $class token class name.
	 * @param string $type gateway name.
	 *
	 * @return string
	 */
	public function modify_token_class( $class, $type ) {
		if ( 'cpsw_sepa' === $type ) {
			return 'CPSW\Inc\Token';
		}

		return $class;
	}

	/**
	 * Description for SEPA gateway
	 *
	 * @since 1.4.0
	 *
	 * @return string
	 */
	public function method_description() {
		$payment_description = $this->payment_description();

		return sprintf(
			/* translators: %1$s: Break, %2$s: HTML entities */
			__( 'Accept payments using SEPA. %1$s %2$s', 'checkout-plugins-stripe-woo' ),
			'<br/>',
			$payment_description
		);
	}

	/**
	 * Returns all supported currencies for this payment method.
	 *
	 * @since 1.4.0
	 *
	 * @return array
	 */
	public function get_supported_currency() {
		return apply_filters(
			'cpsw_sepa_supported_currencies',
			[
				'EUR',
			]
		);
	}

	/**
	 * Init payment supports.
	 *
	 * @since 1.4.0
	 *
	 * @return void
	 */
	public function init_supports() {
		parent::init_supports();
		$this->supports[] = 'tokenization';
		$this->supports[] = 'add_payment_method';
		$this->supports[] = 'pre-orders';
	}

	/**
	 * Process payment method functionality
	 *
	 * @return array
	 */
	public function add_payment_method() {
		$source_id = '';
		if ( empty( $_POST['payment_method_created'] ) && empty( $_POST['stripe_token'] ) || ! is_user_logged_in() ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			$error_msg = __( 'There was a problem adding the payment method.', 'checkout-plugins-stripe-woo' );
			/* translators: error msg */
			Logger::error( sprintf( __( 'Add payment method Error: %1$1s', 'checkout-plugins-stripe-woo' ), $error_msg ) );
			return;
		}

		$customer_id   = $this->get_customer_id();
		$source        = ! empty( $_POST['payment_method_created'] ) ? wc_clean( wp_unslash( $_POST['payment_method_created'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
		$stripe_api    = new Stripe_Api();
		$response      = $stripe_api->payment_methods( 'retrieve', [ $source ] );
		$source_object = $response['success'] ? $response['data'] : false;

		if ( isset( $source_object ) ) {
			if ( ! empty( $source_object->error ) ) {
				$error_msg = __( 'Invalid stripe source', 'checkout-plugins-stripe-woo' );
				wc_add_notice( $error_msg, 'error' );
				/* translators: error msg */
				Logger::error( sprintf( __( 'Add payment method Error: %1$1s', 'checkout-plugins-stripe-woo' ), $error_msg ) );
				return;
			}

			$source_id = $source_object->id;
		}
		$stripe_api = new Stripe_Api();
		$response   = $stripe_api->payment_methods( 'attach', [ $source_id, [ 'customer' => $customer_id ] ] );
		$response   = $response['success'] ? $response['data'] : false;
		$user       = wp_get_current_user();
		$user_id    = ( $user->ID && $user->ID > 0 ) ? $user->ID : false;
		$this->create_payment_token_for_user( $user_id, $source_object );

		if ( ! $response || is_wp_error( $response ) || ! empty( $response->error ) ) {
			$error_msg = __( 'Unble to attach payment method to customer', 'checkout-plugins-stripe-woo' );
			wc_add_notice( $error_msg, 'error' );
			/* translators: error msg */
			Logger::error( sprintf( __( 'Add payment method Error: %1$1s', 'checkout-plugins-stripe-woo' ), $error_msg ) );
			return;
		}

		do_action( 'cpsw_add_payment_method_' . ( isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '' ) . '_success', $source_id, $source_object ); //phpcs:ignore WordPress.Security.NonceVerification.Missing

		Logger::info( __( 'New payment method added successfully', 'checkout-plugins-stripe-woo' ) );
		return [
			'result'   => 'success',
			'redirect' => wc_get_endpoint_url( 'payment-methods' ),
		];
	}

	/**
	 * Add more gateway form fields
	 *
	 * @since 1.4.0
	 *
	 * @return array
	 */
	public function get_default_settings() {
		$company_name = [
			'enable_saved_card'    => [
				'label'       => __( 'Enable Payment via Saved IBAN', 'checkout-plugins-stripe-woo' ),
				'title'       => __( 'Saved IBAN', 'checkout-plugins-stripe-woo' ),
				'type'        => 'checkbox',
				'description' => __( 'Save IBAN details for future orders', 'checkout-plugins-stripe-woo' ),
				'default'     => 'yes',
				'desc_tip'    => true,
			],
			'statement_descriptor' => [
				'title'       => __( 'Statement Descriptor', 'checkout-plugins-stripe-woo' ),
				'type'        => 'text',
				'description' => __( 'Statement descriptors are limited to 22 characters, cannot use the special characters >, <, ", \, *, /, (, ), {, }, and must not consist solely of numbers. This will appear on your customer\'s statement in capital letters.', 'checkout-plugins-stripe-woo' ),
				'default'     => get_bloginfo( 'name' ),
				'desc_tip'    => true,
			],
			'company_name'         => [
				'title'       => __( 'Company Name', 'checkout-plugins-stripe-woo' ),
				'type'        => 'text',
				'default'     => get_bloginfo( 'name' ),
				'desc_tip'    => true,
				'description' => __( 'The name of your company that will appear in the SEPA mandate info.', 'checkout-plugins-stripe-woo' ),
			],
		];

		$local_settings = parent::get_default_settings();

		$local_settings['description']['default'] = __( 'Mandate Information.', 'checkout-plugins-stripe-woo' );

		return array_merge( $local_settings, $company_name );
	}

	/**
	 * Checks whether this gateway is available.
	 *
	 * @since 1.4.0
	 *
	 * @return boolean
	 */
	public function is_available() {
		if ( ! in_array( $this->get_currency(), $this->get_supported_currency(), true ) ) {
			return false;
		}

		if ( ! Helper::get_webhook_secret() ) {
			return false;
		}

		return parent::is_available();
	}

	/**
	 * Renders the Stripe elements form.
	 *
	 * @since 1.4.0
	 *
	 * @return void
	 */
	public function payment_form() {
		// translators: %s: company name.
		$description = Helper::get_sepa_mandate_description();
		?>
		<fieldset id="<?php echo esc_attr( $this->id ); ?>-form" class="wc-payment-form cpsw_stripe_sepa_payment_form">
			<?php echo wp_kses_post( wpautop( $description ) ); ?>
			<p class="form-row form-row-wide">
				<label for="cpsw-sepa-stripe-iban-element">
					<?php esc_html_e( 'IBAN.', 'checkout-plugins-stripe-woo' ); ?> <span class="required">*</span>
				</label>
				<div id="cpsw_stripe_sepa_iban_element" class="cpsw_stripe_sepa_iban_element_field">
					<!-- A Stripe Element will be inserted here. -->
				</div>
			</p>

			<!-- Used to display form errors -->
			<div class="cpsw_stripe_sepa_error" role="alert"></div>
			<div class="clear"></div>
		</fieldset>
		<?php
	}

	/**
	 * Creates markup for payment form for card payments
	 *
	 * @since 1.4.0
	 *
	 * @return void
	 */
	public function payment_fields() {
		$total                = WC()->cart->total;
		$display_tokenization = $this->supports( 'tokenization' ) && is_checkout() && 'yes' === $this->enable_saved_cards && is_user_logged_in();
		$description          = $this->get_description();
		$description          = ! empty( $description ) ? $description : '';

		// If paying from order, we need to get total from order not cart. Frontend summary nonce verification may not be required.
		if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			global $wp;

			$order = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) );
			$total = $order->get_total();
		}

		if ( is_add_payment_method_page() ) {
			$total = '';
		}

		/**
		 * Action before payment field.
		 *
		 * @since 1.4.0
		 */
		do_action( $this->id . '_before_payment_field_checkout' );

		echo '<div
			id="cpsw-sepa-payment-data"
			data-amount="' . esc_attr( $this->get_formatted_amount( $total ) ) . '"
			data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '">';

		if ( $display_tokenization ) {
			$this->tokenization_script();
			$this->saved_payment_methods();
		}

		$this->payment_form();

		// Nonce verification already done in parent WooCommerce function.
		if ( apply_filters( 'cpsw_sepa_display_save_payment_method_checkbox', $display_tokenization ) && ! $this->is_subscription_item_in_cart() && ! is_add_payment_method_page() && ! isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$this->save_payment_method_checkbox();
		}

		if ( 'live' !== Helper::get_payment_mode() ) {
			echo '<div class="cpsw-test-description">';
			/* translators: %1$1s - %6$6s: HTML Markup */
			echo wp_kses_post( Helper::get_sepa_test_mode_description() );
			echo '</div>';
		}

		echo '</div>';

		/**
		 * Action after payment field.
		 *
		 * @since 1.4.0
		 */
		do_action( $this->id . '_after_payment_field_checkout' );
	}

	/**
	 * Tokenize card payment
	 *
	 * @since 1.4.0
	 *
	 * @param int    $user_id id of current user placing .
	 * @param object $payment_method payment method object.
	 *
	 * @return object token object.
	 */
	public function create_payment_token_for_user( $user_id, $payment_method ) {
		$token = new Token();
		$token->set_last4( $payment_method->sepa_debit->last4 );
		$token->set_gateway_id( $this->id );
		$token->set_token( $payment_method->id );
		$token->set_user_id( $user_id );
		$token->save();

		return $token;
	}

	/**
	 * Process woocommerce orders after payment is done
	 *
	 * @since 1.4.0
	 *
	 * @param int $order_id wooCommerce order id.
	 *
	 * @return array data to redirect after payment processing.
	 */
	public function process_payment( $order_id ) {
		if ( $this->maybe_change_subscription_payment_method( $order_id ) ) {
			return $this->process_change_subscription_payment_method( $order_id );
		}

		$order = wc_get_order( $order_id );

		if ( 0 === absint( $order->get_total() ) ) {
			return $this->process_zero_amount_order( $order );
		}

		if ( $this->is_using_saved_payment_method() ) {
			return $this->process_payment_with_saved_payment_method( $order_id );
		}

		try {
			$customer_id     = $this->get_customer_id( $order );
			$idempotency_key = $order->get_order_key() . time();

			$data = [
				'amount'               => $this->get_formatted_amount( $order->get_total() ),
				'currency'             => $this->get_currency(),
				'description'          => $this->get_order_description( $order ),
				'metadata'             => $this->get_metadata( $order_id ),
				'payment_method_types' => [ 'sepa_debit' ],
				'customer'             => $customer_id,
			];

			if ( ! empty( trim( $this->statement_descriptor ) ) ) {
				$data['statement_descriptor_suffix'] = $this->statement_descriptor;
			}

			if ( $this->should_save_card( $order_id ) ) {
				$data['setup_future_usage'] = 'off_session';
			}

			/* translators: %1$1s order id, %2$2s order total amount  */
			Logger::info( sprintf( __( 'Begin processing payment with SEPA for order %1$1s for the amount of %2$2s', 'checkout-plugins-stripe-woo' ), $order_id, $order->get_total() ) );
			$intent_data = $this->get_payment_intent( $order_id, $idempotency_key, $data );

			if ( $intent_data ) {
				$response_data = [
					'result'        => 'success',
					'redirect'      => false,
					'intent_secret' => $intent_data['client_secret'],
					'save_card'     => $this->should_save_card( $order_id ),
				];
				/**
				 * This is used to verify nonce for payment method checkout perform by block checkout.
				 */
				if ( isset( $_POST['payment_local_nonce'] ) && wp_verify_nonce( sanitize_text_field( $_POST['payment_local_nonce'] ), 'stripe_local_nonce' ) ) {
					$response_data['verification_url'] = $this->get_verification_url( $order_id, $this->get_return_url( $order ), $this->should_save_card( $order_id ) );
				}

				return $response_data;

			} else {
				return [
					'result'   => 'fail',
					'redirect' => '',
				];
			}
		} catch ( Exception $e ) {
			Logger::error( $e->getMessage(), true );
			return new WP_Error( 'order-error', '<div class="woocommerce-error">' . $e->getMessage() . '</div>', [ 'status' => 200 ] );
		}
	}

	/**
	 * Prepare payment method object
	 *
	 * @since 1.4.5
	 *
	 * @param object $payment_method payment method object from intent.
	 * @param object $token token object used for payment.
	 *
	 * @return object
	 */
	public function prepare_payment_method_zero_amount( $payment_method, $token ) {
		return (object) apply_filters(
			'cpsw_prepare_payment_method_args',
			[
				'token_id'              => $token,
				'customer'              => $payment_method->customer,
				'source'                => $payment_method->id,
				'source_object'         => $payment_method,
				'payment_method'        => $payment_method->id,
				'payment_method_object' => $payment_method,
			]
		);
	}

	/**
	 * Process zero amount order
	 *
	 * @since 1.4.5
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	public function process_zero_amount_order( $order ) {
		$token                   = '';
		$prepared_payment_method = '';

		// Nonce verification already done in parent woocommerce function.
		if ( isset( $_POST['payment_method_created'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Missing
			$method_created          = ! empty( $_POST['payment_method_created'] ) ? wc_clean( wp_unslash( $_POST['payment_method_created'] ) ) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Missing
			$stripe_api              = new Stripe_Api();
			$response                = $stripe_api->payment_methods( 'retrieve', [ $method_created ] );
			$payment_method          = $response['success'] ? $response['data'] : false;
			$user                    = $order->get_id() ? $order->get_user() : wp_get_current_user();
			$user_id                 = $user->ID;
			$token                   = $this->create_payment_token_for_user( $user_id, $payment_method );
			$prepared_payment_method = $this->prepare_payment_method( $payment_method, $token );
		} elseif ( $this->is_using_saved_payment_method() ) {
			// Nonce verification already done in parent woocommerce function.
			$token                   = $this->get_token_from_request( $_POST ); //phpcs:ignore WordPress.Security.NonceVerification.Missing
			$stripe_api              = new Stripe_Api();
			$response                = $stripe_api->payment_methods( 'retrieve', [ $token ] );
			$payment_method          = $response['success'] ? $response['data'] : false;
			$prepared_payment_method = $this->prepare_payment_method_zero_amount( $payment_method, $token );
		}

		if ( ! empty( $token ) ) {
			$order->payment_complete();
			$this->save_payment_method_to_order( $order, $prepared_payment_method );

			return $this->process_change_subscription_payment_method( $order->get_id() );
		}

		return false;
	}

	/**
	 * Verify intent state and redirect.
	 *
	 * @since 1.4.0
	 *
	 * @return void
	 */
	public function verify_intent() {
		$checkout_url = wc_get_checkout_url();

		$order_id = isset( $_GET['order'] ) ? sanitize_text_field( $_GET['order'] ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// Check for empty order id.
		if ( empty( $order_id ) ) {
			wc_add_notice( __( 'No orders are found for provided order ID.', 'checkout-plugins-stripe-woo' ), 'error' );
			Logger::error( __( 'Invalid order Id.', 'checkout-plugins-stripe-woo' ) );
			wp_safe_redirect( $checkout_url );
			exit();
		}

		$order = wc_get_order( $order_id );

		// Check for empty order object.
		if ( empty( $order ) ) {
			wc_add_notice( __( 'No valid order found.', 'checkout-plugins-stripe-woo' ), 'error' );
			Logger::error( __( 'Invalid order. No order found for provided order ID.', 'checkout-plugins-stripe-woo' ) );
			wp_safe_redirect( $checkout_url );
			exit();
		}

		if ( ! isset( $_GET['order_key'] ) || ! $order->key_is_valid( wc_clean( $_GET['order_key'] ) ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			wc_add_notice( __( 'Invalid order key received.', 'checkout-plugins-stripe-woo' ), 'error' );
			Logger::error( __( 'Invalid order key.', 'checkout-plugins-stripe-woo' ) );
			wp_safe_redirect( $checkout_url );
			exit();
		}

		$intent_secret = $order->get_meta( '_cpsw_intent_secret' );
		$stripe_api    = new Stripe_Api();
		$response      = $stripe_api->payment_intents( 'retrieve', [ $intent_secret['id'] ] );
		$intent        = $response['success'] ? $response['data'] : false;

		if ( ! $intent ) {
			return;
		}

		if ( isset( $_GET['save_card'] ) && '1' === $_GET['save_card'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$user           = $order->get_id() ? $order->get_user() : wp_get_current_user();
			$user_id        = $user->ID;
			$payment_method = $intent->payment_method;
			$response       = $stripe_api->payment_methods( 'retrieve', [ $payment_method ] );
			$payment_method = $response['success'] ? $response['data'] : false;
			$token          = $this->create_payment_token_for_user( $user_id, $payment_method );
			/* translators: %1$1s order id, %2$2s token id  */
			Logger::info( sprintf( __( 'Payment method tokenized for Order id - %1$1s with token id - %2$2s', 'checkout-plugins-stripe-woo' ), $order_id, $token->get_id() ) );
			$prepared_payment_method = $this->prepare_payment_method( $payment_method, $token );
			$this->save_payment_method_to_order( $order, $prepared_payment_method );
		}

		if ( 'succeeded' === $intent->status ) {
			$redirect_to  = $this->process_order( end( $intent->charges->data ), $order_id );
			$redirect_url = apply_filters( 'cpsw_redirect_order_url', $redirect_to, $order );
		} elseif ( 'pending' === $intent->status || 'processing' === $intent->status ) {
			$order_stock_reduced = $order->get_meta( '_order_stock_reduced', true );

			if ( ! $order_stock_reduced ) {
				wc_reduce_stock_levels( $order_id );
			}

			$order->set_transaction_id( $intent->id );
			$others_info = __( 'Payment will be completed once payment_intent.succeeded webhook received from Stripe.', 'checkout-plugins-stripe-woo' );

			/* translators: transaction id, other info */
			$order->update_status( 'on-hold', sprintf( __( 'Stripe charge awaiting payment: %1$s. %2$s', 'checkout-plugins-stripe-woo' ), $intent->id, $others_info ) );
			$redirect_to  = $this->get_return_url( $order );
			$redirect_url = apply_filters( 'cpsw_redirect_order_url', $redirect_to, $order );
		} elseif ( isset( $response['data']->last_payment_error ) ) {
			$message = isset( $response['data']->last_payment_error->message ) ? $response['data']->last_payment_error->message : '';

			// translators: %s: payment fail message.
			wc_add_notice( sprintf( __( 'Payment failed. %s', 'checkout-plugins-stripe-woo' ), $message ), $notice_type = 'error' );
			/* translators: %1$1s order id, %2$2s payment fail message.  */
			Logger::error( sprintf( __( 'Payment failed for Order id - %1$1s. %2$2s', 'checkout-plugins-stripe-woo' ), $order_id, $message ) );
			$redirect_url = $checkout_url;
		}
		wp_safe_redirect( $redirect_url );
		exit();
	}

	/**
	 * Modify redirect url
	 *
	 * @since 1.4.0
	 *
	 * @param array $result redirect url array.
	 * @param int   $order_id woocommerce order id.
	 *
	 * @return array modified redirect url array.
	 */
	public function modify_successful_payment_result( $result, $order_id ) {
		if ( empty( $order_id ) ) {
			return $result;
		}

		$order = wc_get_order( $order_id );

		if ( $this->id !== $order->get_payment_method() ) {
			return $result;
		}

		if ( ! isset( $result['intent_secret'] ) ) {
			return $result;
		}

		// Put the final thank you page redirect into the verification URL.
		$verification_url = add_query_arg(
			[
				'order'                 => $order_id,
				'confirm_payment_nonce' => wp_create_nonce( 'cpsw_confirm_payment_intent' ),
				'redirect_to'           => rawurlencode( $result['redirect'] ),
				'save_card'             => $result['save_card'],
				'order_key'             => $order->get_order_key(),
			],
			WC_AJAX::get_endpoint( $this->id . '_verify_payment_intent' )
		);

		// Combine into a hash.
		$redirect = sprintf( '#confirm-pi-%s:%s:%s', $result['intent_secret'], rawurlencode( $verification_url ), $this->id );

		return [
			'result'   => 'success',
			'redirect' => $redirect,
		];
	}
}
