<?php
/**
 * Class Payment_Information
 *
 * @package WooCommerce\Payments
 */

namespace WCPay;

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

use WCPay\Constants\Payment_Method;
use WCPay\Constants\Payment_Type;
use WCPay\Constants\Payment_Initiated_By;
use WCPay\Constants\Payment_Capture_Type;
use WCPay\Exceptions\Invalid_Payment_Method_Exception;

/**
 * Mostly a wrapper containing information on a single payment.
 */
class Payment_Information {
	/**
	 * Key used to indicate that an error occurred during the payment method creation in the client.
	 *
	 * @type string
	 */
	const PAYMENT_METHOD_ERROR = 'woocommerce_payments_payment_method_error';

	/**
	 * The ID of the payment method used for this payment.
	 *
	 * @var string
	 */
	private $payment_method;

	/**
	 * The order object.
	 *
	 * @var ?\WC_Order
	 */
	private $order;

	/**
	 * The payment token used for this payment.
	 *
	 * @var ?\WC_Payment_Token
	 */
	private $token;

	/**
	 * The CVC confirmation used for this payment.
	 *
	 * @var string
	 */
	private $cvc_confirmation;

	/**
	 * Indicates whether the payment is merchant-initiated (true) or customer-initiated (false).
	 *
	 * @var ?Payment_Initiated_By
	 */
	private $payment_initiated_by;

	/**
	 * Indicates whether the payment will be only authorized (true) or captured immediately (false).
	 *
	 * @var ?Payment_Capture_Type
	 */
	private $manual_capture;

	/**
	 * The type of the payment. `single`, `recurring`, etc.
	 *
	 * @var Payment_Type
	 */
	private $payment_type;

	/**
	 * Indicates whether the payment method should be saved to the store.
	 *
	 * @var bool
	 */
	private $save_payment_method_to_store = false;

	/**
	 * Indicates whether the payment method should be saved to the platform.
	 *
	 * @var bool
	 */
	private $save_payment_method_to_platform = false;

	/**
	 * Indicates whether user is changing payment method for subscriptions order.
	 *
	 * @var bool
	 */
	private $is_changing_payment_method_for_subscription = false;

	/**
	 * The attached fingerprint.
	 *
	 * @var string
	 */
	private $fingerprint = '';

	/**
	 * The Stripe ID of the payment method used for this payment.
	 *
	 * @var string
	 */
	private $payment_method_stripe_id;

	/**
	 * The WCPay Customer ID that owns the payment token.
	 *
	 * @var string
	 */
	private $customer_id;

	/**
	 * Will be set if there was an error during setup.
	 *
	 * @var ?\WP_Error
	 */
	private $error = null;

	/**
	 * Payment information constructor.
	 *
	 * @param string               $payment_method The ID of the payment method used for this payment.
	 * @param \WC_Order            $order The order object.
	 * @param Payment_Type         $payment_type The type of the payment.
	 * @param \WC_Payment_Token    $token The payment token used for this payment.
	 * @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated.
	 * @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately.
	 * @param string               $cvc_confirmation The CVC confirmation for this payment method.
	 * @param string               $fingerprint The attached fingerprint.
	 * @param string               $payment_method_stripe_id The Stripe ID of the payment method used for this payment.
	 * @param string               $customer_id The WCPay Customer ID that owns the payment token.
	 *
	 * @throws Invalid_Payment_Method_Exception When no payment method is found in the provided request.
	 */
	public function __construct(
		string $payment_method,
		?\WC_Order $order = null,
		?Payment_Type $payment_type = null,
		?\WC_Payment_Token $token = null,
		?Payment_Initiated_By $payment_initiated_by = null,
		?Payment_Capture_Type $manual_capture = null,
		?string $cvc_confirmation = null,
		string $fingerprint = '',
		?string $payment_method_stripe_id = null,
		?string $customer_id = null
	) {
		if ( empty( $payment_method ) && empty( $token ) && ! \WC_Payments::is_network_saved_cards_enabled() ) {
			// If network-wide cards are enabled, a payment method or token may not be specified and the platform default one will be used.
			throw new Invalid_Payment_Method_Exception(
				esc_html__( 'Invalid or missing payment details. Please ensure the provided payment method is correctly entered.', 'woocommerce-payments' ),
				'payment_method_not_provided'
			);
		}
		$this->payment_method           = $payment_method;
		$this->order                    = $order;
		$this->token                    = $token;
		$this->payment_initiated_by     = $payment_initiated_by ?? Payment_Initiated_By::CUSTOMER();
		$this->manual_capture           = $manual_capture ?? Payment_Capture_Type::AUTOMATIC();
		$this->payment_type             = $payment_type ?? Payment_Type::SINGLE();
		$this->cvc_confirmation         = $cvc_confirmation;
		$this->fingerprint              = $fingerprint;
		$this->payment_method_stripe_id = $payment_method_stripe_id;
		$this->customer_id              = $customer_id;
	}

