<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

class WC_Verifone_Woocommerce_Subscriptions {
	function __construct() {
		add_filter( 'woocommerce_verifone_available_payment_methods', [ $this, 'filter_payment_methods' ], 900 );
		add_filter( 'wc_verifone_settings' , [ $this, 'add_subscription_settings' ] );
		add_action( 'woocommerce_scheduled_subscription_payment_' . WC_VERIFONE_GATEWAY_ID, [ $this, 'process_scheduled_subscription_payment' ], 10, 2 );
		add_action( 'woocommerce_subscription_before_actions', [ $this, 'display_saved_payment_method_details' ] );
	}

	## GLOBALLY AVAILABLE HELPER FUNCTIONS

	/**
	 * Wrapper to check if the cart contains a subscription.
	 *
	 * @return bool True if the cart contains a subscription, false otherwise.
	 */
	public static function cart_contains_subscription() {
		if ( ! class_exists( 'WC_Subscriptions_Cart' ) ) {
			return false;
		}

		if ( ! method_exists( 'WC_Subscriptions_Cart', 'cart_contains_subscription' ) ) {
			return false;
		}

		$return = WC_Subscriptions_Cart::cart_contains_subscription();
		$return = ! $return ? wcs_cart_contains_renewal() : $return;

		/**
		 * On checkout pay page, check if the order being paid for is a subscription.
		 * This can happen if the payment method is invalid and user is trying to pay again,
		 * user is changing payment method for a subscription, or user is paying for a failed renewal order.
		 *
		 * For some reason WC_Subscriptions_Cart::cart_contains_subscription() does not return true in this case,
		 * so we need to check if the order is actually an instance of WC_Subscription (which extends WC_Order).
		 */
		if ( ! $return && is_checkout_pay_page() ) {
			$order_id = absint( get_query_var( 'order-pay' ) );
			$order = wc_get_order( $order_id );
			if ( is_a( $order, 'WC_Subscription' ) ) {
				$return = true;
			}
		}

		return $return;
	}

	/**
	 * Wrapper to check if an order contains a subscription.
	 *
	 * @param mixed $order WC_Order object or order ID.
	 * @return bool True if the order contains a subscription, false otherwise.
	 */
	public static function order_contains_subscription( $order ) {
		if ( ! function_exists( 'wcs_order_contains_subscription' ) ) {
			return false;
		}

		if ( is_numeric( $order ) ) {
			$order = wc_get_order( $order );
		}

		$return = wcs_order_contains_subscription( $order );
		$return = ! $return ? wcs_order_contains_renewal( $order ) : $return;

		/**
		 * For some reason wcs_order_contains_subscription() does not return true if the order itself is
		 * a subscription (like when changing payment method), so we need to check if the order is
		 * actually an instance of WC_Subscription (which extends WC_Order).
		 */
		if ( ! $return && is_a( $order, 'WC_Subscription' ) ) {
			$return = true;
		}

		return $return;
	}

	/**
	 * Get or generate a unique external ID for the customer based on user ID or email.
	 * This is used to identify customers in Verifone system for recurring payments.
	 *
	 * @param mixed $order WC_Order object or order ID.
	 * @return string External ID for the customer.
	 */
	public static function get_customer_external_id( $order ) {
		if ( is_numeric( $order ) ) {
			$order = wc_get_order( $order );
		}

		$external_id = $order->get_user_id() ? $order->get_user_id() : $order->get_billing_email();
		$external_id = $order->get_user_id() . '|' . self::crypt_string( $external_id );

		return $external_id;
	}

	## HOOKED FUNCTIONS

