<?php
/**
 * Admin Views: Payouts Tab
 *
 * Alternative design using the new card layout
 *
 * @package     AffiliateWP
 * @subpackage  Admin/Payouts
 * @copyright   Copyright (c) 2025, Sandhills Development, LLC
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       2.29.0
 */

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

$payment_methods = AffiliateWP_Payment_Methods::get_all();

// Payout settings section values.
$default_payout_method      = affiliate_wp()->settings->get( 'default_payout_method', 'manual' );
$allow_selection            = affwp_affiliates_can_select_payout_method();
$auto_email_payout_failures = (bool) affiliate_wp()->settings->get( 'auto_email_payout_failures', true );
$auto_retry_failed_payouts  = (bool) affiliate_wp()->settings->get( 'auto_retry_failed_payouts', true );

/*
 * Reactive method-selection data for the Default Payout Method select and
 * the summary footer. The select option list and the "can choose between
 * X and Y" clause both react to method-enabled toggles in the Payout
 * Methods list below, so the admin sees changes immediately without
 * having to save & reload.
 *
 * Card method_ids and registry ids share the same namespace — both use
 * the hyphenated form for the Payouts Service ('payouts-service') after
 * the 2.33.0 normalization. The card registration in
 * class-payouts-tab.php matches the registry id from
 * includes/payouts/methods/payouts-service/init.php. Setting/meta/URL
 * keys retain the underscore form (`enable_payouts_service`,
 * `payouts_service_account`, `?tab=payouts_service`) because those are
 * persisted DB and URL contracts.
 */
$all_method_labels = array();
/*
 * Prefer registry labels (e.g. "Stripe", "PayPal", "Manual Payout") since
 * those are what affwp_select displayed before this view became reactive.
 * Then fall back to card names (e.g. "Stripe Payouts", "Store Credit") for
 * methods the registry doesn't list yet — typically because they're
 * currently disabled and their registry filter callback returns early.
 * Same label appears once a save & reload re-runs the filters with the
 * method enabled, but for the immediate post-toggle render this fallback
 * keeps the new <option> from being label-less.
 */
foreach ( affwp_get_payout_methods() as $registry_id => $registry_label ) {
	$all_method_labels[ $registry_id ] = wp_strip_all_tags( $registry_label );
}
foreach ( $payment_methods as $card_method_id => $method ) {
	if ( ! isset( $all_method_labels[ $card_method_id ] ) ) {
		$all_method_labels[ $card_method_id ] = $method['name'];
	}
}

// Method IDs currently enabled — initial state for the reactive list.
$enabled_method_ids = affwp_get_enabled_payout_methods();

// Saved default can outlive its method (addon disabled, credentials cleared); fall back so summary + select agree.
if ( ! in_array( $default_payout_method, $enabled_method_ids, true ) ) {
	$default_payout_method = ! empty( $enabled_method_ids ) ? $enabled_method_ids[0] : 'manual';
}

/*
 * Methods admin can pick as the site default but affiliates cannot
 * self-select. Mirrors the admin_only list inside
 * affwp_get_payout_methods_for_display() so the summary's "can choose"
 * clause stays consistent with what the Affiliate Area actually shows.
 * Not run through the affwp_admin_only_payout_methods filter here because
 * Alpine state has no clean way to react to PHP filter mutations; if a
 * site adds methods via that filter, save & reload is required for the
 * summary to reflect them (an acceptable degradation).
 */
$hidden_methods        = get_option( 'affwp_hidden_affiliate_payout_methods', array() );
$admin_only_method_ids = array_merge( array( 'payouts-service', 'manual' ), is_array( $hidden_methods ) ? $hidden_methods : array() );

/*
 * Oxford-comma joiner used to render initial summary text server-side
 * before Alpine boots. Mirrors the JS join logic in
 * formatEnabledMethodsList / getSelectableMethodsList — same rules,
 * different language. Empty array returns ''; the caller decides
 * which copy to fall back to.
 *
 * Local function (no global pollution) so multiple instances of the
 * settings page in the same request don't trigger redefinition.
 */
if ( ! function_exists( 'affwp_payouts_tab_oxford_join' ) ) {
	/**
	 * Join an array of strings with Oxford commas and a final conjunction.
	 *
	 * @param array  $items       Items to join.
	 * @param string $conjunction Either 'or' (selectable list) or 'and' (enabled list).
	 * @return string Joined string, or empty when no items.
	 */
	function affwp_payouts_tab_oxford_join( array $items, string $conjunction ) {
		$items = array_values( $items );
		$count = count( $items );
		if ( 0 === $count ) {
			return '';
		}
		if ( 1 === $count ) {
			return $items[0];
		}
		if ( 2 === $count ) {
			return $items[0] . ' ' . $conjunction . ' ' . $items[1];
		}
		$last = array_pop( $items );
		return implode( ', ', $items ) . ', ' . $conjunction . ' ' . $last;
	}
}

