<?php
/**
 * UPE Payment Method class
 *
 * Handles general functionality for UPE payment methods
 *
 * @package WCPay\Payment_Methods
 */

namespace WCPay\Payment_Methods;

use WC_Payments_Utils;
use WP_User;
use WC_Payments_Token_Service;
use WC_Payment_Token_CC;
use WC_Payment_Token_WCPay_SEPA;
use WC_Payments_Subscriptions_Utilities;
use WCPay\PaymentMethods\Configs\Utils\PaymentMethodUtils;

/**
 * Extendable class for payment methods.
 *
 * @template T of \WCPay\PaymentMethods\Configs\Interfaces\PaymentMethodDefinitionInterface
 */
class UPE_Payment_Method {

	use WC_Payments_Subscriptions_Utilities;

	/**
	 * Payment method definition.
	 *
	 * @var class-string<T>|null
	 */
	protected $definition;

	/**
	 * Stripe key name
	 *
	 * @var string
	 */
	protected $stripe_id;

	/**
	 * Display title
	 *
	 * @var string
	 */
	protected $title;

	/**
	 * Can payment method be saved or reused?
	 *
	 * @var bool
	 */
	protected $is_reusable;

	/**
	 * Instance of WC Payments Token Service to save payment method
	 *
	 * @var WC_Payments_Token_Service
	 */
	protected $token_service;

	/**
	 * Supported presentment currencies for which charges for a payment method can be processed
	 * Empty if all currencies are supported
	 *
	 * @var string[]
	 */
	protected $currencies;

	/**
	 * Should payment method be restricted to only domestic payments.
	 * E.g. only to Stripe's connected account currency.
	 *
	 * @var boolean
	 */
	protected $accept_only_domestic_payment = false;

	/**
	 * Represent payment total limitations for the payment method (per-currency).
	 *
	 * @var array<string,array<string,array<string,int>>>
	 */
	protected $limits_per_currency = [];

	/**
	 * Payment method icon URL
	 *
	 * @var string
	 */
	protected $icon_url;

	/**
	 * Payment method icon URL for dark themes (optional)
	 *
	 * @var string
	 */
	protected $dark_icon_url;

	/**
	 * Is the payment method a BNPL (Buy Now Pay Later) method?
	 *
	 * @var boolean
	 */
	protected $is_bnpl = false;

	/**
	 * Supported customer locations for which charges for a payment method can be processed
	 * Empty if all customer locations are supported
	 *
	 * @var string[]
	 */
	protected $countries = [];

	/**
	 * Create instance of payment method
	 *
	 * @param WC_Payments_Token_Service $token_service Instance of WC_Payments_Token_Service.
	 * @param class-string<T>|null      $definition    Optional payment method definition class name.
	 */
	public function __construct( $token_service, ?string $definition = null ) {
		$this->token_service = $token_service;
		$this->definition    = $definition;

		if ( null !== $this->definition ) {
			// Cache values that don't require context.
			$this->stripe_id                    = $this->definition::get_id();
			$this->is_reusable                  = PaymentMethodUtils::is_reusable( $this->definition );
			$this->currencies                   = $this->definition::get_supported_currencies();
			$this->accept_only_domestic_payment = PaymentMethodUtils::accepts_only_domestic_payments( $this->definition );
			$this->limits_per_currency          = $this->definition::get_limits_per_currency();
			$this->is_bnpl                      = PaymentMethodUtils::is_bnpl( $this->definition );
			$this->countries                    = $this->definition::get_supported_countries();
		}
	}

	/**
	 * Returns payment method ID
	 *
	 * @return string
	 */
	public function get_id() {
		if ( null !== $this->definition ) {
			return $this->definition::get_id();
		}
		return $this->stripe_id;
	}

