<?php
/**
 * Handles the Swish payment gateway callback requests.
 *
 * @package flo-checkout
 */

namespace Flo_Checkout\Gateways\Swish;

use Flo_Checkout\Gateways\Swish_Pay;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use stdClass;
use WC_Data_Exception;
use WC_Order;
use WC_Order_Refund;

defined( 'ABSPATH' ) || exit;

/**
 * Handles the Swish payment gateway callback requests.
 */
class Swish_Callback_Handler {

	/**
	 * Flag indicating whether the code is executed in a local development environment.
	 *
	 * @var bool $is_local_dev Set to true if the code is running in a local development environment, false otherwise.
	 */
	private static bool $is_local_dev = false;

	/**
	 * Handles callback from Swish.
	 *
	 * @phpcs:disable WordPress.Security.NonceVerification.Recommended
	 * We won't require nonce on Swish callback.
	 */
	public static function process_callback(): void {
		self::$is_local_dev = false;
		Swish_Settings::log(
			'Processing Swish callback',
			array( 'request' => $_REQUEST )
		);

		$order = self::validate_order();
		$input = self::validate_input( $order );

		$result = self::handle_callback( $order, $input );

		// Send received notification to server.
		wp_send_json( $result, $result['successCode'] );
	}

	/**
	 * Allows for testing swish payments locally.
	 * Requires constant WP_ENVIRONMENT_TYPE be set to 'local' and Swish payment gateway environment to be 'sandbox' or 'mss'.
	 *
	 * Trigger at:
	 * /?wc-api=swish_pay_gateway_local_dev&event=payment&status=paid&order_id=42
	 * Replace order_id.
	 *
	 * Parameter event and status can also be changed.
	 *
	 * @return never
	 */
	public static function process_local_dev_callback(): never {
		$swish_environment   = Swish_Settings::get( 'swish_environment' );
		$required_swish_envs = array( 'sandbox', 'mss' );

		// Only allow local dev callback if WP_ENVIRONMENT_TYPE is 'local' and Swish environment is 'sandbox' or 'mss'.
		if (
			! in_array( $swish_environment, $required_swish_envs, true ) ||
			'local' !== wp_get_environment_type()
		) {
			Swish_Settings::log(
				'Aborting attempt for local Swish callback in prod env',
				array(
					'order_id'  => $_REQUEST['order_id'] ?? '',
					'wp_env'    => wp_get_environment_type(),
					'swish_env' => $swish_environment,
				),
				Log_Level_Enum::WARNING
			);

			wp_die();
		}
		self::$is_local_dev = true;

		Swish_Settings::log(
			'swish_callback_response_LOCAL_DEV',
			array(
				'request' => $_REQUEST,
			)
		);

		$order_id = $_REQUEST['order_id'] ?? false;
		$order    = wc_get_order( $order_id );

		if ( ! $order_id || ! $order ) {
			Swish_Settings::log(
				'swish_callback_response_LOCAL_DEV order not found',
				array(
					'request'  => $_REQUEST,
					'order_id' => $order_id,
				),
				Log_Level_Enum::ERROR
			);
			wp_die( 'Order not found' );
		}

		$input = self::validate_input( $order );

		$result = self::handle_callback( $order, $input );

		echo '<pre>';
		print_r( $result ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
		echo '</pre>';

		Swish_Settings::log( 'swish_callback_response_LOCAL_DEV done' );
		wp_die( 'Swish local dev callback finished' );
	}

	/**
	 * Validates the order and returns either a WC_Order or a WC_Order_Refund object.
	 *
	 * @return WC_Order|WC_Order_Refund The validated order.
	 */
	private static function validate_order(): WC_Order|WC_Order_Refund {
		$order_id = (int) $_REQUEST['order_id'];
		$order    = wc_get_order( $order_id );

		if ( ! $order ) {
			Swish_Settings::log(
				'Order not found for Swish callback',
				array(
					'order_id' => $order_id,
				),
				Log_Level_Enum::ERROR
			);

			wp_die();
		}

		if ( ! Swish_Client::is_callback_valid( $order_id ) ) {
			wp_die();
		}

		return $order;
	}

	/**
	 * Validates the input for the given order and returns a stdClass object.
	 *
	 * @param WC_Order|WC_Order_Refund $order The order to validate.
	 *
	 * @return stdClass The validated input.
	 */
	private static function validate_input( WC_Order|WC_Order_Refund $order ): stdClass {
		$input = new stdClass();

		$order_id = $order->get_id();

		$input->event_type = $_REQUEST['event'] ?? null;

		$valid_event_types = array( 'payment', 'refund' );

		if ( ! in_array( $input->event_type, $valid_event_types, true ) ) {
			Swish_Settings::log(
				'Invalid Swish event type',
				array(
					'event_type' => $input->event_type,
					'order_id'   => $order_id,
				),
				Log_Level_Enum::ERROR
			);
			wp_die();
		}

		$request_body = file_get_contents( 'php://input' );
		$json         = json_decode( $request_body );

		if ( ! self::$is_local_dev ) {
			if ( empty( $request_body ) ) {
				Swish_Settings::log(
					'Request body missing for Swish order',
					array(
						'order_id' => $order_id,
					),
					Log_Level_Enum::ERROR
				);

				wp_die();
			}
		}
		Swish_Settings::log(
			'Swish request body details',
			array(
				'order_id' => $order_id,
				'body'     => $request_body,
			)
		);

		$allowed_input_properties = array(
			'id'               => true,
			'status'           => true,
			'errorCode'        => 'error_code',
			'paymentReference' => 'payment_reference',
		);

		foreach ( $allowed_input_properties as $key => $value ) {
			if ( ! empty( $json->$key ) ) {
				if ( is_string( $value ) ) {
					$input->$value = $json->$key;
				} else {
					$input->$key = $json->$key;
				}
			}
		}

		if ( self::$is_local_dev ) {
			$input = self::handle_local_dev_input( $input );
		}

		if ( empty( $input->status ) ) {
			Swish_Settings::log(
				'Status missing for Swish order',
				array(
					'order_id' => $order_id,
				),
				Log_Level_Enum::ERROR
			);

			wp_die();
		}

		$allowed_status = array( 'CANCELLED', 'DECLINED', 'ERROR', 'DEBITED', 'PAID' );

		if ( ! in_array( $input->status, $allowed_status, true ) ) {
			Swish_Settings::log(
				'Invalid status for Swish order',
				array(
					'order_id' => $order_id,
					'status'   => $input->status,
				),
				Log_Level_Enum::ERROR
			);

			wp_die();
		}

		if ( ! empty( $input->error_code ) ) {
			$input->error_details = Swish_Errors::get_message_by_code( $input->error_code );
		}

		return $input;
	}

	/**
	 * Handles and validates the local development input object and returns a stdClass object.
	 *
	 * @param stdClass $input The input object that needs to be handled.
	 *
	 * @return stdClass The handled and validated input object.
	 */
	private static function handle_local_dev_input( stdClass $input ): stdClass {
		if ( ! isset( $input->status ) ) {
			$input->status = 'PAID';
		}

		if ( ! isset( $input->id ) ) {
			$input->id = 'DEBUG 42';
		}

		if ( ! isset( $input->payment_reference ) ) {
			$input->payment_reference = 'DEBUG reference 42';
		}

		return $input;
	}

	/**
	 * Handles the callback for a validated order and updates the order status accordingly.
	 *
	 * @param WC_Order|WC_Order_Refund $order The validated order.
	 * @param stdClass                 $input The callback input data.
	 *
	 * @return array The response indicating the success of the callback handling.
	 */
	private static function handle_callback( WC_Order|WC_Order_Refund $order, stdClass $input ): array {
		$order_id = $order->get_id();
		switch ( $input->status ) {
			case 'DECLINED':
			case 'CANCELLED':
				if (
					$order->get_payment_method() === Swish_Pay::get_id() &&
					in_array( $order->get_status(), array( 'pending', 'cancelled' ), true )
				) {
					$order->add_order_note(
						__(
							'Northmill - Payment declined by user',
							'flo-checkout-plugin'
						)
					);
					$order->update_status( 'wc-cancelled' );
					Swish_Settings::log( 'Swish payment declined by user' );
				} else {
					$order->add_order_note(
						__(
							'Northmill Swish - Payment declined by user but order status cannot be updated (not pending swish payment)',
							'flo-checkout-plugin'
						)
					);
					Swish_Settings::log(
						'Swish payment declined by user, failed to set DECLINED status on order',
						array( 'order_id' => $order_id ),
						Log_Level_Enum::ERROR
					);
				}
				break;
			case 'ERROR':
				Swish_Settings::log(
					'Swish callback error',
					array(
						'order_id' => $order_id,
						'data'     => $input,
					),
					Log_Level_Enum::ERROR
				);

				if (
					$order->get_payment_method() === Swish_Pay::get_id() &&
					in_array( $order->get_status(), array( 'pending', 'cancelled' ), true )
				) {
					$order->add_order_note( $input->error_code . ' - ' . $input->error_details );
					$order->update_status( 'wc-failed' );
				} else {
					$order->add_order_note(
						__(
							'Northmill Swish - Payment error but order status cannot be updated (not pending swish payment)',
							'flo-checkout-plugin'
						)
					);
					Swish_Settings::log(
						'Payment error, and failed to update order status',
						array(
							'order_id' => $order_id,
							'data'     => $input,
						),
						Log_Level_Enum::ERROR
					);
				}
				break;
			case 'DEBITED':
				update_post_meta( $order_id, '_swish_refund_debited', true );
				$order->update_status( 'wc-pending' );
				$order->add_order_note(
					sprintf(
					// translators: %1$s is "Transaction", %2$s is the transaction ID.
						__(
							'Northmill - Merchant account debited - %1$s ID: %2$s',
							'flo-checkout-plugin'
						),
						'Transaction',
						$input->id
					)
				);
				Swish_Settings::log(
					'Got DEBITED from Swish',
					array(
						'order_id' => $order_id,
					)
				);
				break;
			case 'PAID':
				if ( 'refund' !== $input->event_type ) {
					$order->add_order_note(
						sprintf(
						// translators: %1$s is "Transaction", %2$s is the transaction ID.
							__(
								'Northmill - Payment confirmed - %1$s ID: %2$s',
								'flo-checkout-plugin'
							),
							'Transaction',
							$input->id
						)
					);
					try {
						$order->set_transaction_id( $input->payment_reference );
					} catch ( WC_Data_Exception $e ) {
						Swish_Settings::log(
							'Failed to set Swish transaction ID on order',
							array(
								'order_id'  => $order_id,
								'data'      => $input,
								'exception' => $e,
							),
							Log_Level_Enum::ERROR
						);
					}
					$order->payment_complete();
					Swish_Settings::log(
						'Got PAID from Swish',
						array( 'order_id' => $order_id )
					);

					// Refund.
				} elseif ( get_post_meta( $order_id, '_swish_refund_debited', true ) ) {
					$order->update_status( 'wc-refunded' );

					Swish_Settings::log(
						'Successfully credited to customer account',
						array( 'order_id' => $order_id )
					);

					$order->add_order_note(
						sprintf(
						// translators: %1$s is "Transaction", %2$s is the transaction ID.
							__(
								'Northmill - Successfully credited to customer account - %1$s ID: %2$s',
								'flo-checkout-plugin'
							),
							'Transaction',
							$input->id
						)
					);
				}
				break;
		}
		$order->save();

		return array(
			'success'     => true,
			'successCode' => '200',
		);
	}
}
