<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase, WordPress.Files.FileName.InvalidClassFileName
/**
 * This class represents the WooPayments payment integration.
 *
 * @package Autoship
 * @since 2.13.0
 */

namespace Autoship\Domain\PaymentIntegrations;

use Autoship\Domain\AbstractPaymentGateway;
use Autoship\Domain\PaymentMethodType;
use Autoship\Modules\Payments\PaymentUtilities;
use Autoship\Services\Logging\Logger;
use Exception;
use QPilotPaymentData;
use WC_Order;
use WC_Payment_Token;
use WC_Payment_Tokens;

/**
 * Represents the WooPayments payment integration.
 *
 * @package Autoship
 * @since 2.13.0
 */
class WooPaymentsPaymentIntegration extends AbstractPaymentGateway {

	/**
	 * The allowed payment gateways for this integration.
	 *
	 * @var array
	 */
	private static array $allowed = array(
		'woocommerce_payments',
	);

	/**
	 * Initializing.
	 *
	 * @return void
	 */
	public function initialize(): void {
		if ( Logger::is_tracing_enabled() ) {
			Logger::log( 'WooPayments Payment Integration', 'Initializing WooPayments payment integration.' );
		}
	}

	/**
	 * Builds a WooPayments payment integration.
	 *
	 * @param string $gateway_id The gateway ID.
	 * @param array  $settings The gateway settings.
	 * @return WooPaymentsPaymentIntegration
	 * @throws Exception When the gateway ID is not supported.
	 */
	public static function build( string $gateway_id, array $settings ): WooPaymentsPaymentIntegration {
		if ( ! in_array( $gateway_id, self::$allowed, true ) ) {
			throw new Exception( 'The current payment integration is not supported by this gateway.' );
		}

		$integration = new self();
		$integration->set_method_id( $gateway_id );
		$integration->set_method_type( PaymentMethodType::WOO_PAYMENTS );
		$integration->set_method_name( $settings['title'] ?? '' );
		$integration->set_authorize_only( false );

		$is_test_mode = 'yes' === ( $settings['test_mode'] ?? 'no' );
		$integration->set_test_mode( $is_test_mode );

		return $integration;
	}