	/**
	 * Collect recurring payment data for orders that contain subscriptions.
	 *
	 * Hooked into WC_Verifone_Data::collectData via compatibility class.
	 *
	 * @param array $data
	 * @param WC_Order $order
	 * @return array
	 */
	public static function collect_recurring_payment_data( $data, $order ) {
		if ( ! function_exists( 'wcs_get_subscriptions_for_order' ) ) {
			return $data;
		}

		$subscriptions = wcs_get_subscriptions_for_order( $order );

		/**
		 * If no subscriptions found, check if the order itself is a subscription and use it.
		 * This can happen when changing payment method for a subscription.
		 */
		if ( empty( $subscriptions ) && is_a( $order, 'WC_Subscription' ) ) {
			$subscriptions = [ $order ];
		}

		/**
		 * If no subscriptions found, check also if the order is a renewal order for a subscription.
		 * Because yet again, earlier checks do not catch renewals.
		 */
		if ( empty( $subscriptions ) && wcs_order_contains_renewal( $order ) ) {
			$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
		}

		if ( empty( $subscriptions ) ) {
			return $data;
		}

		$subscriptions_name = [];
		foreach ( $subscriptions as $subscription ) {
			foreach ( $subscription->get_items() as $item ) {
				$subscriptions_name[] = $item->get_name();
			}
		}

		$data['recurring'] = [
			'subscription_name' => ! empty( $subscriptions_name ) ? implode( ', ', $subscriptions_name ) : 'Subscription #' . $order->get_id(),
			'subscription_code' => self::get_subscription_code( $order ),
		];

		// If customer external_id is not set, force it to be used for data integrity
		if ( empty( $data['customer']['external_id'] ) ) {
			$data['customer']['external_id'] = self::get_customer_external_id( $order );
		}

		// If order total is zero (starts with free trial), set save_payment_method to save only
		if ( $order->get_total() <= 0 ) {
			$data['save_payment_method'] = '2'; // 2 = PaymentInfoImpl::SAVE_METHOD_SAVE_ONLY
		}

		/**
		 * If changing payment method for a subscription, set save_payment_method to save only.
		 *
		 * ..and custom flag to indicate changing payment method for recurring payments so later
		 * logic (like redirect urls) can be adjusted accordingly.
		 */
		if ( isset( $_GET['change_payment_method'] ) && sanitize_text_field( wp_unslash( $_GET['change_payment_method'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - call verified earlier in the process
			$data['save_payment_method'] = '2'; // 2 = PaymentInfoImpl::SAVE_METHOD_SAVE_ONLY
			$data['change_recurring_payment_method'] = true;
		}

		return $data;
	}

	/**
	 * Filter out payment methods that are not able to handle subscription recurring payments.
	 *
	 * Hooked into 'woocommerce_verifone_available_payment_methods' filter.
	 *
	 * @param array $methods Available payment methods.
	 * @return array Filtered payment methods.
	 */
	public function filter_payment_methods( $methods ) {
		if ( ! $this->cart_contains_subscription() ) {
			return $methods;
		}

		// Filter out payment methods that are not type 'card'
		foreach ( $methods as $key => $method ) {
			if ( isset( $method['type'] ) && $method['type'] !== WC_Verifone_PaymentMethods::TYPE_CARD ) {
				unset( $methods[ $key ] );
			}
		}

		return $methods;
	}

	/**
	 * Add subscription related settings to Verifone settings.
	 *
	 * Hooked into 'wc_verifone_settings' filter.
	 *
	 * @param array $settings Current Verifone settings.
	 * @return array Updated Verifone settings.
	 */
	public function add_subscription_settings( $settings ) {
		if ( ! class_exists( 'WC_Subscriptions' ) ) {
			return $settings;
		}

		$subscription_settings = [
			'remember_cc_subscription_info' => [
				'title'   		=> __( 'Subscription remember payment method info', 'verifone-bluecommerce-for-woocommerce' ),
				'type'    		=> 'textarea',
				'css'			=> 'max-width:400px;',
				'description'	=> __( 'Note in checkout after method selection to inform client about Verifone storing their card information for further subscription payments.', 'verifone-bluecommerce-for-woocommerce' )
									. ' ' . __( 'If left empty, default message will be used.', 'verifone-bluecommerce-for-woocommerce' ),
			],
			'subscription_free_trial_reservation_info' => [
				'title'   		=> __( 'Subscription free trial reservation info', 'verifone-bluecommerce-for-woocommerce' ),
				'type'    		=> 'textarea',
				'css'			=> 'max-width:400px;',
				'description'	=> __( 'Note in checkout for subscriptions that start with a free trial to inform client about Verifone doing 30€ reservation on their card to its validity.', 'verifone-bluecommerce-for-woocommerce' )
									. ' ' . __( 'If left empty, default message will be used.', 'verifone-bluecommerce-for-woocommerce' ),
			],
		];

		// Add before show_advanced_settings key
		$insert_position = array_search( 'show_advanced_settings', array_keys( $settings ), true );
		if ( false !== $insert_position ) {
			$settings = array_merge(
				array_slice( $settings, 0, $insert_position ),
				$subscription_settings,
				array_slice( $settings, $insert_position )
			);
		} else {
			$settings = array_merge( $settings, $subscription_settings );
		}

		return $settings;
	}

	/**
	 * Process scheduled subscription payment.
	 *
	 * Hooked into 'woocommerce_scheduled_subscription_payment_' . WC_VERIFONE_GATEWAY_ID action.
	 *
	 * @param float $amount Amount to charge.
	 * @param WC_Order $renewal_order Order object.
	 * @return void
	 */
	public function process_scheduled_subscription_payment( $amount, $renewal_order ) {
		$renewal_order->add_order_note( __( 'Processing subscription scheduled payment via Verifone.', 'verifone-bluecommerce-for-woocommerce' ) );

		// Get the subscription code
		$subscription_code = $renewal_order->get_meta( 'verifone_subscription_code', true );
		if ( empty( $subscription_code ) ) {
			$renewal_order->add_order_note( __( 'Subscription scheduled payment failed: No subscription code found.', 'verifone-bluecommerce-for-woocommerce' ) );
			$renewal_order->update_status( 'failed', __( 'Subscription scheduled payment failed.', 'verifone-bluecommerce-for-woocommerce' ) );
			WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $renewal_order->get_parent_id() );
			return false;
		}

		// Process the payment
		$renewal_payment = new WC_Verifone_Recurring_Payment( $renewal_order, $amount, $subscription_code );
		$renewal_result = $renewal_payment->process_renewal_payment();
		if ( is_wp_error( $renewal_result ) ) {
			$renewal_order->add_order_note( sprintf( __( 'Subscription scheduled payment failed: %s', 'verifone-bluecommerce-for-woocommerce' ), $renewal_result->get_error_message() ) );
			$renewal_order->update_status( 'failed', __( 'Subscription scheduled payment failed.', 'verifone-bluecommerce-for-woocommerce' ) );
			WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $renewal_order->get_parent_id() );
			return false;
		}

		WC_Verifone_Payment::doneOrder( $renewal_order, $renewal_result->getBody() );
		return true;
	}

	/**
	 * Display saved payment method details after subscription payment method.
	 *
	 * @param WC_Subscription $subscription Subscription object.
	 * @return void
	 */
	public function display_saved_payment_method_details( $subscription ) {
		// Check that subscription payment method is ours
		if ( $subscription->get_payment_method() !== WC_VERIFONE_GATEWAY_ID ) {
			return;
		}

		// Check that subscription is for the same user as current user
		$current_user_id = get_current_user_id();
		if ( $subscription->get_user_id() !== $current_user_id ) {
			return;
		}

		// For some reason autoloaded classes might not be available here, so do load them
		if ( ! class_exists( '\Verifone\Core' ) ) {
			require_once WC_VERIFONE_PLUGIN_PATH . '/vendor/autoload.php';
		}

		// Build renewal payment object for getting saved payment method info
		$subscription_code = $subscription->get_meta( 'verifone_subscription_code', true );
		$renewal_payment = new WC_Verifone_Recurring_Payment( $subscription, null, $subscription_code );

		// Get saved payment method info
		$payment_method = $renewal_payment->get_saved_payment_method_info();
		if ( ! $payment_method ) {
			return;
		}

		ob_start(); ?>
		<tr>
			<td style="padding-top:0;"></td>
			<td style="padding-top:0;">
				<?php echo esc_html( $payment_method['method']['display_name'] ); ?>
				<?php echo esc_html( $payment_method['title'] ); ?>
				<?php if ( ! empty( $payment_method['validity'] ) ) : ?>
					<br/><small><?php printf( esc_html__( 'Expires: %s', 'verifone-bluecommerce-for-woocommerce' ), esc_html( $payment_method['validity'] ) ); ?></small>
				<?php endif; ?>
			</td>
		</tr>
		<?php echo ob_get_clean(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - Output is escaped earlier
	}

	## PRIVATE HELPER FUNCTIONS

	/**
	 * Get generated code for the subscription, or generate a new one if not found.
	 * This is used to identify subscriptions and link renewal payments to the original subscription
	 * in Verifone system.
	 *
	 * @param WC_Order $order Order object.
	 * @return string Subscription code.
	 */
	private static function get_subscription_code( $order ) {
		$subscription_code = $order->get_meta( 'verifone_subscription_code', true );
		if ( ! empty( $subscription_code ) ) {
			return $subscription_code;
		}

		$subscription_code = self::generate_subscription_code( $order );

		$order->add_meta_data( 'verifone_subscription_code', $subscription_code, true );
		$order->save_meta_data();

		// Save to subscription meta for later use
		$subscriptions = wcs_get_subscriptions_for_order( $order );
		if ( ! empty( $subscriptions ) ) {
			foreach ( $subscriptions as $subscription ) {
				$subscription->add_meta_data( 'verifone_subscription_code', $subscription_code, true );
				$subscription->save_meta_data();
			}
		}

		return $subscription_code;
	}

	/**
	 * Generate a subscription code based on order ID and subscription product IDs.
	 *
	 * @param WC_Order $order Order object.
	 * @return string Subscription code.
	 */
	private static function generate_subscription_code( $order ) {
		$code = $order->get_id(); // Base

		$subscriptions = wcs_get_subscriptions_for_order( $order );
		if ( empty( $subscriptions ) ) {
			return $code;
		}

		$subscriptions_product_ids = [];
		foreach ( $subscriptions as $subscription ) {
			foreach ( $subscription->get_items() as $item ) {
				$subscriptions_product_ids[] = $item->get_id();
			}
		}

		return $code . '-' . self::crypt_string( implode( '|', $subscriptions_product_ids ) );
	}

	/**
	 * Generate a simple hash string based on site URL and given string.
	 *
	 * @param string $str Input string.
	 * @return string Hash string.
	 */
	private static function crypt_string( $str ) {
		$parts = [
			get_home_url(),
			$str,
		];

		return crc32( implode( '|', array_filter( $parts ) ) );
	}
}

new WC_Verifone_Woocommerce_Subscriptions();
