<?php
/**
 * Stripe payout failure classifier.
 *
 * Maps Stripe-specific error codes (both WP_Error codes from our processor
 * and underlying Stripe API exception types when we can extract them) onto
 * the cross-method Failure_Class enum.
 *
 * Called from `affwp_stripe_process_completed_batch()` when a referral's
 * Transfer::create() call fails, and again from `check_payout_status()` if
 * we surface a deferred failure later.
 *
 * Registered via `AffWP\Payouts\Failure_Classifier::register( 'stripe', ... )`
 * at init time.
 *
 * @package    AffiliateWP
 * @subpackage Payouts/Methods/Stripe
 * @since      2.33.0
 */

use AffWP\Payouts\Failure_Class;

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

/**
 * Classifies a Stripe payout error into a Failure_Class bucket.
 *
 * Reads two signals:
 *   1. Our own WP_Error code (from process_single_payout): `stripe_not_connected`,
 *      `invalid_referral`, `stripe_error`.
 *   2. Substrings in the error message that come from the underlying Stripe
 *      API exception (currently we don't preserve the Stripe code as a
 *      structured field — see class-stripe-payouts.php:570 where it's
 *      extracted only for the failure email — so we match on the human
 *      message that gets stored to the payout description).
 *
 * Substring matching is a temporary measure until the Stripe processor
 * stores the structured `getStripeCode()` value on the payout row. Once
 * that lands, this classifier reads the code directly.
 *
 * @since 2.33.0
 *
 * @param string $error_code    The error code (from WP_Error->get_error_code()).
 * @param string $error_message Human-readable message; may include the underlying Stripe message.
 * @return string One of the Failure_Class constants.
 */
function affwp_stripe_payouts_classify_failure( $error_code, $error_message ) {

	$code    = (string) $error_code;
	$message = strtolower( (string) $error_message );

	/*
	 * Direct WP_Error code matches first — these come from our own
	 * processor and are unambiguous.
	 */
	if ( 'stripe_not_connected' === $code ) {
		return Failure_Class::AFFILIATE_ACTION_REQUIRED;
	}

	/*
	 * `invalid_referral` means the referral was already paid (idempotency
	 * reject). Not really a failure; the caller treats it as a no-op
	 * success. Classify as transient so the row, if it somehow gets
	 * persisted as failed, stays retryable.
	 */
	if ( 'invalid_referral' === $code ) {
		return Failure_Class::TRANSIENT;
	}

	/*
	 * `stripe_error` is our catch-all for exceptions thrown by
	 * Transfer::create(). The actionable signal is in the message.
	 * Patterns below match Stripe's standard message templates.
	 */

	if ( false !== strpos( $message, 'insufficient' ) ) {
		/*
		 * "Your Stripe balance is insufficient to make this transfer."
		 * Admin needs to top up the platform balance.
		 */
		return Failure_Class::ADMIN_ACTION_REQUIRED;
	}

	if ( false !== strpos( $message, 'capability' ) || false !== strpos( $message, 'capabilities' ) ) {
		/*
		 * "Transfers capability is not active on the connected account."
		 * The affiliate's Connect account doesn't have the `transfers`
		 * capability enabled — usually because Stripe is asking for
		 * additional verification (e.g. tax info). Affiliate action.
		 */
		return Failure_Class::AFFILIATE_ACTION_REQUIRED;
	}

	if ( false !== strpos( $message, 'rate limit' ) || false !== strpos( $message, 'rate_limit' ) ) {
		return Failure_Class::TRANSIENT;
	}

	if ( false !== strpos( $message, 'timeout' ) || false !== strpos( $message, 'timed out' ) ) {
		return Failure_Class::TRANSIENT;
	}

	if ( false !== strpos( $message, 'invalid_request' ) || false !== strpos( $message, 'invalid request' ) ) {
		/*
		 * Often indicates a data error on the affiliate's profile (bad
		 * email, missing required field). Surface to admin to investigate.
		 */
		return Failure_Class::DATA_ERROR;
	}

	if ( false !== strpos( $message, 'authentication' ) || false !== strpos( $message, 'api_key' ) ) {
		/*
		 * Stripe API key problem; admin must fix in settings.
		 */
		return Failure_Class::ADMIN_ACTION_REQUIRED;
	}

	return Failure_Class::UNKNOWN;
}

AffWP\Payouts\Failure_Classifier::register( 'stripe', 'affwp_stripe_payouts_classify_failure' );

/*
 * Default body for the Stripe variant of the cross-method "Payout failed,
 * action required" email. Method-specific wording (the affiliate must finish
 * Stripe Connect), composed only from the cross-method merge tags so the
 * unified dispatcher resolves it — {affiliate_payout_settings_url} lands them
 * on the Stripe section of their payout settings (portal-aware). Editable in
 * Settings → Emails.
 */
AffWP\Payouts\Failure_Email_Registry::register(
	'stripe',
	array(
		'label' => __( 'Stripe', 'affiliate-wp' ),
		'body'  => __( "Hi {name},\n\nWe couldn't send your {amount} commission from {site_name} because your Stripe account needs attention.\n\nHere's how to fix it:\n1. Open your settings: {affiliate_payout_settings_url}\n2. Connect or finish setting up your Stripe account\n3. Resolve anything Stripe is asking you to verify\n\nOnce that's done, we'll send it again automatically. Questions? Just reply to this email.\n\nThanks,\n{site_name}", 'affiliate-wp' ),
	)
);

/**
 * Readiness predicate for the auto-retry sweep — Stripe.
 *
 * Returns true when the affiliate has finished Stripe Connect onboarding
 * (account ID stored on user meta). Cheap check, no API roundtrip.
 *
 * Hooks the cross-method `affwp_payout_method_is_affiliate_ready` filter
 * with method=='stripe'. Other methods' hooks live in their own files.
 *
 * @since 2.33.0
 *
 * @param bool   $is_ready     Default from the filter chain.
 * @param string $method       Method slug.
 * @param int    $affiliate_id Affiliate ID.
 * @return bool
 */
function affwp_stripe_payouts_filter_is_affiliate_ready( $is_ready, $method, $affiliate_id ) {
	if ( 'stripe' !== $method ) {
		return $is_ready;
	}
	if ( ! function_exists( 'affwp_stripe_payouts_is_affiliate_connected' ) ) {
		return $is_ready;
	}
	return (bool) affwp_stripe_payouts_is_affiliate_connected( $affiliate_id );
}

add_filter( 'affwp_payout_method_is_affiliate_ready', 'affwp_stripe_payouts_filter_is_affiliate_ready', 10, 3 );