	/**
	 * Returns true if payment was initiated by the merchant, false otherwise.
	 *
	 * @return bool True if payment was initiated by the merchant, false otherwise.
	 */
	public function is_merchant_initiated(): bool {
		return $this->payment_initiated_by->equals( Payment_Initiated_By::MERCHANT() );
	}

	/**
	 * Returns the payment method ID or confirmation token.
	 *
	 * @return string The payment method ID or confirmation token.
	 */
	public function get_payment_method(): string {
		// Use the token if we have it.
		if ( $this->is_using_saved_payment_method() ) {
			return $this->token->get_token();
		}

		return $this->payment_method;
	}

	/**
	 * Returns whether the payment is using a confirmation token or a payment method.
	 *
	 * @see https://docs.stripe.com/payments/mobile/migration-confirmation-tokens
	 *
	 * @return bool True if using a confirmation token, false otherwise.
	 */
	public function is_using_confirmation_token(): bool {
		return 0 === strpos( $this->get_payment_method(), 'ctoken_' );
	}

	/**
	 * Returns the order object.
	 *
	 * @return ?\WC_Order The order object.
	 */
	public function get_order(): ?\WC_Order {
		return $this->order;
	}

	/**
	 * Returns the payment token.
	 *
	 * TODO: Once php requirement is bumped to >= 7.1.0 change return type to ?\WC_Payment_Token
	 * since the return type is nullable, as per
	 * https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
	 *
	 * @return ?\WC_Payment_Token The payment token.
	 */
	public function get_payment_token(): ?\WC_Payment_Token {
		return $this->token;
	}

	/**
	 * Update the payment token associated with this payment.
	 *
	 * @param \WC_Payment_Token $token The new payment token.
	 */
	public function set_token( \WC_Payment_Token $token ) {
		$this->token = $token;
	}

	/**
	 * Returns true if the payment token is not empty, false otherwise.
	 *
	 * @return bool True if payment token is not empty, false otherwise.
	 */
	public function is_using_saved_payment_method(): bool {
		return ! empty( $this->token );
	}

	/**
	 * Returns true if the payment should be only authorized, false if it should be captured immediately.
	 *
	 * @return bool True if the payment should be only authorized, false if it should be captured immediately.
	 */
	public function is_using_manual_capture(): bool {
		return $this->manual_capture->equals( Payment_Capture_Type::MANUAL() );
	}