/*
 * Pre-compute initial summary state for the Payout Settings card so the
 * first paint already contains the correct copy. Without this, the
 * <template x-if> branches stay empty until Alpine boots — visible as a
 * brief flash on every page load. The Alpine bindings still drive live
 * updates after boot; PHP just supplies the first frame.
 *
 * Computes three things: which branch is initially visible (no-choice /
 * single / multiple), and the dynamic label values each branch needs.
 */
$affiliate_selectable_method_ids = array_values(
	array_filter(
		$enabled_method_ids,
		function ( $id ) use ( $admin_only_method_ids ) {
			return ! in_array( $id, $admin_only_method_ids, true );
		}
	)
);
$affiliate_selectable_count = count( $affiliate_selectable_method_ids );

if ( ! $allow_selection || 0 === $affiliate_selectable_count ) {
	$initial_settings_branch = 'no-choice';
} elseif ( 1 === $affiliate_selectable_count ) {
	$initial_settings_branch = 'single';
} else {
	$initial_settings_branch = 'multiple';
}

$initial_default_method_label = isset( $all_method_labels[ $default_payout_method ] )
	? $all_method_labels[ $default_payout_method ]
	: $default_payout_method;

$initial_selectable_labels = array_map(
	function ( $id ) use ( $all_method_labels ) {
		return isset( $all_method_labels[ $id ] ) ? $all_method_labels[ $id ] : $id;
	},
	array_values(
		array_filter(
			$affiliate_selectable_method_ids,
			function ( $id ) use ( $default_payout_method ) {
				return $id !== $default_payout_method;
			}
		)
	)
);
$initial_selectable_methods_text = affwp_payouts_tab_oxford_join( $initial_selectable_labels, 'or' );

/*
 * Summary counts for the Payout Methods section header. The total
 * counts every visible method in the list (including 'Available' addons
 * the admin hasn't installed yet, since those still occupy rows). The
 * setup count is intentionally static (page-load snapshot) — accurate
 * setup_required state requires re-checking credentials per method,
 * which lives server-side. Toggling a method in the list updates the
 * enabled count live via Alpine, but a method moving from 'enabled, no
 * credentials' to 'setup required' still needs a save & reload to
 * reflect in this number. Acceptable: the enabled count is what
 * verify-mode admins glance at most.
 *
 * enabledCardMethodIds is a separate list from enabledMethodIds because
 * the two count different things: enabledMethodIds tracks the registry
 * (includes 'manual' — enabled but no card on the page); the methods
 * section summary needs to count only methods that have a card the user
 * can see. Without the split, the summary read '4 of 3 methods enabled'
 * because manual was being counted but had no row to enumerate against.
 */
$total_methods_count        = count( $payment_methods );
$enabled_card_method_ids    = array();
$setup_required_card_labels = array();
/*
 * Card-method-id-keyed label map for the Payout Methods summary. Uses
 * the short registry label ('Stripe', 'PayPal', 'Store Credit') rather
 * than the verbose card name ('Stripe Payouts', 'PayPal Payouts') so
 * the summary matches the Payout Settings summary's vocabulary one
 * section above.
 */
$card_method_labels = array();
foreach ( $payment_methods as $card_method_id => $method ) {
	$card_method_labels[ $card_method_id ] = isset( $all_method_labels[ $card_method_id ] )
		? $all_method_labels[ $card_method_id ]
		: $method['name'];

	// Setting key per method mirrors the same mapping in payment-method-card.php.
	$setting_key = '';
	if ( 'store_credit' === $card_method_id ) {
		$setting_key = 'store-credit';
	} elseif ( 'manual' === $card_method_id ) {
		$setting_key = 'manual_payouts';
	} elseif ( 'payouts-service' === $card_method_id ) {
		$setting_key = 'enable_payouts_service';
	} elseif ( 'paypal' === $card_method_id ) {
		$setting_key = 'paypal_payouts';
	} elseif ( 'stripe' === $card_method_id ) {
		$setting_key = 'stripe_payouts';
	}
	if ( $setting_key && affiliate_wp()->settings->get( $setting_key, false ) ) {
		$enabled_card_method_ids[] = $card_method_id;
	}

	// Collect names of methods needing configuration. Status is server-rendered, so this list is static until save and reload.
	if ( isset( $method['status'] ) && in_array( $method['status'], array( 'setup_required', 'installed' ), true ) ) {
		$setup_required_card_labels[] = $card_method_labels[ $card_method_id ];
	}
}

/*
 * Join the setup-required names with Oxford commas + "and" before the
 * last item. Same join logic used in getSelectableMethodsList() and
 * formatEnabledMethodsList() on the Alpine side; mirroring it here
 * keeps the cadence identical regardless of which list is being read.
 */
$setup_required_method_names_text = affwp_payouts_tab_oxford_join( $setup_required_card_labels, 'and' );
$setup_required_methods_count     = count( $setup_required_card_labels );

