<?php
/**
 * Swish Payments for Woocommerce.
 *
 * @package flo-checkout
 */

namespace Flo_Checkout\Gateways;

use Flo_Checkout\Gateways\Helpers\Gateway_Logger_Trait;
use Flo_Checkout\Gateways\Helpers\Interface_Flo_WC_Payment_Gateway;
use Flo_Checkout\Gateways\Swish\Swish_API;
use Flo_Checkout\Gateways\Swish\Swish_Callback_Handler;
use Flo_Checkout\Gateways\Swish\Swish_Certificates;
use Flo_Checkout\Gateways\Swish\Swish_Client;
use Flo_Checkout\Gateways\Swish\Swish_Connection_Test;
use Flo_Checkout\Gateways\Swish\Swish_Plugin_Update;
use Flo_Checkout\Gateways\Swish\Swish_Request_IDs;
use Flo_Checkout\Gateways\Swish\Swish_Settings;
use Flo_Checkout\Shared;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Data\Payment_Methods;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use WC_Order;
use WC_Payment_Gateway;

/**
 * Swish_Pay class.
 */
class Swish_Pay extends WC_Payment_Gateway implements Interface_Flo_WC_Payment_Gateway {
	use Gateway_Logger_Trait;

	const ORDER_META_SWISH_PAYMENT_LOCATION   = '_swish_payment_location';
	const ORDER_META_SWISH_PAYMENT_REQUEST_ID = '_swish_payment_request_id';
	const ORDER_META_SWISH_PAYMENT_STATUS     = '_swish_payment_status';
	const ORDER_META_SWISH_REFUND_REQUEST_ID  = '_swish_refund_request_id';

	/**
	 * Phone number of payer.
	 *
	 * @var string $payer_number
	 */
	private string $payer_number;

	/**
	 * Get Gateway ID.
	 *
	 * @return string
	 */
	public static function get_id(): string {
		return Payment_Methods::NMC_SWISH;
	}

	/**
	 * Whether the gateway supports blocks.
	 *
	 * @return bool
	 */
	public static function has_block_support(): bool {
		return true;
	}

	/**
	 * Setup run on each page load. Even if the gateway is not loaded.
	 *
	 * @return void
	 */
	public static function public_setup(): void {
		Swish_Plugin_Update::handle_gateway_update();
		Swish_Certificates::setup();
		Swish_Connection_Test::setup();

		add_action( 'wp_ajax_nopriv_swish_pay_status', array( self::class, 'get_swish_pay_status' ) );
		add_action( 'wp_ajax_swish_pay_status', array( self::class, 'get_swish_pay_status' ) );

		add_action( 'wp_ajax_nopriv_swish_pay_cancel', array( self::class, 'cancel_swish_transaction' ) );
		add_action( 'wp_ajax_swish_pay_cancel', array( self::class, 'cancel_swish_transaction' ) );
	}