	/**
	 * Payment information constructor.
	 *
	 * @param array                $request Associative array containing payment request information.
	 * @param \WC_Order            $order The order object.
	 * @param Payment_Type         $payment_type The type of the payment.
	 * @param Payment_Initiated_By $payment_initiated_by Indicates whether the payment is merchant-initiated or customer-initiated.
	 * @param Payment_Capture_Type $manual_capture Indicates whether the payment will be only authorized or captured immediately.
	 * @param string               $payment_method_stripe_id The Stripe ID of the payment method used for this payment.
	 *
	 * @throws \Exception - If no payment method is found in the provided request.
	 */
	public static function from_payment_request(
		array $request,
		?\WC_Order $order = null,
		?Payment_Type $payment_type = null,
		?Payment_Initiated_By $payment_initiated_by = null,
		?Payment_Capture_Type $manual_capture = null,
		?string $payment_method_stripe_id = null
	): Payment_Information {
		$payment_method   = self::get_payment_method_from_request( $request );
		$token            = self::get_token_from_request( $request );
		$cvc_confirmation = self::get_cvc_confirmation_from_request( $request );
		$fingerprint      = self::get_fingerprint_from_request( $request );

		if ( isset( $request['is_woopay'] ) && $request['is_woopay'] ) {
			$order->add_meta_data( 'is_woopay', true, true );
			$order->save_meta_data();
		}
		$payment_information = new Payment_Information( $payment_method, $order, $payment_type, $token, $payment_initiated_by, $manual_capture, $cvc_confirmation, $fingerprint, $payment_method_stripe_id );

		if ( self::PAYMENT_METHOD_ERROR === $payment_method ) {
			$error_message = empty( $request['wcpay-payment-method-error-message'] )
				? __( "We're not able to process this payment. Please try again later.", 'woocommerce-payments' )
				: $request['wcpay-payment-method-error-message'];
			$error_code    = empty( $request['wcpay-payment-method-error-code'] ) ? 'unknown-error' : $request['wcpay-payment-method-error-code'];
			$error         = new \WP_Error( $error_code, $error_message );
			$payment_information->set_error( $error );
		}

		return $payment_information;
	}

	/**
	 * Extracts the payment method or confirmation token from the provided request.
	 *
	 * @param array $request Associative array containing payment request information.
	 *
	 * @return string
	 */
	public static function get_payment_method_from_request( array $request ): string {
		// Check for confirmation token first (new ECE flow), then fall back to payment method (legacy flow).
		foreach ( [ 'wcpay-confirmation-token', 'wcpay-payment-method', 'wcpay-payment-method-sepa' ] as $key ) {
			if ( ! empty( $request[ $key ] ) ) {
				$normalized = wc_clean( $request[ $key ] );
				return is_string( $normalized ) ? $normalized : '';
			}
		}
		return '';
	}

	/**
	 * Extract the payment token from the provided request.
	 *
	 * TODO: Once php requirement is bumped to >= 7.1.0 set return type to ?\WC_Payment_Token
	 * since the return type is nullable, as per
	 * https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
	 *
	 * @param array $request Associative array containing payment request information.
	 *
	 * @return \WC_Payment_Token|NULL
	 */
	public static function get_token_from_request( array $request ) {
		$payment_method    = $request['payment_method'] ?? null;
		$token_request_key = 'wc-' . $payment_method . '-payment-token';
		if (
			! isset( $request[ $token_request_key ] ) ||
			'new' === $request[ $token_request_key ]
		) {
			return null;
		}

		//phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		$token = \WC_Payment_Tokens::get( wc_clean( $request[ $token_request_key ] ) );

		// If the token doesn't belong to this gateway or the current user it's invalid.
		if ( ! $token || $payment_method !== $token->get_gateway_id() || $token->get_user_id() !== get_current_user_id() ) {
			return null;
		}

		return $token;
	}

	/**
	 * Extract the payment CVC confirmation from the provided request.
	 *
	 * @param array $request Associative array containing payment request information.
	 *
	 * @return string|NULL
	 */
	public static function get_cvc_confirmation_from_request( array $request ) {
		$payment_method = $request['payment_method'] ?? null;
		if ( null === $payment_method ) {
			return null;
		}

		$cvc_request_key = 'wc-' . $payment_method . '-payment-cvc-confirmation';
		if (
			! isset( $request[ $cvc_request_key ] ) ||
			'new' === $request[ $cvc_request_key ]
		) {
			return null;
		}

		return $request[ $cvc_request_key ];
	}

