<?php
/**
 * Payment gateway capability lookup.
 *
 * @since   1.0.0
 * @package WarrantyCart
 */

namespace WarrantyCart\Integrations;

use WC_Order;
use WC_Payment_Gateway;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * What the store's payment gateways can do.
 *
 * ## Why this replaced per-gateway integration classes
 *
 * This was `IntegrationManager` holding a `StripeIntegration` and a
 * `PayPalIntegration`, each keyed to one hardcoded plugin file
 * (`woocommerce-gateway-stripe/woocommerce-gateway-stripe.php`) and a minimum
 * version. Three things were wrong with that:
 *
 * 1. Warranties never needed it. `Warranties` creates records on
 *    `woocommerce_order_status_completed`, which is WooCommerce core and fires
 *    for every gateway including cash on delivery. The two classes contributed
 *    a log line and a `source` value that duplicated the `payment_method`
 *    column already written beside it.
 * 2. A store on Square, Mollie, Amazon Pay, WooPayments or a regional gateway
 *    got nothing, and adding one meant shipping another class.
 * 3. Asking "is plugin X installed at version Y" is the wrong question. A
 *    gateway's plugin file tells you nothing about what the gateway can do.
 *
 * WooCommerce already answers the right question. Every `WC_Payment_Gateway`
 * carries a `$supports` array and a `supports()` method, and gateways declare
 * their own capabilities into it. WooCommerce Subscriptions — the reference
 * implementation for this problem — never checks a plugin slug either; it asks
 * `$gateway->supports( 'subscriptions' )` and enumerates
 * `WC()->payment_gateways->get_available_payment_gateways()`.
 *
 * So this class asks gateways what they can do, and knows the name of no
 * gateway at all.
 */
class GatewaySupport {

	/**
	 * Refunding a payment through the gateway, without leaving WooCommerce.
	 *
	 * The capability that matters for warranties: resolving a claim in the
	 * customer's favour can mean money going back, and whether that can happen
	 * from the claim screen or has to be done by hand in the gateway's own
	 * dashboard depends entirely on this.
	 *
	 * @var string
	 */
	public const REFUNDS = 'refunds';

	/**
	 * Every gateway WooCommerce knows about, enabled or not.
	 *
	 * @return array<string, WC_Payment_Gateway> Keyed by gateway ID.
	 */
	public function all(): array {
		if ( ! function_exists( 'WC' ) || ! WC()->payment_gateways() ) {
			return array();
		}

		$gateways = array();

		foreach ( WC()->payment_gateways()->payment_gateways() as $gateway ) {
			$gateways[ $gateway->id ] = $gateway;
		}

		return $gateways;
	}

	/**
	 * The gateways a customer can actually pay a warranty with right now.
	 *
	 * `get_available_payment_gateways()` rather than filtering `all()` on
	 * `enabled`: availability also accounts for a gateway's own
	 * `is_available()`, which is where currency, country and minimum-amount
	 * restrictions live. A gateway can be enabled and still not offered.
	 *
	 * @return array<string, WC_Payment_Gateway> Keyed by gateway ID.
	 */
	public function available(): array {
		if ( ! function_exists( 'WC' ) || ! WC()->payment_gateways() ) {
			return array();
		}

		return WC()->payment_gateways()->get_available_payment_gateways();
	}

	/**
	 * Get one gateway by ID.
	 *
	 * @param string $gateway_id Gateway ID, e.g. `stripe` or `ppcp-gateway`.
	 *
	 * @return WC_Payment_Gateway|null Null when no such gateway is registered.
	 */
	public function get( string $gateway_id ): ?WC_Payment_Gateway {
		return $this->all()[ $gateway_id ] ?? null;
	}

	/**
	 * Whether a gateway supports a feature.
	 *
	 * @param string $gateway_id Gateway ID.
	 * @param string $feature    Feature name, e.g. self::REFUNDS.
	 *
	 * @return bool False when the gateway is not registered.
	 */
	public function supports( string $gateway_id, string $feature ): bool {
		$gateway = $this->get( $gateway_id );

		$supported = $gateway instanceof WC_Payment_Gateway && $gateway->supports( $feature );

		/**
		 * Filter whether a gateway supports a warranty-related feature.
		 *
		 * The escape hatch for a gateway that can do something but does not
		 * declare it — the same allowance WooCommerce Subscriptions makes, and
		 * for the same reason: a store should not have to wait for a gateway
		 * release to use a capability the gateway already has.
		 *
		 * @param bool                    $supported  Whether the feature is supported.
		 * @param string                  $feature    Feature name.
		 * @param string                  $gateway_id Gateway ID.
		 * @param WC_Payment_Gateway|null $gateway    Gateway instance, if registered.
		 */
		return (bool) \apply_filters( 'warranty_cart_gateway_supports', $supported, $feature, $gateway_id, $gateway );
	}

	/**
	 * Whether the gateway that paid for an order supports a feature.
	 *
	 * @param WC_Order $order   Order to check.
	 * @param string   $feature Feature name, e.g. self::REFUNDS.
	 *
	 * @return bool
	 */
	public function order_supports( WC_Order $order, string $feature ): bool {
		$gateway_id = $order->get_payment_method();

		if ( '' === $gateway_id ) {
			return false;
		}

		return $this->supports( $gateway_id, $feature );
	}

	/**
	 * Whether any gateway the store can currently take payment with supports a
	 * feature.
	 *
	 * Answers "can this store refund a claim at all", which is the question
	 * worth asking before offering the option anywhere in the UI.
	 *
	 * @param string $feature Feature name, e.g. self::REFUNDS.
	 *
	 * @return bool
	 */
	public function any_supports( string $feature ): bool {
		foreach ( $this->available() as $gateway_id => $gateway ) {
			if ( $this->supports( (string) $gateway_id, $feature ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * A report of every gateway and what it can do, for the admin.
	 *
	 * @return array<int, array<string, mixed>> One row per gateway, ordered as
	 *                                          WooCommerce orders them.
	 */
	public function status(): array {
		$available = $this->available();
		$status    = array();

		foreach ( $this->all() as $gateway_id => $gateway ) {
			$status[] = array(
				'id'          => (string) $gateway_id,
				'title'       => \wp_strip_all_tags( $gateway->get_title() ),
				'method_name' => \wp_strip_all_tags( $gateway->get_method_title() ),
				'enabled'     => 'yes' === $gateway->enabled,
				// Enabled and actually offerable are different questions; see
				// available().
				'available'   => isset( $available[ $gateway_id ] ),
				'supports'    => array(
					self::REFUNDS => $this->supports( (string) $gateway_id, self::REFUNDS ),
				),
			);
		}

		return $status;
	}
}