/*
 * Initial Payout Methods summary text — Oxford-joined list of enabled
 * card methods, rendered server-side so the first paint already contains
 * the correct copy. Same join logic as formatEnabledMethodsList() on the
 * Alpine side. With $enabled_card_method_ids defined now (the iteration
 * loop populated it above), this computation can use the card label map.
 */
$initial_enabled_labels = array_map(
	function ( $id ) use ( $card_method_labels ) {
		return isset( $card_method_labels[ $id ] ) ? $card_method_labels[ $id ] : $id;
	},
	$enabled_card_method_ids
);
$initial_enabled_methods_text = affwp_payouts_tab_oxford_join( $initial_enabled_labels, 'and' );
$initial_enabled_count        = count( $enabled_card_method_ids );
?>


<div class="relative z-10 py-8 max-w-4xl affwp-ui"
	@payout-method-toggled.window="onMethodToggled($event.detail)"
	x-data="{
	activeMethod: null,
	message: '',
	messageType: 'success',
	/*
	 * Shared state for the Payout Settings card. Lifted into the
	 * page-level x-data (instead of each row owning its own scope) so
	 * the summary text and the default-method select can react to
	 * changes — including toggles in the Payout Methods list below —
	 * without coupling rows together.
	 *
	 * wp_json_encode is wrapped in esc_attr so inner double quotes
	 * become &quot; in the attribute value; the HTML parser decodes
	 * them back to bare quotes before Alpine evaluates the expression.
	 * Without esc_attr the raw JSON would terminate the attribute
	 * mid-expression and the whole x-data scope would fail to parse.
	 */
	defaultMethod: '<?php echo esc_js( $default_payout_method ); ?>',
	allowSelection: <?php echo $allow_selection ? 'true' : 'false'; ?>,
	enabledMethodIds: <?php echo esc_attr( wp_json_encode( array_values( $enabled_method_ids ) ) ); ?>,
	enabledCardMethodIds: <?php echo esc_attr( wp_json_encode( array_values( $enabled_card_method_ids ) ) ); ?>,
	methodLabels: <?php echo esc_attr( wp_json_encode( $all_method_labels ) ); ?>,
	cardMethodLabels: <?php echo esc_attr( wp_json_encode( $card_method_labels ) ); ?>,
	adminOnlyMethodIds: <?php echo esc_attr( wp_json_encode( $admin_only_method_ids ) ); ?>,
	dismissedMismatchSnapshot: <?php echo esc_attr( wp_json_encode( (string) get_user_meta( get_current_user_id(), 'affwp_default_payout_mismatch_dismissed', true ) ) ); ?>,
	dismissNonce: '<?php echo esc_js( wp_create_nonce( 'affwp_dismiss_default_payout_mismatch_hint' ) ); ?>',
	selectionNudgeDismissed: <?php echo get_user_meta( get_current_user_id(), 'affwp_payout_selection_nudge_dismissed', true ) ? 'true' : 'false'; ?>,
	selectionNudgeNonce: '<?php echo esc_js( wp_create_nonce( 'affwp_dismiss_payout_selection_nudge' ) ); ?>',
	currentMismatchSnapshot() {
		const enabled = [...this.enabledMethodIds].sort();
		return JSON.stringify({ default: this.defaultMethod, enabled });
	},
	isMismatchDismissed() {
		return this.dismissedMismatchSnapshot === this.currentMismatchSnapshot();
	},
	dismissMismatchHint() {
		const snapshot = this.currentMismatchSnapshot();
		this.dismissedMismatchSnapshot = snapshot;
		const params = new URLSearchParams();
		params.append('action', 'affwp_dismiss_default_payout_mismatch_hint');
		params.append('_wpnonce', this.dismissNonce);
		params.append('snapshot', snapshot);
		fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: params });
	},
	dismissSelectionNudge() {
		this.selectionNudgeDismissed = true;
		const params = new URLSearchParams();
		params.append('action', 'affwp_dismiss_payout_selection_nudge');
		params.append('_wpnonce', this.selectionNudgeNonce);
		fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: params });
	},
	getActiveMethodTitle() {
		if (!this.activeMethod) return '';
		const methods = window.affwp_payouts?.methods || {};
		return methods[this.activeMethod]?.name || '';
	},
	/*
	 * Admin-context method list — drives the Default Payout Method
	 * <select>. Returns [{id, label}, ...] in enabledMethodIds order so
	 * <template x-for> can render <option>s reactively. Falls back to
	 * the id string when methodLabels doesn't have an entry, which
	 * shouldn't happen but keeps the select usable rather than blank.
	 */
	getEnabledAdminMethods() {
		return this.enabledMethodIds.map((id) => ({
			id,
			label: this.methodLabels[id] || id
		}));
	},
	getDefaultMethodLabel() {
		return this.methodLabels[this.defaultMethod] || this.defaultMethod;
	},
	/*
	 * Names of enabled methods that aren't Manual, joined with 'or'
	 * ('Stripe', 'Stripe or PayPal', 'Stripe, PayPal, or Store Credit').
	 * Used in the default-mismatch callout heading — 'or' because only
	 * one method can be the default. No literal double-quotes in here:
	 * this lives inside the page x-data attribute and a double-quote
	 * would truncate it.
	 */
	getNonManualMethodsList() {
		const labels = this.enabledMethodIds
			.filter((id) => id !== 'manual')
			.map((id) => this.methodLabels[id] || id);
		if (labels.length === 0) return '';
		if (labels.length === 1) return labels[0];
		if (labels.length === 2) return labels.join(' or ');
		return labels.slice(0, -1).join(', ') + ', or ' + labels[labels.length - 1];
	},
	/*
	 * Affiliate-selectable list for the summary footer. Filters out
	 * adminOnly methods (Manual, Payouts Service) and the current
	 * default (so the sentence doesn't read 'paid via PayPal by
	 * default, but can choose PayPal'). Joins with Oxford commas
	 * because 3+ methods is realistic once Store Credit lands.
	 */
	getSelectableMethodsList() {
		const labels = this.enabledMethodIds
			.filter((id) => !this.adminOnlyMethodIds.includes(id))
			.filter((id) => id !== this.defaultMethod)
			.map((id) => this.methodLabels[id] || id);
		if (labels.length === 0) return '';
		if (labels.length === 1) return labels[0];
		if (labels.length === 2) return labels.join(' or ');
		return labels.slice(0, -1).join(', ') + ', or ' + labels[labels.length - 1];
	},
	/*
	 * Total affiliate-selectable methods (registry IDs minus admin-only
	 * methods like Manual / Payouts Service). Drives the Payout Settings
	 * summary's three branches: 0 selectable (no choice for affiliates),
	 * exactly 1 (affiliate gets that method, no real choice), 2+ (true
	 * picker in the affiliate area). The previous two-branch version
	 * conflated 1-and-2+ into 'can choose X', which misled admins when
	 * only one method existed (no UI choice actually rendered).
	 */
	getAffiliateSelectableCount() {
		return this.enabledMethodIds
			.filter((id) => !this.adminOnlyMethodIds.includes(id))
			.length;
	},
	/*
	 * Enabled-methods list for the Payout Methods summary. Reactive to
	 * card toggles via enabledCardMethodIds. Same Oxford-comma + 'and'
	 * join logic the other list helpers use so all three section
	 * summaries read with identical cadence.
	 */
	formatEnabledMethodsList() {
		const labels = this.enabledCardMethodIds.map((id) => this.cardMethodLabels[id] || id);
		if (labels.length === 0) return '';
		if (labels.length === 1) return labels[0];
		if (labels.length === 2) return labels.join(' and ');
		return labels.slice(0, -1).join(', ') + ', and ' + labels[labels.length - 1];
	},
	/*
	 * Listener for the cards' enable/disable toggles. Card method_id
	 * and registry id share the same namespace post-2.33.0
	 * normalization, so no translation is needed here. If the admin
	 * disables the method that's currently selected as the default,
	 * fall through to the first remaining enabled method (or 'manual'
	 * if everything's off) — a no-options select would silently lose
	 * the user's selection and confuse them on save.
	 */
	onMethodToggled(detail) {
		if (!detail || !detail.methodId) return;
		const methodId = detail.methodId;
		if (detail.enabled) {
			if (!this.enabledMethodIds.includes(methodId)) {
				this.enabledMethodIds = [...this.enabledMethodIds, methodId];
			}
			if (!this.enabledCardMethodIds.includes(methodId)) {
				this.enabledCardMethodIds = [...this.enabledCardMethodIds, methodId];
			}
		} else {
			this.enabledMethodIds = this.enabledMethodIds.filter((id) => id !== methodId);
			this.enabledCardMethodIds = this.enabledCardMethodIds.filter((id) => id !== methodId);
			if (this.defaultMethod === methodId) {
				this.defaultMethod = this.enabledMethodIds[0] || 'manual';
			}
		}
	}
}">

	<!-- Skip Links -->
	<a href="#payment-methods" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-white focus:border focus:border-gray-300 focus:rounded-md focus:shadow-sm">
		<?php esc_html_e( 'Skip to payment methods', 'affiliate-wp' ); ?>
	</a>

	<?php
	// In-product feature discovery notice (eligibility-gated; renders inside the plugin's own
	// page container, never the global admin_notices zone).
	if ( function_exists( 'affwp_discovery_render_notice' ) ) {
		echo affwp_discovery_render_notice( 'intl_affiliate_payouts' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- returns pre-escaped component HTML (button/badge helpers escape; slot values escaped at build time).
	}
	?>

	<!--
		Payout Settings — heading + dynamic summary at the gray frame's
		top edge, white card with the controls below. Mirrors the
		calc-engine 'Available with Pro' container pattern: gray top
		holds the section identity (title + live status), white card
		holds the actionable rows. Putting the summary above the card
		(instead of as a footer) means an admin returning to verify the
		configuration sees the answer where their eye lands first, not
		after scrolling past three toggle rows.
	-->
	<div class="p-1 mb-6 bg-affwp-admin-bg rounded-xl border border-affwp-admin-border">
		<div class="px-5 sm:px-7 pt-4 pb-4">
			<h2 class="text-lg font-semibold text-gray-900 sm:text-xl">
				<?php esc_html_e( 'Payout Settings', 'affiliate-wp' ); ?>
			</h2>
			<p class="mt-1 text-sm text-gray-600">
				<!--
					Three states describe what affiliates actually experience:

					  - allow_choose OFF (or 0 selectable methods): routing
					    is admin-driven via Default Payout Method.
					  - allow_choose ON, exactly 1 selectable method:
					    the picker has nothing to offer — affiliates fall
					    through to the Default. The toggle is inert until
					    a second method is enabled. Copy makes this explicit
					    so admins don't think their toggle silently changed
					    routing.
					  - allow_choose ON, 2+ selectable methods: dropdown
					    renders, default is the pre-selection, can pick
					    alternates.
				-->
				<span x-show="!allowSelection || getAffiliateSelectableCount() === 0"
					<?php echo 'no-choice' === $initial_settings_branch ? '' : 'style="display:none"'; ?>>
					<?php esc_html_e( 'Affiliates are paid via', 'affiliate-wp' ); ?>
					<strong class="text-gray-700" x-text="getDefaultMethodLabel()"><?php echo esc_html( $initial_default_method_label ); ?></strong>
					<?php esc_html_e( 'by default. Admins can override per affiliate on the Edit Affiliate screen.', 'affiliate-wp' ); ?>
				</span>
				<span x-show="allowSelection && getAffiliateSelectableCount() === 1"
					<?php echo 'single' === $initial_settings_branch ? '' : 'style="display:none"'; ?>>
					<?php esc_html_e( 'Affiliates are paid via', 'affiliate-wp' ); ?>
					<strong class="text-gray-700" x-text="getDefaultMethodLabel()"><?php echo esc_html( $initial_default_method_label ); ?></strong>
					<?php esc_html_e( 'by default. The payout method picker won\'t appear in the Affiliate Area until you enable a second method. Admins can override per affiliate on the Edit Affiliate screen.', 'affiliate-wp' ); ?>
				</span>
				<span x-show="allowSelection && getAffiliateSelectableCount() > 1"
					<?php echo 'multiple' === $initial_settings_branch ? '' : 'style="display:none"'; ?>>
					<?php esc_html_e( 'Affiliates are paid via', 'affiliate-wp' ); ?>
					<strong class="text-gray-700" x-text="getDefaultMethodLabel()"><?php echo esc_html( $initial_default_method_label ); ?></strong>
					<?php esc_html_e( 'by default, but can choose', 'affiliate-wp' ); ?>
					<strong class="text-gray-700" x-text="getSelectableMethodsList()"><?php echo esc_html( $initial_selectable_methods_text ); ?></strong>
					<?php esc_html_e( 'in their Affiliate Area. Admins can override per affiliate on the Edit Affiliate screen.', 'affiliate-wp' ); ?>
				</span>
				<?php
				affwp_link(
					[
						'text'     => esc_html__( 'Learn more', 'affiliate-wp' ),
						'href'     => affwp_utm_link( 'https://affiliatewp.com/docs/payout-settings/', 'settings-payouts', 'Payout Settings Learn More' ),
						'icon'     => 'share-up-right',
						'external' => true,
					]
				);
				?>
			</p>
		</div>

		<div class="overflow-hidden bg-white rounded-lg border border-gray-900/10">

			<!-- Default Payout Method -->
			<div class="p-4 sm:p-5 lg:p-6 border-b border-gray-200">
				<label for="affwp-default-payout-method" class="block mb-2 text-base font-medium text-gray-900">
					<?php esc_html_e( 'Default Payout Method', 'affiliate-wp' ); ?>
				</label>
				<?php
				/*
				 * Inlined instead of affwp_select() because the option list
				 * needs to react to enable/disable toggles in the Payout
				 * Methods list below. Wrapper, select, and chevron classes
				 * mirror render_select() exactly so the visual treatment is
				 * identical to every other affwp_select on the page.
				 */
				?>
				<div class="grid grid-cols-1 affwp-ignore-select2 max-w-sm">
					<?php
					/*
					 * Deliberately NOT using x-model here. With <template x-for>
					 * the option list renders one tick after Alpine wires up
					 * the select, which means x-model finds zero options on
					 * its first sync pass and "corrects" defaultMethod to the
					 * select's empty value (which then falls back to the
					 * first option once options arrive). Switching to a
					 * one-way :selected binding per option plus an explicit
					 * @change handler avoids that race — the data drives the
					 * DOM, never the reverse on initial render.
					 */
					?>
					<select id="affwp-default-payout-method"
						name="affwp_settings[default_payout_method]"
						class="col-start-1 row-start-1 w-full pr-10 text-gray-900 bg-white rounded-lg border border-gray-300 transition-colors appearance-none affwp-ignore-select2 focus:outline-hidden focus:ring-2 focus:ring-affwp-brand-500 hover:border-gray-400 py-2 pl-4 text-base"
						@change="defaultMethod = $event.target.value">
						<template x-for="m in getEnabledAdminMethods()" :key="m.id">
							<option :value="m.id" :selected="m.id === defaultMethod" x-text="m.label"></option>
						</template>
					</select>
					<?php \AffiliateWP\Utils\Icons::render( 'chevron-down', [ 'class' => 'col-start-1 row-start-1 mr-3 text-gray-500 pointer-events-none self-center justify-self-end size-3.5' ] ); ?>
				</div>
				<p class="mt-2 max-w-prose text-sm text-gray-600">
					<?php esc_html_e( 'Every affiliate is paid via this method unless they pick a different one in their Affiliate Area (when allowed) or you override them on the Edit Affiliate screen.', 'affiliate-wp' ); ?>
				</p>

				<?php
				$mismatch_heading = sprintf(
					/* translators: %s: Readable list of enabled non-Manual payout methods (e.g. "Stripe or PayPal"). Rendered at runtime by Alpine. Phrased as a question so it doesn't presume Manual is the wrong default. */
					esc_html__( 'Use %s as your default payout method?', 'affiliate-wp' ),
					'<span x-text="getNonManualMethodsList()"></span>'
				);

				$mismatch_body = esc_html__( 'Your default is Manual, so affiliates are paid directly unless you override them or they pick another method.', 'affiliate-wp' );

				ob_start();
				?>
				<template x-for="(m, i) in getEnabledAdminMethods().filter(m =&gt; m.id !== 'manual')" :key="m.id">
					<span>
						<span x-show="i &gt; 0" class="text-yellow-700">&nbsp;·&nbsp;</span>
						<button type="button"
							class="font-medium text-yellow-900 underline decoration-yellow-500 underline-offset-4 cursor-pointer hover:decoration-yellow-700"
							@click="defaultMethod = m.id">
							<?php esc_html_e( 'Use', 'affiliate-wp' ); ?>&nbsp;<span x-text="m.label"></span>&nbsp;<?php esc_html_e( 'as default', 'affiliate-wp' ); ?>
						</button>
					</span>
				</template>
				<?php
				$mismatch_actions = ob_get_clean();

				?>
				<div class="mt-3">
					<?php
					affwp_notice(
						[
							'variant'       => 'tip',
							'heading'       => $mismatch_heading,
							'body'          => $mismatch_body,
							'actions'       => $mismatch_actions,
							'dismissable'   => true,
							'class'         => 'max-w-2xl',
							'attributes'    => [
								'x-show'  => "defaultMethod === 'manual' && getEnabledAdminMethods().filter(m => m.id !== 'manual').length > 0 && !isMismatchDismissed()",
								'x-cloak' => '',
							],
							'dismiss_attrs' => [
								'@click' => 'dismissMismatchHint()',
							],
						]
					);
					?>
				</div>
			</div>

			<!--
				Allow affiliates to choose. No nested x-data: `allowSelection`
				resolves up to the page-level scope so the summary footer
				stays in sync as this toggle flips.
			-->
			<div id="allow-affiliate-payout-method-selection" class="border-b border-gray-200">
				<div class="flex overflow-hidden gap-6 justify-between items-center p-4 sm:p-5 lg:p-6 cursor-pointer"
					@click="if ( ! window.getSelection().isCollapsed ) return; allowSelection = !allowSelection; $refs.toggle_allow_selection.querySelector('input[type=checkbox]').click()">
					<div class="flex-1 min-w-0">
						<span class="block mb-1 text-base font-medium text-gray-900">
							<?php esc_html_e( 'Allow affiliates to choose their payout method', 'affiliate-wp' ); ?>
						</span>
						<p class="max-w-prose text-sm text-gray-600">
							<?php esc_html_e( 'Show a payout method dropdown to affiliates when at least two methods are enabled.', 'affiliate-wp' ); ?>
						</p>
					</div>
					<div class="shrink-0" @click.stop x-ref="toggle_allow_selection">
						<?php
						affwp_toggle(
							array(
								'name'         => 'affwp_settings[allow_affiliate_payout_method_selection]',
								'label'        => __( 'Allow affiliates to choose their payout method', 'affiliate-wp' ),
								'checked'      => $allow_selection,
								'size'         => 'md',
								'color'        => 'blue',
								'alpine_model' => 'allowSelection',
							)
						);
						?>
					</div>
				</div>

				<?php
				/*
				 * Nudge: self-selection is opt-in (off by default). Once two or more affiliate-
				 * selectable methods exist, prompt the admin to turn it on. Reactive — hides the
				 * moment the toggle flips on or the method count drops below two.
				 */
				?>
				<div class="px-4 pb-5 sm:px-5 sm:pb-6 lg:px-6">
					<?php
					affwp_notice(
						array(
							'variant'       => 'tip',
							'heading'       => __( "Want affiliates to choose how they're paid?", 'affiliate-wp' ),
							'body'          => __( 'You have two or more payout methods enabled. Turn this option on to let each affiliate pick their own.', 'affiliate-wp' ),
							'dismissable'   => true,
							'class'         => 'max-w-2xl',
							'attributes'    => array(
								'x-show'  => "!allowSelection && getEnabledAdminMethods().filter(m => m.id !== 'manual').length >= 2 && !selectionNudgeDismissed",
								'x-cloak' => '',
							),
							'dismiss_attrs' => array(
								'@click' => 'dismissSelectionNudge()',
							),
						)
					);
					?>
				</div>
			</div>

			<!-- Auto-email affiliates about failed payouts (was a separate Notifications card). -->
		<div id="auto-email-payout-failures" class="border-b border-gray-200" x-data="{ autoEmailFailures: <?php echo $auto_email_payout_failures ? 'true' : 'false'; ?> }">
			<div class="flex overflow-hidden gap-6 justify-between items-center p-4 sm:p-5 lg:p-6 cursor-pointer"
				@click="if ( ! window.getSelection().isCollapsed ) return; autoEmailFailures = !autoEmailFailures; $refs.toggle_auto_email_failures.querySelector('input[type=checkbox]').click()">
				<div class="flex-1 min-w-0">
					<span class="block mb-1 text-base font-medium text-gray-900">
						<?php esc_html_e( 'Auto-email affiliates about failed payouts', 'affiliate-wp' ); ?>
					</span>
					<p class="max-w-prose text-sm text-gray-600">
						<?php esc_html_e( 'Email affiliates whose payout failed because they need to take action, like verifying their PayPal email.', 'affiliate-wp' ); ?>
					</p>
				</div>
				<div class="shrink-0" @click.stop x-ref="toggle_auto_email_failures">
					<?php
					affwp_toggle(
						array(
							'name'         => 'affwp_settings[auto_email_payout_failures]',
							'label'        => __( 'Auto-email affiliates about failed payouts', 'affiliate-wp' ),
							'checked'      => $auto_email_payout_failures,
							'size'         => 'md',
							'color'        => 'blue',
							'alpine_model' => 'autoEmailFailures',
						)
					);
					?>
				</div>
			</div>
		</div>

		<!--
			Auto-retry failed payouts. Companion to auto-email above. When ON,
			a daily background sweep re-attempts payouts whose affiliate has
			since completed the blocker (e.g. finished Stripe Connect). When
			OFF, failures sit until manually retried. Both default OFF on
			sites upgrading from 2.33 (see the v2.33.0 upgrade routine).
		-->
		<div id="auto-retry-failed-payouts" x-data="{ autoRetryFailures: <?php echo $auto_retry_failed_payouts ? 'true' : 'false'; ?> }">
			<div class="flex overflow-hidden gap-6 justify-between items-center p-4 sm:p-5 lg:p-6 cursor-pointer"
				@click="if ( ! window.getSelection().isCollapsed ) return; autoRetryFailures = !autoRetryFailures; $refs.toggle_auto_retry_failures.querySelector('input[type=checkbox]').click()">
				<div class="flex-1 min-w-0">
					<span class="block mb-1 text-base font-medium text-gray-900">
						<?php esc_html_e( 'Auto-retry failed payouts', 'affiliate-wp' ); ?>
					</span>
					<p class="max-w-prose text-sm text-gray-600">
						<?php esc_html_e( 'Automatically retry failed payouts once the affiliate has resolved the blocker.', 'affiliate-wp' ); ?>
					</p>
				</div>
				<div class="shrink-0" @click.stop x-ref="toggle_auto_retry_failures">
					<?php
					affwp_toggle(
						array(
							'name'         => 'affwp_settings[auto_retry_failed_payouts]',
							'label'        => __( 'Auto-retry failed payouts', 'affiliate-wp' ),
							'checked'      => $auto_retry_failed_payouts,
							'size'         => 'md',
							'color'        => 'blue',
							'alpine_model' => 'autoRetryFailures',
						)
					);
					?>
				</div>
			</div>
		</div>

		</div>

	</div>

	<!--
		Payout Methods — same gray-frame-with-summary pattern as Payout
		Settings above. Cards remain individual (each one needs its own
		expandable settings panel, which would degrade visually inside a
		shared parent card), but the surrounding frame gives the section
		an identity block at the top so admins reading the page get the
		'what's the state of this' answer in one glance.

		The enabled count uses Alpine (reactive to method toggles); the
		'needs setup' clause is server-rendered because it depends on
		per-method credential checks that don't have a clean Alpine
		equivalent. Updates on save & reload, which is the actual moment
		a setup_required state would change anyway.
	-->
	<div class="p-1 mb-6 bg-affwp-admin-bg rounded-xl border border-affwp-admin-border">
		<div class="px-5 sm:px-7 pt-4 pb-4">
			<h2 class="text-lg font-semibold text-gray-900 sm:text-xl">
				<?php esc_html_e( 'Payout Methods', 'affiliate-wp' ); ?>
			</h2>
			<p class="mt-1 text-sm text-gray-600">
				<!--
					Two summary states, plus inline is/are switch:
					  - 0 enabled: empty state + manual fallback.
					  - 1+ enabled: name the methods by name (reactive).
					  - Setup-required clause appended when applicable
					    (server-rendered, since status depends on
					    credential checks that don't have a clean Alpine
					    equivalent).

					Initial visibility uses inline `style` so the right
					branch shows immediately; Alpine's x-show takes over
					after boot. The Oxford-joined enabled list is also
					pre-rendered into the <strong> tag for first paint.
				-->
				<span x-show="enabledCardMethodIds.length === 0"
					<?php echo 0 === $initial_enabled_count ? '' : 'style="display:none"'; ?>>
					<?php esc_html_e( 'No payout methods enabled. Payouts can still be processed manually.', 'affiliate-wp' ); ?>
				</span>
				<span x-show="enabledCardMethodIds.length > 0"
					<?php echo $initial_enabled_count > 0 ? '' : 'style="display:none"'; ?>>
					<strong class="text-gray-700" x-text="formatEnabledMethodsList()"><?php echo esc_html( $initial_enabled_methods_text ); ?></strong>
					<span x-show="enabledCardMethodIds.length === 1"
						<?php echo 1 === $initial_enabled_count ? '' : 'style="display:none"'; ?>><?php esc_html_e( 'is enabled.', 'affiliate-wp' ); ?></span>
					<span x-show="enabledCardMethodIds.length > 1"
						<?php echo $initial_enabled_count > 1 ? '' : 'style="display:none"'; ?>><?php esc_html_e( 'are enabled.', 'affiliate-wp' ); ?></span>
					<?php if ( '' !== $setup_required_method_names_text ) : ?>
						<strong class="text-gray-700"><?php echo esc_html( $setup_required_method_names_text ); ?></strong>
						<?php
						echo esc_html(
							_n(
								'needs setup.',
								'need setup.',
								$setup_required_methods_count,
								'affiliate-wp'
							)
						);
						?>
					<?php endif; ?>
				</span>
			</p>
		</div>

		<!--
			Payment methods list. No extra horizontal/bottom padding on
			the container — the outer wrapper's p-1 already provides the
			4px gutter to the frame edge, matching the inner white card
			of the Payout Settings section above. Adding px-1 here would
			double-pad and push the cards 8px from the frame edge (4px
			outer + 4px inner) while Payout Settings sits at 4px. The
			eye reads the difference as inconsistency between the two
			peer sections; removing the inner padding makes the edges
			line up across both gray frames.
		-->
		<div id="payment-methods" class="space-y-1" role="region" aria-label="<?php esc_attr_e( 'Payment Methods', 'affiliate-wp' ); ?>">
			<?php foreach ( $payment_methods as $method_id => $method ) : ?>
				<?php include 'payment-method-card.php'; ?>
			<?php endforeach; ?>
		</div>
	</div>


	<!-- Success/Error Messages -->
	<div x-show="message"
		x-cloak
		x-transition:enter="transition ease-out duration-300"
		x-transition:enter-start="opacity-0 transform translate-y-2"
		x-transition:enter-end="opacity-100 transform translate-y-0"
		x-transition:leave="transition ease-in duration-200"
		x-transition:leave-start="opacity-100"
		x-transition:leave-end="opacity-0"
		class="fixed right-4 bottom-4 left-4 z-50 mx-auto max-w-sm sm:left-auto sm:mx-0"
		role="status"
		aria-live="polite"
		aria-atomic="true">
		<div class="p-4 rounded-md shadow-lg"
			:class="messageType === 'success' ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
			<div class="flex">
				<div class="flex-shrink-0">
					<svg x-show="messageType === 'success'" class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg x-show="messageType === 'error'" class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
					</svg>
				</div>
				<div class="ml-3">
					<p class="text-sm font-medium"
						:class="messageType === 'success' ? 'text-green-800' : 'text-red-800'"
						x-text="message"></p>
				</div>
			</div>
		</div>
	</div>

</div>