	/**
	 * Extracts the fingerprint data from the provided request.
	 *
	 * @param array $request Associative array containing payment request information.
	 *
	 * @return string
	 */
	public static function get_fingerprint_from_request( array $request ) {
		if ( ! empty( $request['wcpay-fingerprint'] ) ) {
			$normalized = wc_clean( $request['wcpay-fingerprint'] );
			return is_string( $normalized ) ? $normalized : '';
		}

		return '';
	}

	/**
	 * Changes the type of the payment.
	 *
	 * @param Payment_Type $type The new type.
	 */
	public function set_payment_type( $type ) {
		$this->payment_type = $type;
	}

	/**
	 * Retrieves the type of the payment.
	 *
	 * @return Payment_Type The payment type.
	 */
	public function get_payment_type() {
		return $this->payment_type;
	}

	/**
	 * Forces the payment method to be saved to merchant store when the payment gets processed.
	 */
	public function must_save_payment_method_to_store() {
		$this->save_payment_method_to_store = true;
	}

	/**
	 * Forces the payment method to be saved to platform when the payment gets processed.
	 */
	public function must_save_payment_method_to_platform() {
		$this->save_payment_method_to_platform = true;
	}

	/**
	 * Indicates whether the payment method needs to be saved to the store for later usage.
	 *
	 * @return bool The flag.
	 */
	public function should_save_payment_method_to_store() {
		return ! $this->is_using_saved_payment_method() && $this->save_payment_method_to_store;
	}

	/**
	 * Indicates whether the payment method needs to be saved to the platform for later usage.
	 *
	 * @return bool The flag.
	 */
	public function should_save_payment_method_to_platform() {
		return ! $this->is_using_saved_payment_method() && $this->save_payment_method_to_platform;
	}

	/**
	 * Mark that we are changing payment for subscriptions order or not.
	 *
	 * @param bool $is_changing_payment_method_for_subscription Whether or not we are changing payment for subscriptions order.
	 */
	public function set_is_changing_payment_method_for_subscription( bool $is_changing_payment_method_for_subscription ) {
		$this->is_changing_payment_method_for_subscription = $is_changing_payment_method_for_subscription;
	}

	/**
	 * Returns the flag of whether or not we are changing payment for subscriptions order.
	 *
	 * @return bool Whether or not we are changing payment for subscriptions order.
	 */
	public function is_changing_payment_method_for_subscription(): bool {
		return $this->is_changing_payment_method_for_subscription;
	}

	/**
	 * Returns the payment method CVC confirmation.
	 *
	 * @return string|NULL The payment method CVC confirmation.
	 */
	public function get_cvc_confirmation() {
		return $this->cvc_confirmation;
	}

	/**
	 * Returns the attached fingerprint.
	 *
	 * @return string The attached fingerprint.
	 */
	public function get_fingerprint() {
		return $this->fingerprint;
	}

	/**
	 * Returns the Stripe ID of payment method.
	 *
	 * @return string The Stripe ID of payment method.
	 */
	public function get_payment_method_stripe_id() {
		return $this->payment_method_stripe_id;
	}

	/**
	 * Returns the WCPay Customer ID that owns the payment token.
	 *
	 * @return string The WCPay Customer ID.
	 */
	public function get_customer_id() {
		return $this->customer_id;
	}


	/**
	 * Sets the error data.
	 *
	 * @param \WP_Error $error The error to be set.
	 * @return void
	 */
	public function set_error( \WP_Error $error ) {
		$this->error = $error;
	}

	/**
	 * Returns the error data.
	 *
	 * @return ?\WP_Error
	 */
	public function get_error() {
		return $this->error;
	}

	/**
	 * Returns true if the payment method is an offline payment method, false otherwise.
	 *
	 * @return bool True if the payment method is an offline payment method, false otherwise.
	 */
	public function is_offline_payment_method(): bool {
		return in_array( $this->payment_method_stripe_id, Payment_Method::OFFLINE_PAYMENT_METHODS, true );
	}
}