	/**
	 * Gets the value indicating if the payment integration is valid or not.
	 *
	 * @return bool
	 */
	public function is_valid(): bool {
		if ( PaymentMethodType::WOO_PAYMENTS !== $this->get_method_type() ) {
			return false;
		}

		if ( ! in_array( $this->get_method_id(), self::$allowed, true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get order payment data for QPilot.
	 *
	 * @param int      $order_id The order ID.
	 * @param WC_Order $order The order object.
	 *
	 * @return ?QPilotPaymentData The payment data.
	 */
	public function get_order_payment_data( int $order_id, WC_Order $order ): ?QPilotPaymentData {
		// WCPay stores the Stripe customer ID on the WP user (via
		// mode-specific user options like `_wcpay_customer_id_live` /
		// `_wcpay_customer_id_test`), NOT on the order under the
		// `_stripe_customer_id` key that the standard Stripe plugin uses.
		$customer_id = $this->resolve_order_customer_id( $order );
		if ( empty( $customer_id ) ) {
			Logger::error(
				'WooPayments Order Payment Data',
				sprintf( 'Order #%d: no WooPayments customer_id resolvable (user=%d, gateway=%s). SO will be created without WooPayments assignment.', $order_id, (int) $order->get_customer_id(), $order->get_payment_method() )
			);
			return null;
		}

		// WCPay reliably writes `_payment_method_id` on the order in both
		// non-3DS and 3DS-authenticated checkouts. The corresponding
		// WC_Payment_Token row, however, is only persisted later in WCPay's
		// 3DS return-URL handler — by which time our priority-11
		// `woocommerce_payment_complete` hook has already fired. So the
		// pm_... ID is the canonical source for QPilot's payment data; the
		// WC_Payment_Token is only consulted as a best-effort source of
		// display metadata (description / last4 / expiration).
		$payment_method_id = (string) $order->get_meta( '_payment_method_id' );
		if ( '' === $payment_method_id ) {
			Logger::error(
				'WooPayments Order Payment Data',
				sprintf( 'Order #%d: no _payment_method_id meta on order (user=%d, gateway=%s). SO will be created without WooPayments assignment.', $order_id, (int) $order->get_customer_id(), $order->get_payment_method() )
			);
			return null;
		}

		$payment_data                       = new QPilotPaymentData();
		$payment_data->type                 = 'WooPayments';
		$payment_data->gateway_payment_id   = $payment_method_id;
		$payment_data->gateway_customer_id  = $customer_id;
		$payment_data->gateway_payment_type = 7;

		$token = $this->resolve_order_payment_token( $order );
		if ( null !== $token ) {
			$payment_data->description = $token->get_display_name();
			if ( method_exists( $token, 'get_last4' ) && method_exists( $token, 'get_expiry_month' ) && method_exists( $token, 'get_expiry_year' ) ) {
				$payment_data->last_four  = $token->get_last4();
				$payment_data->expiration = $token->get_expiry_month() . substr( $token->get_expiry_year(), -2 );
			}
		} else {
			// 3DS-initial-checkout fallthrough: pm is on the order but the
			// WC token row hasn't been written yet. QPilot's API validates
			// description / lastFourDigits / expiration as non-null strings
			// and 400s the SO creation otherwise, so we must populate them.
			// Prefer card details WCPay wrote to order meta (some versions
			// do); fall back to placeholder values that match QPilot's
			// expected formats. The first successful renewal response will
			// carry the real card details from Stripe and overwrite these.
			$card_details = $this->resolve_card_details_from_order_meta( $order );

			$payment_data->description = $card_details['description'];
			$payment_data->last_four   = $card_details['last_four'];
			$payment_data->expiration  = $card_details['expiration'];

			Logger::trace(
				'WooPayments Order Payment Data',
				sprintf( 'Order #%d: WC_Payment_Token not yet persisted; populated display fields from order meta (real=%s, pm=%s).', $order_id, $card_details['from_meta'] ? 'yes' : 'no', $payment_method_id )
			);
		}

		return $payment_data;
	}

	/**
	 * Resolve display-only card details (description / last_four / expiration)
	 * from WCPay-written order meta. Used as a fallback when the
	 * WC_Payment_Token hasn't been persisted yet at the moment our
	 * `woocommerce_payment_complete` hook fires (typical for the 3DS-initial
	 * checkout flow).
	 *
	 * Returns placeholder values when WCPay hasn't written card details to
	 * the order; QPilot's API requires non-null strings here, and the
	 * placeholders are accurate enough to pass validation. Real card details
	 * propagate back on the first successful renewal response from Stripe.
	 *
	 * @param WC_Order $order The order.
	 *
	 * @return array{description: string, last_four: string, expiration: string, from_meta: bool}
	 */
	private function resolve_card_details_from_order_meta( WC_Order $order ): array {
		$brand     = trim( (string) $order->get_meta( '_card_brand' ) );
		$last4     = trim( (string) $order->get_meta( '_card_last4' ) );
		$exp_month = trim( (string) $order->get_meta( '_card_exp_month' ) );
		$exp_year  = trim( (string) $order->get_meta( '_card_exp_year' ) );

		if ( '' !== $last4 && '' !== $exp_month && '' !== $exp_year ) {
			$brand_label = '' !== $brand ? ucfirst( $brand ) : 'Card';
			return array(
				'description' => sprintf( '%s ending in %s', $brand_label, $last4 ),
				'last_four'   => $last4,
				'expiration'  => sprintf( '%02d%02d', (int) $exp_month, ( (int) $exp_year ) % 100 ),
				'from_meta'   => true,
			);
		}

		return array(
			'description' => 'WooPayments Card',
			'last_four'   => '0000',
			'expiration'  => '1299',
			'from_meta'   => false,
		);
	}

	/**
	 * Resolve the WC_Payment_Token used for this WooPayments order.
	 *
	 * Tries multiple paths because WCPay's write of `_payment_method_id` on
	 * the order can lag for 3DS-authenticated checkouts (the customer is
	 * redirected to Stripe's hosted page; meta is only finalised when the
	 * customer returns and WCPay processes the post-3DS callback). The
	 * fallbacks anchor on WC's own order-token tracking and the customer's
	 * recent WooPayments tokens.
	 *
	 * @param WC_Order $order The order.
	 *
	 * @return WC_Payment_Token|null The token, or null when none can be resolved.
	 */
	private function resolve_order_payment_token( WC_Order $order ): ?WC_Payment_Token {
		// Path 1: `_payment_method_id` order meta. Primary path for non-3DS
		// checkouts and most 3DS checkouts — WCPay writes the Stripe `pm_...`
		// ID here via Order_Service::set_payment_method_id_for_order().
		$token_id = $order->get_meta( '_payment_method_id' );
		if ( ! empty( $token_id ) ) {
			$token = PaymentUtilities::get_related_tokenized_id( $token_id );
			if ( $token instanceof WC_Payment_Token ) {
				return $token;
			}
		}

		// Path 2: WC's own order-token tracking. When the customer pays with
		// a tokenized method (saved or just-saved card), WC links the
		// `WC_Payment_Token` to the order via `add_payment_token()`. This is
		// populated independently of the `_payment_method_id` meta and
		// survives the 3DS round-trip.
		$tokens_on_order = $order->get_payment_tokens();
		if ( ! empty( $tokens_on_order ) ) {
			foreach ( $tokens_on_order as $tid ) {
				$candidate = WC_Payment_Tokens::get( $tid );
				if ( $candidate instanceof WC_Payment_Token && 'woocommerce_payments' === $candidate->get_gateway_id() ) {
					return $candidate;
				}
			}
		}

		// Path 3: last-resort customer-level lookup — the most recently
		// created WooPayments token for the order's purchaser. Only safe
		// when the customer has a single WooPayments card on file (the one
		// they just used). Logged at trace so we can spot if this becomes
		// the common path, which would indicate the earlier paths regressed.
		$user_id = (int) $order->get_customer_id();
		if ( $user_id > 0 ) {
			$customer_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id, 'woocommerce_payments' );
			if ( ! empty( $customer_tokens ) ) {
				$token = reset( $customer_tokens );
				if ( $token instanceof WC_Payment_Token ) {
					Logger::trace(
						'WooPayments Order Payment Data',
						sprintf( 'Order #%d had no _payment_method_id meta or order-tokens; falling back to most recent WooPayments token for user %d.', $order->get_id(), $user_id )
					);
					return $token;
				}
			}
		}

		return null;
	}

	/**
	 * Resolve the Stripe customer ID (cus_...) for the order's purchaser.
	 *
	 * Prefers WCPay's user-option storage via `get_wcpay_customer_id()`. For
	 * guest checkouts (no WP user), falls back to whatever the WCPay
	 * order-level meta keys carry — different WCPay versions and the
	 * Blocks/Classic checkout flows have used slightly different names.
	 *
	 * @param WC_Order $order The order.
	 *
	 * @return string|null The customer ID, or null when none can be resolved.
	 */
	private function resolve_order_customer_id( WC_Order $order ): ?string {
		$user_id = (int) $order->get_customer_id();
		if ( $user_id > 0 ) {
			$from_user = $this->get_wcpay_customer_id( $user_id );
			if ( null !== $from_user ) {
				return $from_user;
			}
		}

		foreach ( array( '_wcpay_customer_id_live', '_wcpay_customer_id_test', '_wcpay_customer_id' ) as $key ) {
			$value = $order->get_meta( $key );
			if ( is_string( $value ) && '' !== $value ) {
				return $value;
			}
		}

		return null;
	}

	/**
	 * Add payment method data for QPilot.
	 *
	 * @param array            $payment_method_data The payment method data.
	 * @param string           $type The payment method type.
	 * @param WC_Payment_Token $token The payment token.
	 * @return array The modified payment method data.
	 */
	public function add_payment_method( array $payment_method_data, string $type, WC_Payment_Token $token ): array {
		$customer_id = $this->get_wcpay_customer_id( $token->get_user_id() );

		if ( null !== $customer_id ) {
			$payment_method_data['gatewayCustomerId'] = $customer_id;
		}

		return $payment_method_data;
	}

	/**
	 * Delete payment method validation.
	 *
	 * @param bool             $valid Current validation status.
	 * @param string           $type The payment method type.
	 * @param WC_Payment_Token $token The payment token.
	 * @param object           $method The QPilot payment method.
	 * @return bool Whether the deletion is valid.
	 */
	public function delete_payment_method( bool $valid, string $type, WC_Payment_Token $token, object $method ): bool {
		if ( 'WooPayments' === $type ) {
			$customer_id = $this->get_wcpay_customer_id( $token->get_user_id() );

			return ( $method->gatewayCustomerId === $customer_id && $method->gatewayPaymentId === $token->get_token() ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}

		return $valid;
	}

	/**
	 * Resolve the WooPayments (Stripe) customer ID for a WP user.
	 *
	 * WCPay stores the customer ID under mode-specific user-option keys
	 * (`_wcpay_customer_id_test` / `_wcpay_customer_id_live`) since the
	 * test/live mode split. Prefer the canonical service accessor when
	 * available, then fall back to raw meta with a legacy-key fallback.
	 *
	 * @param int $user_id The WP user ID.
	 *
	 * @return string|null The Stripe customer ID (cus_...), or null when unknown.
	 */
	private function get_wcpay_customer_id( int $user_id ): ?string {
		if ( class_exists( '\WC_Payments' ) && method_exists( '\WC_Payments', 'get_customer_service' ) ) {
			try {
				$service = \WC_Payments::get_customer_service();
				if ( is_object( $service ) && method_exists( $service, 'get_customer_id_by_user_id' ) ) {
					$id = $service->get_customer_id_by_user_id( $user_id );
					if ( is_string( $id ) && '' !== $id ) {
						return $id;
					}
				}
			} catch ( \Throwable $e ) {
				Logger::trace( 'WooPayments Customer Lookup', sprintf( 'get_customer_service() threw, falling back to raw meta: %s', $e->getMessage() ) );
			}
		}

		$is_test_mode = false;
		if ( class_exists( '\WC_Payments' ) && method_exists( '\WC_Payments', 'mode' ) ) {
			try {
				$mode = \WC_Payments::mode();
				if ( is_object( $mode ) && method_exists( $mode, 'is_test' ) ) {
					$is_test_mode = (bool) $mode->is_test();
				}
			} catch ( \Throwable $e ) {
				Logger::trace( 'WooPayments Customer Lookup', sprintf( 'WC_Payments::mode() threw, defaulting to live-mode key: %s', $e->getMessage() ) );
			}
		}

		$candidates = $is_test_mode
			? array( '_wcpay_customer_id_test', '_wcpay_customer_id_live', '_wcpay_customer_id' )
			: array( '_wcpay_customer_id_live', '_wcpay_customer_id_test', '_wcpay_customer_id' );

		foreach ( $candidates as $key ) {
			$value = get_user_option( $key, $user_id );
			if ( is_string( $value ) && '' !== $value ) {
				return $value;
			}
		}

		return null;
	}
}