	/**
	 * Returns payment method title
	 *
	 * @param string|null $account_country Country of merchants account.
	 * @param array|false $payment_details Optional payment details from charge object.
	 *
	 * @return string
	 *
	 * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
	 */
	public function get_title( ?string $account_country = null, $payment_details = false ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_title( $account_country );
		}
		return $this->title;
	}

	/**
	 * Returns payment method currencies
	 *
	 * @return array
	 */
	public function get_currencies() {
		return $this->currencies;
	}

	/**
	 * Determines whether the payment method is restricted to the Stripe account's currency.
	 * E.g.: Afterpay/Clearpay and Affirm only supports domestic payments; Klarna also implements a simplified version of these market restrictions.
	 *
	 * @return bool
	 */
	public function has_domestic_transactions_restrictions() {
		return $this->accept_only_domestic_payment;
	}

	/**
	 * Returns boolean dependent on whether payment method can be used at checkout.
	 *
	 * Payment method can be used at checkout if:
	 *  - If there are payment amount limits, order total is within limits.
	 *  - If it is a subscription order, payment method is either reusable, or subscription is manual.
	 *
	 * @param string $account_country Country of merchants account.
	 * @param bool   $skip_limits_per_currency_check Whether to skip limits per currency check.
	 *
	 * @return bool
	 */
	public function is_enabled_at_checkout( string $account_country, bool $skip_limits_per_currency_check = false ) {
		// Check if we're in a subscription context (cart checkout, changing payment method, or renewal).
		$is_subscription_context = $this->is_subscription_item_in_cart() || $this->is_changing_payment_method_for_subscription();

		// Also check if we're on the order-pay page for a renewal order.
		if ( ! $is_subscription_context && is_wc_endpoint_url( 'order-pay' ) && function_exists( 'wcs_order_contains_renewal' ) ) {
			$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
			if ( $order && wcs_order_contains_renewal( $order ) ) {
				$is_subscription_context = true;
			}
		}

		// Reusable methods are always available for subscriptions. Other methods are available if manual renewal is allowed.
		$are_manual_renewals_accepted  = function_exists( 'wcs_is_manual_renewal_enabled' ) && wcs_is_manual_renewal_enabled();
		$is_available_for_subscription = $are_manual_renewals_accepted || $this->is_reusable();

		$order_is_within_currency_limits = true;
		// This part ensures that when payment limits for the currency declared, those will be respected (e.g. BNPLs).
		if ( [] !== $this->limits_per_currency && ! $skip_limits_per_currency_check ) {
			$order = null;
			if ( is_wc_endpoint_url( 'order-pay' ) ) {
				$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
				$order = is_a( $order, 'WC_Order' ) ? $order : null;
			}

			$currency = get_woocommerce_currency();
			if ( $order ) {
				$currency = $order->get_currency();
			}

			// If the currency limits are not defined, we allow the PM for now (gateway has similar validation for limits).
			$total = null;
			if ( $order ) {
				$total = $order->get_total();
			} elseif ( isset( WC()->cart ) ) {
				$total = WC()->cart->get_total( '' );
			}

			if ( isset( $this->limits_per_currency[ $currency ], WC()->cart ) && ! empty( $total ) ) {
				$amount = WC_Payments_Utils::prepare_amount( $total, $currency );

				if ( $amount > 0 ) {
					$range = null;
					if ( isset( $this->limits_per_currency[ $currency ][ $account_country ] ) ) {
						$range = $this->limits_per_currency[ $currency ][ $account_country ];
					} elseif ( isset( $this->limits_per_currency[ $currency ]['default'] ) ) {
						$range = $this->limits_per_currency[ $currency ]['default'];
					}
					// If there is no range specified for the currency-country pair we don't support it and return false.
					if ( null === $range ) {
						$order_is_within_currency_limits = false;
					} else {
						$is_valid_minimum                = null === $range['min'] || $amount >= $range['min'];
						$is_valid_maximum                = null === $range['max'] || $amount <= $range['max'];
						$order_is_within_currency_limits = $is_valid_minimum && $is_valid_maximum;
					}
				}
			}
		}

		return $order_is_within_currency_limits
			&& ( ( ! $is_subscription_context ) || $is_available_for_subscription );
	}

	/**
	 * Returns boolean dependent on whether payment method
	 * will support saved payments/subscription payments
	 *
	 * @return bool
	 */
	public function is_reusable() {
		return $this->is_reusable;
	}

	/**
	 * Returns boolean dependent on whether payment method
	 * will support BNPL (Buy Now Pay Later) payments
	 *
	 * @return bool
	 */
	public function is_bnpl() {
		return $this->is_bnpl;
	}

	/**
	 * Returns boolean dependent on whether payment method will accept charges
	 * with chosen currency
	 *
	 * @param string   $account_domestic_currency Domestic currency of the account.
	 * @param int|null $order_id                 Optional order ID, if order currency should take precedence.
	 *
	 * @return bool
	 */
	public function is_currency_valid( string $account_domestic_currency, $order_id = null ) {
		$current_store_currency = $this->get_currency( $order_id );
		if ( null === $current_store_currency ) {
			return false;
		}

		if ( $this->has_domestic_transactions_restrictions() ) {
			if ( strtolower( $current_store_currency ) !== strtolower( $account_domestic_currency ) ) {
				return false;
			}
		}

		$supported_currencies = $this->get_currencies();

		return empty( $supported_currencies ) || in_array( $current_store_currency, $supported_currencies, true );
	}

	/**
	 * Add payment method to user and return WC payment token
	 *
	 * @param WP_User $user User to get payment token from.
	 * @param string  $payment_method_id Stripe payment method ID string.
	 *
	 * @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA WC object for payment token.
	 */
	public function get_payment_token_for_user( $user, $payment_method_id ) {
		return $this->token_service->add_payment_method_to_user( $payment_method_id, $user );
	}

	/**
	 * Returns testing credentials to be printed at checkout in test mode.
	 *
	 * @param string $account_country The country of the account.
	 * @return string
	 */
	public function get_testing_instructions( string $account_country ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_testing_instructions( $account_country );
		}
		return '';
	}

	/**
	 * Returns the payment method icon URL or an empty string.
	 *
	 * @param string|null $account_country Optional account country.
	 * @return string
	 *
	 * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
	 */
	public function get_icon( ?string $account_country = null ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_icon_url( $account_country );
		}
		return isset( $this->icon_url ) ? $this->icon_url : '';
	}

	/**
	 * Returns icon to use on dark themes.
	 *
	 * @param string|null $account_country Optional account country.
	 * @return string
	 */
	public function get_dark_icon( ?string $account_country = null ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_dark_icon_url( $account_country );
		}
		return isset( $this->dark_icon_url ) ? $this->dark_icon_url : $this->get_icon( $account_country );
	}

	/**
	 * Gets the theme appropriate icon for the payment method for a given location and context.
	 *
	 * @param string  $location The location to get the icon for.
	 * @param boolean $is_blocks Whether the icon is for blocks.
	 * @param string  $account_country Optional account country.
	 * @return string
	 */
	public function get_payment_method_icon_for_location( string $location = 'checkout', bool $is_blocks = true, ?string $account_country = null ) {
		$appearance_theme = WC_Payments_Utils::get_active_upe_theme_transient_for_location( $location, $is_blocks ? 'blocks' : 'classic' );

		if ( 'night' === $appearance_theme ) {
			return $this->get_dark_icon( $account_country );
		}

		return $this->get_icon( $account_country );
	}

	/**
	 * Returns payment method supported countries
	 *
	 * @return array
	 */
	public function get_countries() {
		$account         = \WC_Payments::get_account_service()->get_cached_account_data();
		$account_country = isset( $account['country'] ) ? strtoupper( $account['country'] ) : '';

		// For definition-based payment methods, call get_supported_countries with account_country.
		// The definition handles any domestic/regional restrictions (e.g., Klarna's EEA cross-border logic).
		if ( ! empty( $this->definition ) ) {
			return $this->definition::get_supported_countries( $account_country );
		}

		return $this->has_domestic_transactions_restrictions() ? [ $account_country ] : $this->countries;
	}

	/**
	 * Returns payment method description for the settings page.
	 *
	 * @param string|null $account_country Country of merchants account.
	 *
	 * @return string
	 */
	public function get_description( ?string $account_country = null ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_description( $account_country );
		}
		return '';
	}

	/**
	 * Returns payment method settings label.
	 *
	 * @param string $account_country Country of merchants account.
	 * @return string
	 */
	public function get_settings_label( string $account_country ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_settings_label( $account_country );
		}
		return $this->get_title( $account_country );
	}

	/**
	 * Returns payment method settings icon.
	 *
	 * @param string|null $account_country Country of merchants account.
	 * @return string
	 */
	public function get_settings_icon_url( ?string $account_country = null ) {
		if ( null !== $this->definition ) {
			return $this->definition::get_settings_icon_url( $account_country );
		}
		return $this->get_icon( $account_country );
	}

	/**
	 * Returns boolean dependent on whether payment method allows manual capture.
	 *
	 * @return bool
	 */
	public function allows_manual_capture() {
		if ( null !== $this->definition ) {
			return PaymentMethodUtils::allows_manual_capture( $this->definition );
		}

		return false;
	}

	/**
	 * Returns the Stripe key for the payment method.
	 *
	 * @return string
	 */
	public function get_stripe_key() {
		if ( null !== $this->definition ) {
			return $this->definition::get_stripe_id();
		}
		return \WC_Payments::get_gateway()->get_payment_method_capability_key_map()[ $this->stripe_id ];
	}

	/**
	 * Returns payment method settings definition.
	 *
	 * @param string $account_country Country of merchants account.
	 * @return array
	 */
	public function get_payment_method_information_object( string $account_country ) {
		return [
			'id'                            => $this->get_id(),
			'label'                         => $this->get_settings_label( $account_country ),
			'description'                   => $this->get_description( $account_country ),
			'settings_icon_url'             => $this->get_settings_icon_url( $account_country ),
			'currencies'                    => $this->get_currencies(),
			'stripe_key'                    => $this->get_stripe_key(),
			'allows_manual_capture'         => $this->allows_manual_capture(),
			'allows_pay_later'              => $this->is_bnpl(),
			'accepts_only_domestic_payment' => $this->has_domestic_transactions_restrictions(),
		];
	}

	/**
	 * Returns valid currency to use to filter payment methods.
	 *
	 * @param int $order_id Optional order ID, if order currency should take precedence.
	 *
	 * @return string|null
	 */
	private function get_currency( $order_id = null ) {
		if ( is_wc_endpoint_url( 'order-pay' ) || null !== $order_id ) {
			global $wp;
			if ( null === $order_id ) {
				$order_id = absint( $wp->query_vars['order-pay'] );
			}
			$order = wc_get_order( $order_id );
			if ( false === $order ) {
				return null;
			}
			return $order->get_currency();
		}
		return get_woocommerce_currency();
	}
}