	/**
	 * Class constructor
	 */
	public function __construct() {
		$this->id                 = self::get_id();
		$this->method_title       = __( 'Northmill Checkout - Swish', 'flo-checkout-plugin' );
		$this->method_description = __( 'Use Swish payment as payment gateway. Requires SSL and generate certificate from swish.', 'flo-checkout-plugin' );
		$this->has_fields         = true;
		$this->supports           = array(
			'products',
			'refunds',
		);

		// Get setting values.
		$options_title       = $this->get_option( 'title' );
		$options_description = $this->get_option( 'description' );

		$this->title       = $options_title ? $options_title : __( 'Swish', 'flo-checkout-plugin' );
		$this->description = $options_description ? $options_description : '';

		$this->init_form_fields();

		// Save settings fields.
		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ), 10, 0 );

		// Handles certificate uploads in settings.
		add_action( 'woocommerce_update_option', array( $this, 'process_admin_file_options' ) );

		// Registers Swish settings scripts and styles.
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );

		// Render Swish errors on its settings page.
		add_action( 'woocommerce_settings_checkout', array( $this, 'display_swish_errors' ) );

		if ( 'yes' === $this->enabled ) {
			// Registers Swish waiting page to the WooCommerce API.
			add_action( 'woocommerce_api_swish_pay_gateway_wait', array( $this, 'render_swish_waiting_screen' ), 10, 0 );

			// Registers Swish callback url to the WooCommerce API.
			add_action( 'woocommerce_api_swish_pay_gateway', array( Swish_Callback_Handler::class, 'process_callback' ), 10, 0 );

			// Registers LOCAL DEV function for the Swish callback url to the WooCommerce API.
			add_action( 'woocommerce_api_swish_pay_gateway_local_dev', array( Swish_Callback_Handler::class, 'process_local_dev_callback' ), 10, 0 );

			// Registers Swish thank you page scripts and styles.
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_thankyou_scripts' ) );

			// Renders Swish thank you page content.
			add_action( 'woocommerce_before_thankyou', array( $this, 'render_before_thankyou' ) );
		}
	}

	/**
	 * Plugin options, we deal with it in Step 3 too
	 */
	public function init_form_fields(): void {
		$this->form_fields = Swish_Settings::get_settings_fields();
	}

	/**
	 * Generate html for the certificate upload form.
	 *
	 * @param string $key  Field key.
	 * @param array  $data Field data.
	 *
	 * @return string HTML for form.
	 * @noinspection PhpUnused Called by parent class. WC_Settings_API
	 */
	public function generate_file_html( $key, $data ): string {
		$field_key = $this->get_field_key( $key );
		$defaults  = array(
			'title'             => '',
			'disabled'          => false,
			'class'             => '',
			'css'               => '',
			'placeholder'       => '',
			'type'              => 'text',
			'desc_tip'          => false,
			'description'       => '',
			'custom_attributes' => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		$value = empty( $data['custom_attributes']['no_value'] ) ? Swish_Settings::get( $key ) : '';

		ob_start();
		require Shared::instance()->package_info->paths->get_path() . 'views/swish/admin/file_input_row.php';
		return ob_get_clean();
	}

	/**
	 * Process settings file uploads. Called on settings page save.
	 *
	 * @param array $option_data Array of option data.
	 *
	 * @return void
	 *
	 * @phpcs:disable WordPress.Security.NonceVerification.Missing
	 */
	public function process_admin_file_options( array $option_data ): void {
		if (
			! empty( $option_data['id'] ) &&
			"woocommerce_{$this->id}_settings" === $option_data['id']
		) {
			$this->handle_cert_upload(
				$_FILES[ 'woocommerce_' . $this->id . '_' . Swish\Swish_Certificates::get_cert_info_by_type( 'public', 'filename' ) ] ?? array(),
				Swish\Swish_Certificates::get_cert_info_by_type( 'public', 'filename' ),
				'live_certificate',
				array(
					'pem',
					'p12',
				)
			);

			$this->handle_cert_upload(
				$_FILES[ 'woocommerce_' . $this->id . '_' . Swish\Swish_Certificates::get_cert_info_by_type( 'private', 'filename' ) ] ?? array(),
				Swish\Swish_Certificates::get_cert_info_by_type( 'private', 'filename' ),
				'live_private_certificate',
				array(
					'key',
					'p12',
				),
				true
			);

			$this->handle_cert_upload(
				$_FILES[ 'woocommerce_' . $this->id . '_' . Swish\Swish_Certificates::get_cert_info_by_type( 'sandbox_public', 'filename' ) ] ?? array(),
				Swish\Swish_Certificates::get_cert_info_by_type( 'sandbox_public', 'filename' ),
				'sandbox_certificate',
				array(
					'pem',
					'p12',
				)
			);

			$this->handle_cert_upload(
				$_FILES[ 'woocommerce_' . $this->id . '_' . Swish\Swish_Certificates::get_cert_info_by_type( 'sandbox_private', 'filename' ) ] ?? array(),
				Swish\Swish_Certificates::get_cert_info_by_type( 'sandbox_private', 'filename' ),
				'sandbox_private_certificate',
				array(
					'key',
					'p12',
				),
				true
			);
		}
	}
	// @phpcs:enable

	/**
	 * Handles the upload and processing of a certificate file.
	 *
	 * @param array  $file                The uploaded file data.
	 * @param string $filename            The name of the uploaded file.
	 * @param string $option_name         The name of the option to store the uploaded file.
	 * @param array  $accepted_extensions An array of accepted file extensions.
	 * @param bool   $is_private_key      Optional. Whether the uploaded file is a private key file. Default is false.
	 *
	 * @return void
	 */
	private function handle_cert_upload(
		array $file,
		string $filename,
		string $option_name,
		array $accepted_extensions,
		bool $is_private_key = false
	): void {
		$uploaded_file = Swish_Certificates::handle_uploaded_cert_file(
			$file,
			$filename,
			$accepted_extensions,
			$is_private_key
		);

		if ( is_wp_error( $uploaded_file ) ) {
			$this->add_error( $uploaded_file->get_error_message() );
		} elseif ( ! empty( $uploaded_file ) ) {
			$this->settings[ $option_name ] = $uploaded_file;
		}
	}

	/**
	 * Displays errors on settings page.
	 *
	 * @return void
	 */
	public function display_swish_errors(): void {
		global $current_section;
		if ( $current_section === $this->id ) {
			$this->display_errors();
		}
	}

	/**
	 * Enqueue admin scripts files.
	 *
	 * @return void
	 */
	public function admin_scripts(): void {

		wp_enqueue_style(
			'flo-checkout-swish-admin',
			Shared::instance()->assets->get_package_assets_url() . 'css/admin/flo_swish_settings.css',
			array(),
			filemtime( Shared::instance()->assets->get_package_assets_path() . 'css/admin/flo_swish_settings.css' )
		);

		wp_enqueue_script(
			'flo-checkout-swish-admin',
			Shared::instance()->assets->get_package_assets_url() . 'js/flo_swish_settings.js',
			array( 'jquery' ),
			filemtime( Shared::instance()->assets->get_package_assets_path() . 'js/flo_swish_settings.js' ),
			true
		);

		wp_localize_script(
			'flo-checkout-swish-admin',
			'flo_swish_settings',
			array(
				'ajax_url'                      => admin_url( 'admin-ajax.php' ),
				'nonce'                         => wp_create_nonce( 'flo_swish_settings' ),
				'abort_nonce'                   => wp_create_nonce( 'flo_swish_settings_abort_request' ),
				'generate_cert_nonce'           => wp_create_nonce( 'flo_swish_settings_generate_certificate' ),
				'connection_test_error_message' => __( 'An error occurred when running connection test, please try again.', 'flo-checkout-plugin' ),
				'cert_generation_error_message' => __( 'The following error occurred when generating certificate, did you fill in all the fields?', 'flo-checkout-plugin' ),
			)
		);
	}

	/**
	 * Renders additional info and fields for payment method in checkout.
	 */
	public function payment_fields(): void {
		require Shared::instance()->package_info->paths->get_path() . 'views/swish/checkout_fields.php';
	}

	/**
	 * Enqueues the thank you scripts.
	 *
	 * This method is used to enqueue the necessary scripts and styles
	 * on the order received page, specifically for the thank you page.
	 *
	 * @return void
	 */
	public function enqueue_thankyou_scripts(): void {
		// Only enqueue on order received page.
		if (
			! is_checkout() &&
			! empty( is_wc_endpoint_url( 'order-received' ) )
		) {
			return;
		}

		wp_enqueue_style( 'mf-swish-pay', Shared::instance()->assets->get_package_assets_url() . 'css/flo_pay_swish.css', array(), filemtime( Shared::instance()->assets->get_package_assets_path() . 'css/flo_pay_swish.css' ) );
	}

	/**
	 * Renders content before the thank you section on the order received page.
	 *
	 * This method is used to render custom content before the thank you section
	 * on the order received page for orders paid using the Swish Pay gateway.
	 *
	 * @param int $order_id The ID of the WooCommerce order.
	 *
	 * @return void
	 */
	public function render_before_thankyou( $order_id ): void {
		$order = wc_get_order( $order_id );
		if (
			is_a( $order, 'WC_Order' ) &&
			$order->get_payment_method() === self::get_id()
		) {
			$logo = Shared::instance()->assets->get_package_assets_url() . 'images/swish_horizontal_main_rgb.png';

			include Shared::instance()->package_info->paths->get_path() . 'views/swish/thank_you.php';
		}
	}

	/**
	 * Validates frontend fields
	 * More in Step 5
	 *
	 * @phpcs:disable WordPress.Security.NonceVerification.Missing, Nonce is verified in parent class.
	 */
	public function validate_fields(): bool {
		$payer_raw     = isset( $_POST[ esc_attr( $this->id ) . '_payer' ] ) ? $_POST[ esc_attr( $this->id ) . '_payer' ] : '';
		$payer         = preg_replace( '/[^0-9]/', '', $payer_raw );
		$number_length = strlen( $payer );

		if ( 0 === $number_length ) {
			wc_add_notice( __( 'Swish number missing', 'flo-checkout-plugin' ), 'error' );

			return false;
		}

		// Make sure the phone number starts with 46.
		if ( '0' === $payer[0] ) {
			$payer         = '46' . substr( $payer, 1 );
			$number_length = strlen( $payer );
		}

		if ( $number_length < 8 ) {
			wc_add_notice( __( 'Swish number must be at least 8 characters long', 'flo-checkout-plugin' ), 'error' );

			return false;
		}
		if ( $number_length > 15 ) {
			wc_add_notice( __( 'Swish number can be maximum 15 characters long', 'flo-checkout-plugin' ), 'error' );

			return false;
		}
		$this->payer_number = $payer;

		return true;
	}

	// @phpcs:enable

	/**
	 * Begins processing payment.
	 * Will redirect user to Swish waiting page on success, else add wc notice and return empty.
	 *
	 * @param int $order_id Order ID.
	 */
	public function process_payment( $order_id ): array {
		$order = new WC_Order( $order_id );

		Swish_Settings::log( 'process_payment' );

		$request_id   = Swish_Request_IDs::generate();
		$payment_data = Swish_Client::prepare_payment_data(
			$order,
			$this->payer_number
		);

		Swish_Settings::log(
			'Payment data',
			array(
				'payment_data' => $payment_data,
				'endpoint'     => Swish_Settings::get_swish_setting( 'api_end_url' ),
			)
		);

		$payment = Swish_API::api_request( $payment_data, "paymentrequests/$request_id", 'PUT', 'v2' );

		Swish_Settings::log(
			'Payment response',
			array(
				'response' => $payment,
				'endpoint' => Swish_Settings::get_swish_setting( 'api_end_url' ),
			)
		);

		if ( $payment['status'] ) {
			Swish_Client::store_callback_id( $order_id, $payment_data['callbackIdentifier'] );
			update_post_meta( $order_id, self::ORDER_META_SWISH_PAYMENT_LOCATION, $payment['response'] );
			update_post_meta( $order_id, self::ORDER_META_SWISH_PAYMENT_REQUEST_ID, $request_id );
			update_post_meta( $order_id, self::ORDER_META_SWISH_PAYMENT_STATUS, 'WAITING' );

			// Mark as pending (we're awaiting the Swish confirmation).
			$order->add_order_note( __( 'Payment initiated', 'flo-checkout-plugin' ) );
			$order->update_status( 'wc-pending', __( 'Awaiting Swish payment confirmation', 'flo-checkout-plugin' ) );

			// Redirect user to Swish payment waiting page.
			return array(
				'result'   => 'success',
				'redirect' => add_query_arg(
					array(
						'event_type' => 'process',
						'order_id'   => $order_id,
						'order_key'  => $order->get_order_key( 'edit' ),
					),
					WC()->api_request_url( 'swish_pay_gateway_wait' )
				),
			);
		}

		wc_add_notice( 'Error : ' . $payment['response'], 'error' );
		return array(
			'result' => 'fail',
		);
	}

	/**
	 * Process refunds
	 *
	 * @param int        $order_id Order ID.
	 * @param float|null $amount Amount to refund.
	 * @param string     $reason Reason for refund.
	 *
	 * @return bool
	 */
	public function process_refund( $order_id, $amount = null, $reason = '' ): bool {
		Swish_Settings::log(
			'Process refund',
			array(
				'order_id' => $order_id,
			)
		);

		$order = wc_get_order( $order_id );

		$transaction_id = $order->get_transaction_id();

		if ( ! $transaction_id ) {
			Swish_Settings::log(
				'No transaction ID for order',
				array(
					'order_id' => $order_id,
				),
				Log_Level_Enum::WARNING
			);
		}

		$refund_payment_data = Swish_Client::prepare_refund_payment_data(
			$transaction_id,
			$order,
			$amount,
			$reason
		);

		Swish_Settings::log(
			'Payment refund data',
			array(
				'refund_payment_data' => $refund_payment_data,
			),
		);

		$request_id = Swish_Request_IDs::generate();
		$payment    = Swish_API::api_request( $refund_payment_data, "refunds/$request_id", 'PUT', 'v2' );

		Swish_Settings::log(
			'Payment refund response',
			array(
				'response' => $payment,
			),
		);

		if ( $payment['status'] ) {
			Swish_Client::store_callback_id( $order_id, $refund_payment_data['callbackIdentifier'] );
			update_post_meta( $order_id, self::ORDER_META_SWISH_REFUND_REQUEST_ID, $request_id );
			return true;
		} else {
			Swish_Settings::log(
				'Payment refund error above',
				null,
				Log_Level_Enum::ERROR
			);

			return false;
		}
	}

	/**
	 * Renders Swish waiting screen, via the WooCommerce API.
	 *
	 * @phpcs:disable WordPress.Security.NonceVerification.Recommended
	 * We won't require nonce on awaiting payment page, since we require the order key.
	 */
	public function render_swish_waiting_screen(): never {
		$error = false;
		if (
			! isset( $_GET['order_id'] ) ||
			! isset( $_GET['order_key'] ) ||
			! isset( $_GET['event_type'] ) ||
			'process' !== $_GET['event_type']
		) {
			$error = true;
		}

		/**
		 * Function wc_get_order_id_by_order_key() might actually return string.
		 *
		 * @noinspection PhpCastIsUnnecessaryInspection
		 */
		$order_id_from_key  = (int) wc_get_order_id_by_order_key( $_GET['order_key'] );
		$url_param_order_id = (int) $_GET['order_id'];

		if ( $order_id_from_key !== $url_param_order_id ) {
			$error = true;
		}

		if ( $error ) {
			echo '<a href="' . esc_attr( wc_get_cart_url() ) . '">' . esc_html__( 'Something went wrong, please go back and try again.', 'flo-checkout-plugin' );
			exit;
		}

		$order = wc_get_order( $order_id_from_key );

		$css_link = add_query_arg(
			array(
				'v' => filemtime( Shared::instance()->assets->get_package_assets_path() . 'css/flo_pay_swish.css' ),
			),
			Shared::instance()->assets->get_package_assets_url() . 'css/flo_pay_swish.css'
		);
		$js_links = array(
			add_query_arg(
				array(
					'v' => filemtime( ABSPATH . WPINC . '/js/jquery/jquery.js' ),
				),
				'/wp-includes/js/jquery/jquery.js'
			),
			add_query_arg(
				array(
					'v' => filemtime( Shared::instance()->assets->get_package_assets_path() . 'js/flo_swish_pay.js' ),
				),
				Shared::instance()->assets->get_package_assets_url() . 'js/flo_swish_pay.js'
			),
		);

		/**
		 * Array keys are used in view file.
		 *
		 * @noinspection PhpArrayWriteIsNotUsedInspection
		 */
		$js_local = array(
			'ajaxurl'            => admin_url( 'admin-ajax.php' ),
			'nonce'              => wp_create_nonce( 'flo_swish_pay' ),
			'redirect_success'   => $order->get_checkout_order_received_url(),
			'redirect_cancelled' => apply_filters( 'moreflo_checkout_swish_pay_redirect_cancelled', $order->get_cancel_order_url_raw() ),
			'redirect_cancel'    => $order->get_cancel_order_url_raw(),
		);
		$logo     = Shared::instance()->assets->get_package_assets_url() . 'images/swish_horizontal_main_rgb.png';

		$swish_environment = Swish_Settings::get( 'swish_environment' );

		include Shared::instance()->package_info->paths->get_path() . 'views/swish/awaiting_payment.php';
		exit;
	}
	// @phpcs:enable

	/**
	 * Ajax function to get Swish payment status.
	 */
	public static function get_swish_pay_status(): void {
		if ( ! wp_verify_nonce( $_POST['valid'], 'flo_swish_pay' ) ) {
			wp_send_json_error( 'not verified' );
		}

		preg_match( '/=(wc_order_[^&]*)/', $_POST['url'], $key );

		$order_id = wc_get_order_id_by_order_key( $key[1] );

		if ( $order_id ) {

			$order = wc_get_order( $order_id );

			$status             = $order->get_status();
			$status_translation = array(
				'cancelled' => __( 'cancelled', 'flo-checkout-plugin' ),
				'pending'   => __( 'pending', 'flo-checkout-plugin' ),
			);
			$order_status_array = array( 'processing', 'completed', 'cancelled', 'failed' );
			$order_status_msg   = array(
				'processing' => __( 'Thank you, You have successfully approved your payment', 'flo-checkout-plugin' ),
				'completed'  => __( 'Thank you, You have successfully approved your payment', 'flo-checkout-plugin' ),
				'cancelled'  => sprintf(
						// translators: %s is the order status.
					__(
						'Thank you, You have %s your payment',
						'flo-checkout-plugin'
					),
					$status_translation[ $status ] ?? $status
				),
				'failed'     => __( 'There were errors on you payment, Please contact shop for details', 'flo-checkout-plugin' ),
			);

			if ( in_array( $status, $order_status_array, true ) ) {
				$response = array(
					'error'   => false,
					'status'  => $status,
					'message' => $order_status_msg[ $status ],
				);
			} else {
				$response = array(
					'error' => true,
				);
			}
		} else {

			$response = array(
				'error'   => true,
				'message' => __( 'Error on recognize you order, please contact your shop', 'flo-checkout-plugin' ),
			);
		}

		wp_send_json( $response );
	}

	/**
	 * Ajax action to cancel Swish transaction.
	 */
	public static function cancel_swish_transaction(): void {
		if ( ! wp_verify_nonce( $_POST['valid'], 'flo_swish_pay' ) ) {
			wp_send_json_error( 'not verified' );
		}

		preg_match( '/=(wc_order_[^&]*)/', $_POST['url'], $key );

		$order_id = wc_get_order_id_by_order_key( $key[1] );

		if ( $order_id ) {
			$request_id = get_post_meta( $order_id, self::ORDER_META_SWISH_PAYMENT_REQUEST_ID, true );

			if ( empty( $request_id ) ) {
				wp_send_json_error( 'No request id found' );
			}

			$response = Swish_API::api_request(
				array(
					array(
						'op'    => 'replace',
						'path'  => '/status',
						'value' => 'cancelled',
					),
				),
				"paymentrequests/$request_id",
				'PATCH'
			);

			Swish_Settings::log(
				'Payment cancelled',
				array(
					'response' => $response,
					'payment'  => $request_id,
				)
			);

			wp_send_json_success();
		}
	}
}
