<?php
/**
 * Admin: Preview Payout View
 *
 * @package    AffiliateWP
 * @subpackage Admin/Payouts
 * @copyright  Copyright (c) 2019, Sandhills Development, LLC
 * @license    http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since      2.4
 */

// Multi-select inputs (2.33.0+). Each accepts an array of IDs/keys.
$include_affiliates    = isset( $_REQUEST['include_affiliates'] ) && is_array( $_REQUEST['include_affiliates'] )
	? array_values( array_filter( array_map( 'absint', $_REQUEST['include_affiliates'] ) ) )
	: array();
$include_groups        = isset( $_REQUEST['include_groups'] ) && is_array( $_REQUEST['include_groups'] )
	? array_values( array_filter( array_map( 'absint', $_REQUEST['include_groups'] ) ) )
	: array();
$payout_methods_filter = isset( $_REQUEST['payout_methods'] ) && is_array( $_REQUEST['payout_methods'] )
	? array_values( array_filter( array_map( 'sanitize_key', $_REQUEST['payout_methods'] ) ) )
	: array();

$start = ! empty( $_REQUEST['from'] ) ? sanitize_text_field( $_REQUEST['from'] ) : false;
$end   = ! empty( $_REQUEST['to'] ) ? sanitize_text_field( $_REQUEST['to'] ) : false;

// Legacy single-affiliate field — only used when the new arrays are empty.
$user_name = ! empty( $_REQUEST['user_name'] ) ? sanitize_text_field( $_REQUEST['user_name'] ) : false;
if ( $user_name && empty( $include_affiliates ) ) {
	$legacy_affiliate = affwp_get_affiliate( $user_name );
	if ( $legacy_affiliate ) {
		$include_affiliates[] = (int) $legacy_affiliate->ID;
	}
}

/*
 * Plural `payout_methods[]` (the canonical 2.33.0+ input from the chips) always means filter by
 * each affiliate's configured method, never a batch-level override.
 *
 * Singular `payout_method` is the legacy entry point (custom integrations, saved bookmarks,
 * older addons). It still means override: fire the per-method addon hooks and treat the whole
 * batch as that method. We only honor it when the plural array is absent, so a new-form
 * submission with one chip selected stays in filter mode.
 */
$legacy_method = ( empty( $payout_methods_filter ) && ! empty( $_REQUEST['payout_method'] ) )
	? strtolower( sanitize_text_field( $_REQUEST['payout_method'] ) )
	: '';

if ( '' !== $legacy_method ) {
	$payout_method         = array_key_exists( $legacy_method, affwp_get_payout_methods() ) ? $legacy_method : 'manual';
	$per_affiliate_routing = false;
} else {
	$payout_method         = '';
	$per_affiliate_routing = true;
}

$payout_method_label = $per_affiliate_routing
	? __( "Each affiliate's configured method", 'affiliate-wp' )
	: affwp_get_payout_method_display_label( $payout_method );

// Expand selected groups to their affiliate IDs.
$group_affiliate_ids = array();
foreach ( $include_groups as $group_id ) {
	if ( 'affiliate-group' !== affiliate_wp()->groups->get_group_type( $group_id ) ) {
		continue;
	}
	foreach ( (array) affiliate_wp()->connections->get_connected( 'affiliate', 'group', $group_id ) as $aid ) {
		if ( affwp_get_affiliate( $aid ) ) {
			$group_affiliate_ids[] = (int) $aid;
		}
	}
}

$affiliate_id_filter = array_values( array_unique( array_merge( $include_affiliates, $group_affiliate_ids ) ) );
$affiliate_id_arg    = ! empty( $affiliate_id_filter ) ? $affiliate_id_filter : false;

// The minimum payout amount.
$minimum = ! empty( $_REQUEST['minimum'] ) ? sanitize_text_field( affwp_sanitize_amount( $_REQUEST['minimum'] ) ) : 0;

$bypass_holding = absint( filter_input( INPUT_GET, 'bypass_holding', FILTER_SANITIZE_NUMBER_INT ) );

$preview_data = affwp_build_payout_preview_data(
	array(
		'affiliate_id'          => $affiliate_id_arg,
		'date'                  => array(
			'start' => $start,
			'end'   => $end,
		),
		'minimum'               => (float) $minimum,
		'bypass_holding'        => (bool) $bypass_holding,
		'apply_addon_filters'   => true,
		/*
		 * 'id' (integer IDs) instead of 'full' (4-key arrays per referral). The only render-site
		 * consumer is `count( $payout['referrals'] )` in affwp_render_preview_payout_row(), which works
		 * on either shape. 'full' would attach date_i18n() and wp_trim_words() per referral on every
		 * page load, a hot path on large datasets.
		 */
		'referral_shape'        => 'id',
		'per_affiliate_routing' => $per_affiliate_routing,
		'payout_method'         => $payout_method,
		'method_filter'         => $payout_methods_filter,
	)
);

$payouts             = $preview_data['payouts'];
$invalid_affiliates  = $preview_data['invalid_affiliates'];
$referrals_total     = $preview_data['referrals_total'];
$payouts_by_method   = $preview_data['payouts_by_method'];
$payouts_count       = $preview_data['payouts_count'];
$going_out_total     = $preview_data['going_out_total'];
$going_out_count     = $preview_data['going_out_count'];
$grand_ready_count   = $preview_data['grand_ready_count'];
$grand_invite_count  = $preview_data['grand_invite_count'];
$grand_blocked_count = $preview_data['grand_blocked_count'];
$grand_blocked_total = $preview_data['grand_blocked_total'];

// Per-method liquidity pre-flight (Stripe balance). Shared with the bulk-pay drawer.
$method_warnings = affwp_get_payout_preview_method_warnings( $payouts_by_method );

$has_blocking_warning = false;
foreach ( $method_warnings as $w ) {
	if ( 'blocking' === $w['severity'] ) {
		$has_blocking_warning = true;
		break;
	}
}

/*
 * First-row cap before the "Show more" expander kicks in. A section at or under the cap (plus a
 * few rows of `$preview_row_snap` headroom) renders in full with no button; a larger section
 * starts with this many rows server-rendered and loads the rest on demand via AJAX in batches of
 * `$method_load_more_batch`. Capping the initial render avoids a multi-second freeze on large
 * datasets where Alpine init over tens of thousands of hidden DOM nodes blocks the main thread.
 */
$method_row_cap         = 25;
$method_load_more_batch = 100;

// Same cap for the "Cannot be paid this run" section, which previously rendered every invalid affiliate up-front (unbounded).
$invalid_row_cap         = 25;
$invalid_load_more_batch = 100;

// Render a section in full when at most this many rows would otherwise hide behind the button.
$preview_row_snap = 3;

$invalid_count = count( $invalid_affiliates );

// Prefetch all server-rendered affiliates in a single query, cutting the initial render from N+1 to roughly two queries (affiliates table plus users via cache_users()).
$initial_render_ids = array();
foreach ( $payouts_by_method as $m ) {
	$visible_count      = affwp_get_preview_visible_count( count( $m['payouts'] ), $method_row_cap, $preview_row_snap );
	$visible_for_method = array_slice( $m['payouts'], 0, $visible_count, true );
	foreach ( array_keys( $visible_for_method ) as $aid ) {
		$initial_render_ids[] = (int) $aid;
	}
}
$initial_invalid_count = affwp_get_preview_visible_count( count( $invalid_affiliates ), $invalid_row_cap, $preview_row_snap );
$initial_invalid_ids   = array_slice( array_keys( $invalid_affiliates ), 0, $initial_invalid_count, true );
$preview_lookup        = affwp_preload_preview_payout_affiliates(
	array_merge( $initial_render_ids, $initial_invalid_ids )
);

$start_display = $start ? affwp_date_i18n( strtotime( $start ) ) : __( 'Earliest unpaid', 'affiliate-wp' );
$end_display   = $end ? affwp_date_i18n( strtotime( $end ) ) : affwp_date_i18n( strtotime( gmdate( 'm/d/Y' ) ) );

$edit_filters_args = array( 'action' => 'new_payout' );

if ( $start ) {
	$edit_filters_args['from'] = $start;
}
if ( $end ) {
	$edit_filters_args['to'] = $end;
}
if ( $user_name ) {
	$edit_filters_args['user_name'] = $user_name;
}
if ( ! empty( $include_affiliates ) ) {
	$edit_filters_args['include_affiliates'] = $include_affiliates;
}
if ( ! empty( $include_groups ) ) {
	$edit_filters_args['include_groups'] = $include_groups;
}
if ( ! empty( $payout_methods_filter ) ) {
	$edit_filters_args['payout_methods'] = $payout_methods_filter;
}
if ( '' !== $legacy_method ) {
	$edit_filters_args['payout_method'] = $legacy_method;
}
if ( $minimum ) {
	$edit_filters_args['minimum'] = $minimum;
}
if ( $bypass_holding ) {
	$edit_filters_args['bypass_holding'] = 1;
}

$edit_filters_url = affwp_admin_url( 'payouts', $edit_filters_args );

$async_capable = $per_affiliate_routing || 'manual' === $payout_method;
?>

<div class="wrap affwp-payout-page">

	<div class="affwp-ui">

		<header class="my-6 max-w-5xl flex flex-wrap items-end justify-between gap-6">
			<div>
				<?php
				/*
				 * Step rail: numbered indicator that previews the two-step flow at a glance.
				 * Step 1 (Configure) is checked-out and gray. Step 2 (Preview) is current —
				 * filled brand circle, brand-colored label.
				 */
				?>
				<nav aria-label="<?php esc_attr_e( 'Payout steps', 'affiliate-wp' ); ?>" class="inline-flex items-center gap-3 text-xs font-semibold uppercase tracking-wide">
					<a href="<?php echo esc_url( $edit_filters_url ); ?>" class="group inline-flex items-center gap-1.5 text-gray-500 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-affwp-brand-500 rounded-sm">
						<span class="inline-flex size-5 items-center justify-center rounded-full ring-1 ring-gray-300 text-[11px] font-bold leading-5 tabular-nums group-hover:ring-gray-400">1</span>
						<?php esc_html_e( 'Configure', 'affiliate-wp' ); ?>
					</a>
					<span class="text-gray-300" aria-hidden="true">→</span>
					<span class="inline-flex items-center gap-1.5 text-affwp-brand-700">
						<span aria-current="step" class="inline-flex size-5 items-center justify-center rounded-full bg-affwp-brand-500 text-white text-[11px] font-bold leading-5 tabular-nums">2</span>
						<?php esc_html_e( 'Preview', 'affiliate-wp' ); ?>
					</span>
				</nav>
				<h1 class="mt-3 text-3xl font-semibold tracking-tight m-0">
					<?php
					if ( $payouts_count > 0 ) {
						printf(
							esc_html(
								/* translators: %d: number of affiliates to be paid */
								_n( 'Pay %d affiliate', 'Pay %d affiliates', $payouts_count, 'affiliate-wp' )
							),
							(int) $payouts_count
						);
					} else {
						esc_html_e( 'Preview payout', 'affiliate-wp' );
					}
					?>
				</h1>
			</div>
			<div class="flex flex-wrap items-center gap-2 text-sm/5">
				<span class="inline-flex items-center rounded-full bg-white px-2.5 py-1 text-gray-600 ring-1 ring-gray-200">
					<?php
					echo esc_html(
						sprintf(
							/* translators: 1: period start, 2: period end */
							__( '%1$s → %2$s', 'affiliate-wp' ),
							$start_display,
							$end_display
						)
					);
					?>
				</span>
				<a href="<?php echo esc_url( $edit_filters_url ); ?>" class="inline-flex items-center gap-1.5 rounded-full bg-white px-2.5 py-1 font-medium text-gray-900 ring-1 ring-gray-200 hover:bg-gray-50">
					<?php echo \AffiliateWP\Utils\Icons::get( 'chevron-left', [ 'class' => 'size-3.5 text-gray-600 shrink-0' ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					<?php esc_html_e( 'Edit filters', 'affiliate-wp' ); ?>
				</a>
			</div>
		</header>

		<?php if ( ! empty( $payouts ) ) : ?>

			<?php
			echo affwp_render_payout_preview_body(
				$preview_data,
				array(
					'context'                 => 'page',
					'deep_link'               => true,
					'row_cap'                 => $method_row_cap,
					'load_more_batch'         => $method_load_more_batch,
					'invalid_row_cap'         => $invalid_row_cap,
					'row_snap'                => $preview_row_snap,
					'load_more'               => true,
					'method_warnings'         => $method_warnings,
					'lookup'                  => $preview_lookup,
					'after_total_hook_method' => $payout_method,
				)
			); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- renderer outputs escaped markup.
			?>

		<?php else : ?>

			<?php
			// Stuck-referrals hint: when nothing's eligible and no invalid set, surface unpaid referrals locked to non-terminal payouts so the admin knows resolution lives in the Payouts list, not here.
			$stuck_referrals_count = 0;
			$stuck_payouts_count   = 0;
			if ( empty( $payouts ) && empty( $invalid_affiliates ) ) {
				global $wpdb;
				$referrals_table = affiliate_wp()->referrals->table_name;
				$payouts_table   = affiliate_wp()->affiliates->payouts->table_name;

				$where  = array( 'r.status = %s', 'r.payout_id != 0', 'p.status IN (%s, %s)' );
				$params = array( 'unpaid', 'processing', 'failed' );

				if ( ! empty( $affiliate_id_filter ) ) {
					$placeholders = implode( ',', array_fill( 0, count( $affiliate_id_filter ), '%d' ) );
					$where[]      = "r.affiliate_id IN ({$placeholders})";
					foreach ( $affiliate_id_filter as $aid ) {
						$params[] = (int) $aid;
					}
				}

				if ( ! empty( $start ) ) {
					$where[]  = 'r.date >= %s';
					$params[] = gmdate( 'Y-m-d 00:00:00', strtotime( $start ) );
				}

				if ( ! empty( $end ) ) {
					$where[]  = 'r.date <= %s';
					$params[] = gmdate( 'Y-m-d 23:59:59', strtotime( $end ) );
				}

				$sql = sprintf(
					'SELECT COUNT(*) AS stuck_referrals, COUNT(DISTINCT r.payout_id) AS stuck_payouts FROM %s r INNER JOIN %s p ON r.payout_id = p.payout_id WHERE %s',
					$referrals_table,
					$payouts_table,
					implode( ' AND ', $where )
				);

				$row = $wpdb->get_row(
					// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table names hard-coded; param placeholders bound below.
					$wpdb->prepare( $sql, $params )
				);

				if ( $row ) {
					$stuck_referrals_count = (int) $row->stuck_referrals;
					$stuck_payouts_count   = (int) $row->stuck_payouts;
				}
			}
			?>

			<?php if ( $stuck_referrals_count > 0 ) : ?>
				<?php
				$stuck_payouts_url_args = array( 'status' => 'failed' );
				// Propagate the single-affiliate filter when applicable; the list table only accepts one.
				if ( count( $affiliate_id_filter ) === 1 ) {
					$stuck_payouts_url_args['affiliate_id'] = (int) $affiliate_id_filter[0];
				}
				$stuck_payouts_url = affwp_admin_url( 'payouts', $stuck_payouts_url_args );
				?>
				<div class="max-w-5xl mb-4 rounded-md bg-amber-50 ring-1 ring-amber-200 px-4 py-3 text-sm/6 text-amber-900">
					<p class="m-0">
						<strong>
							<?php
							printf(
								esc_html(
									/* translators: 1: number of stuck referrals, 2: number of stuck payouts */
									_n(
										'%1$d referral is stuck in %2$d failed or processing payout.',
										'%1$d referrals are stuck in %2$d failed or processing payouts.',
										$stuck_referrals_count,
										'affiliate-wp'
									)
								),
								(int) $stuck_referrals_count,
								(int) $stuck_payouts_count
							);
							?>
						</strong>
						<?php
						printf(
							/* translators: %s: link to the Payouts list filtered by failed status */
							wp_kses(
								__( 'Resolve them from the <a href="%s">Payouts list</a> to free them for the next batch.', 'affiliate-wp' ),
								array( 'a' => array( 'href' => array() ) )
							),
							esc_url( $stuck_payouts_url )
						);
						?>
					</p>
				</div>
			<?php endif; ?>

			<div class="max-w-5xl rounded-xl bg-white ring-1 ring-gray-200 px-6 py-12 text-center">
				<?php if ( ! empty( $invalid_affiliates ) ) : ?>
					<h2 class="text-base/6 font-semibold tracking-tight m-0"><?php esc_html_e( 'No affiliates can be paid this run', 'affiliate-wp' ); ?></h2>
					<p class="mt-2 text-sm/5 text-gray-600 m-0">
						<?php
						echo esc_html(
							sprintf(
								/* translators: %d: number of affiliates */
								_n( '%d affiliate is blocked. Review the reasons below and edit the filters to retry.', '%d affiliates are blocked. Review the reasons below and edit the filters to retry.', $invalid_count, 'affiliate-wp' ),
								(int) $invalid_count
							)
						);
						?>
					</p>
				<?php else : ?>
					<h2 class="text-base/6 font-semibold tracking-tight m-0"><?php esc_html_e( 'No referrals match these filters', 'affiliate-wp' ); ?></h2>
					<p class="mt-2 text-sm/5 text-gray-600 m-0">
						<?php
						printf(
							/* translators: %s: link to referrals screen */
							esc_html__( 'Adjust the period or filters and try again, or view all %s.', 'affiliate-wp' ),
							'<a class="text-gray-700 underline underline-offset-2 hover:no-underline" href="' . esc_url( affwp_admin_url( 'referrals' ) ) . '">' . esc_html__( 'referrals', 'affiliate-wp' ) . '</a>'
						);
						?>
					</p>
				<?php endif; ?>
			</div>

			<?php if ( ! empty( $invalid_affiliates ) ) : ?>
				<?php
				$invalid_visible_count = affwp_get_preview_visible_count( $invalid_count, $invalid_row_cap, $preview_row_snap );
				$invalid_visible       = array_slice( $invalid_affiliates, 0, $invalid_visible_count, true );
				$invalid_overflow      = max( 0, $invalid_count - $invalid_visible_count );
				// The empty-state branch has no prior prefetch, so warm just the affiliates the visible cap will render.
				$empty_state_lookup    = affwp_preload_preview_payout_affiliates( array_keys( $invalid_visible ) );
				?>
				<section id="affwp-invalid-affiliates" data-deeplink-highlight="ring-2! ring-affwp-brand-500! ring-offset-2 transition-all" class="mt-6 max-w-5xl rounded-xl bg-white ring-1 ring-gray-200 overflow-hidden scroll-mt-24">
					<div role="table" class="text-sm/6" aria-label="<?php esc_attr_e( 'Affiliates that cannot be paid this run', 'affiliate-wp' ); ?>">
						<div role="rowgroup" class="divide-y divide-gray-200" data-affwp-preview-rows>
							<?php foreach ( $invalid_visible as $affiliate_id => $status ) : ?>
								<?php echo affwp_render_preview_invalid_row( $affiliate_id, $status, $empty_state_lookup ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper outputs escaped markup. ?>
							<?php endforeach; ?>
						</div>

						<?php if ( $invalid_overflow > 0 ) : ?>
							<button
								type="button"
								class="affwp-preview-load-more w-full bg-gray-50 hover:bg-gray-100 px-6 py-2 text-center cursor-pointer transition-colors border-t border-gray-200 disabled:cursor-wait disabled:opacity-60"
								data-section="invalid"
								data-offset="<?php echo esc_attr( (int) $invalid_visible_count ); ?>"
								data-remaining="<?php echo esc_attr( (int) $invalid_overflow ); ?>"
								data-batch="<?php echo esc_attr( (int) $invalid_load_more_batch ); ?>"
								aria-busy="false"
							>
								<span class="inline-flex items-center gap-1.5 text-sm/5 font-medium text-gray-600">
									<?php \AffiliateWP\Utils\Icons::render( 'chevron-down', [ 'class' => 'size-3.5 text-gray-400 shrink-0' ] ); ?>
									<span data-affwp-load-more-label>
										<?php echo esc_html( affwp_get_preview_load_more_label( $invalid_overflow, $invalid_load_more_batch ) ); ?>
									</span>
									<span data-affwp-load-more-busy hidden><?php esc_html_e( 'Loading…', 'affiliate-wp' ); ?></span>
								</span>
							</button>
						<?php endif; ?>
					</div>
				</section>
			<?php endif; ?>

		<?php endif; ?>

	</div>

	<?php if ( ! empty( $payouts ) ) : ?>
		<?php
		$submit_form_id = $async_capable ? 'affwp-process-payout-async-form' : 'affwp-new-payout-form';
		?>
		<?php // Forms with hidden inputs sit outside the bar (zero visual footprint) so the bar's button can submit either via the `form=` attribute. Keeping the markup unchanged from the dark-island version. ?>
		<?php if ( $async_capable ) : ?>
			<form method="post" action="<?php echo esc_url( affwp_admin_url( 'payouts' ) ); ?>" id="<?php echo esc_attr( $submit_form_id ); ?>" class="hidden">
				<input type="hidden" name="affwp_action" value="process_payout_async" />
				<input type="hidden" name="user_name" value="<?php echo esc_attr( $user_name ); ?>" />
				<input type="hidden" name="from" value="<?php echo esc_attr( $start ); ?>" />
				<input type="hidden" name="to" value="<?php echo esc_attr( $end ); ?>" />
				<input type="hidden" name="bypass_holding" value="<?php echo esc_attr( $bypass_holding ); ?>" />
				<input type="hidden" name="minimum" value="<?php echo esc_attr( $minimum ); ?>" />
				<?php foreach ( $include_affiliates as $aff_id ) : ?>
					<input type="hidden" name="include_affiliates[]" value="<?php echo (int) $aff_id; ?>" />
				<?php endforeach; ?>
				<?php foreach ( $include_groups as $grp_id ) : ?>
					<input type="hidden" name="include_groups[]" value="<?php echo (int) $grp_id; ?>" />
				<?php endforeach; ?>
				<?php foreach ( $payout_methods_filter as $m_key ) : ?>
					<input type="hidden" name="payout_methods[]" value="<?php echo esc_attr( $m_key ); ?>" />
				<?php endforeach; ?>
				<?php if ( '' !== $payout_method ) : ?>
					<?php // Batch-level override ("Pay everyone via X"). Carry the singular method through so the async batch records it instead of falling back to per-affiliate routing. ?>
					<input type="hidden" name="payout_method" value="<?php echo esc_attr( $payout_method ); ?>" />
				<?php endif; ?>
				<?php wp_nonce_field( 'affwp_process_payout_async', 'affwp_process_payout_async_nonce' ); ?>
			</form>
		<?php else : ?>
			<form method="post" id="<?php echo esc_attr( $submit_form_id ); ?>" class="hidden">
				<input type="hidden" name="user_name" value="<?php echo esc_attr( $user_name ); ?>" />
				<input type="hidden" name="from" value="<?php echo esc_attr( $start ); ?>" />
				<input type="hidden" name="to" value="<?php echo esc_attr( $end ); ?>" />
				<input type="hidden" name="minimum" value="<?php echo esc_attr( $minimum ); ?>" />
				<input type="hidden" name="payout_method" value="<?php echo esc_attr( $payout_method ); ?>" />
				<input type="hidden" name="bypass_holding" value="<?php echo esc_attr( $bypass_holding ); ?>" />
				<input type="hidden" name="affwp_action" value="new_payout" />
				<?php wp_nonce_field( 'affwp_new_payout_nonce', 'affwp_new_payout_nonce' ); ?>
			</form>
		<?php endif; ?>

		<?php
		/*
		 * Full-width white sticky bar (Step 2). Mirrors the Step 1 treatment:
		 * fixed to viewport bottom across the WP admin content column, status on
		 * the left, primary action on the right. Status reads "<total> · N
		 * affiliates" (this run's summary). The button branches three ways:
		 *
		 *   - blocking warning (e.g. Stripe shortfall): red "Resolve issue to
		 *     continue" anchor that scrolls to the offending method section.
		 *   - async-capable batches: opens the confirm-payout modal, which then
		 *     POSTs to the hidden process_payout_async form above.
		 *   - sync batches: same modal flow, hidden new_payout form gets submitted.
		 */
		?>
		<?php
		/*
		 * Pay Affiliates action bar. The shell (gutters, max-w-5xl column, fixed positioning, shadow)
		 * comes from partials/payout-action-bar.php, shared with the New Payout screen. Render the
		 * status and action regions, then hand them to the shell.
		 */
		ob_start();
		?>
				<div class="text-sm/5 min-w-0">
					<p class="m-0 tabular-nums">
						<span class="font-medium text-gray-900">
							<?php
							echo esc_html(
								sprintf(
									/* translators: 1: total amount, 2: number of affiliates going out */
									_n(
										'%1$s · %2$d affiliate',
										'%1$s · %2$d affiliates',
										$going_out_count,
										'affiliate-wp'
									),
									affwp_currency_filter( affwp_format_amount( $going_out_total ) ),
									(int) $going_out_count
								)
							);
							?>
						</span>
					</p>
					<?php if ( $has_blocking_warning ) : ?>
						<p class="mt-0.5 m-0 text-xs text-red-800">
							<?php esc_html_e( "Stripe balance shortfall — top up in Stripe before processing this batch.", 'affiliate-wp' ); ?>
						</p>
					<?php endif; ?>
				</div>
		<?php
		$action_bar_status = ob_get_clean();

		ob_start();
		?>
				<?php if ( $has_blocking_warning ) : ?>
					<a href="#affwp-method-<?php echo esc_attr( sanitize_html_class( array_key_first( $method_warnings ) ) ?: 'method' ); ?>" class="inline-flex w-full justify-center sm:w-auto sm:justify-start shrink-0 items-center gap-1.5 rounded-md bg-red-600 px-5 py-2.5 text-base font-semibold text-white hover:bg-red-700 cursor-pointer whitespace-nowrap">
						<?php esc_html_e( 'Resolve issue to continue', 'affiliate-wp' ); ?>
					</a>
				<?php else : ?>
					<button type="button" @click="$store.modals.open('affwp-confirm-payout-modal')" class="inline-flex w-full justify-center sm:w-auto sm:justify-start shrink-0 items-center gap-1.5 rounded-md bg-affwp-brand-500 px-5 py-2.5 text-base font-semibold text-white hover:bg-affwp-brand-600 cursor-pointer whitespace-nowrap">
						<?php esc_html_e( 'Pay affiliates', 'affiliate-wp' ); ?>
						<?php echo \AffiliateWP\Utils\Icons::get( 'chevron-right', [ 'class' => 'size-4' ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</button>
				<?php endif; ?>
		<?php
		$action_bar_action = ob_get_clean();

		require __DIR__ . '/partials/payout-action-bar.php';
		?>

		<?php
		/*
		 * Minimal confirmation: amount and affiliate count form the title question; irreversibility is
		 * inlined only when the run includes external transfers (PayPal/Stripe/manual). Store-credit-only
		 * runs are adjustable in-AffiliateWP, so the warning is dropped there. Conditional callouts
		 * (PayPal invites' 30-day clawback, blocked affiliates) stay either way.
		 */
		$confirm_modal_title_id = 'modal-affwp-confirm-payout-modal-title';

		$confirm_modal_title = sprintf(
			/* translators: 1: total payout amount with currency, 2: formatted affiliate count */
			_n(
				'Pay %1$s to %2$s affiliate?',
				'Pay %1$s to %2$s affiliates?',
				$going_out_count,
				'affiliate-wp'
			),
			affwp_currency_filter( affwp_format_amount( $going_out_total ) ),
			number_format_i18n( $going_out_count )
		);

		/*
		 * Method buckets. store_credit and manual are reversible from inside AffiliateWP: store_credit
		 * adjusts balances, manual marks referrals Paid and generates a CSV (the admin can undo by
		 * deleting the payout until the money is sent externally). PayPal and Stripe move money via API
		 * at confirmation, so they can't be reversed from the admin. The slug is store_credit
		 * (underscore), matching affwp_get_affiliate_payout_method(), not the store-credit (hyphen)
		 * settings option key.
		 */
		$reversible_methods = array( 'store_credit', 'manual' );
		$method_keys        = array_keys( $payouts_by_method );
		$has_irreversible   = (bool) array_diff( $method_keys, $reversible_methods );
		$only_store_credit  = ( array( 'store_credit' ) === $method_keys );
		$only_manual        = ( array( 'manual' ) === $method_keys );

		ob_start();
		?>
		<div>
			<p class="m-0 text-sm/6 text-gray-600">
				<?php
				if ( $only_store_credit ) {
					esc_html_e( 'Affiliate balances update and referrals are marked Paid.', 'affiliate-wp' );
				} elseif ( $only_manual ) {
					esc_html_e( "Referrals are marked Paid and you'll have a CSV ready to send the money.", 'affiliate-wp' );
				} elseif ( ! $has_irreversible ) {
					// store_credit + manual mix: both reversible, but the action differs per method, so describe both.
					esc_html_e( "Affiliate balances update and you'll have a CSV ready for the manual payouts. Referrals are marked Paid.", 'affiliate-wp' );
				} else {
					// External transfers in the run (PayPal / Stripe, alone or mixed with reversible methods).
					echo esc_html(
						_n(
							'Your affiliate receives their payout and referrals are marked Paid.',
							'Your affiliates receive their payouts and referrals are marked Paid.',
							$going_out_count,
							'affiliate-wp'
						)
					);
				}
				?>
			</p>

			<?php if ( $has_irreversible ) : ?>
				<p class="mt-2 text-sm/6 font-medium text-gray-900">
					<?php esc_html_e( "Once paid, this can't be reversed.", 'affiliate-wp' ); ?>
				</p>
			<?php endif; ?>

			<?php if ( $grand_blocked_count > 0 ) : ?>
				<div class="mt-7 rounded-md bg-gray-50 p-4 space-y-3.5">
					<div class="flex items-start gap-3 text-sm/6 text-gray-700">
						<?php echo \AffiliateWP\Utils\Icons::get( 'circle-warning', [ 'class' => 'size-4 mt-1 text-amber-600 shrink-0' ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Icons::get returns sanitized SVG. ?>
						<span>
							<?php
							echo esc_html(
								sprintf(
									/* translators: 1: amount held back, 2: formatted affiliate count */
									_n(
										"%1\$s for %2\$s affiliate isn't deliverable this run, their referrals stay unpaid until the setup is fixed.",
										"%1\$s for %2\$s affiliates isn't deliverable this run, their referrals stay unpaid until the setup is fixed.",
										$grand_blocked_count,
										'affiliate-wp'
									),
									affwp_currency_filter( affwp_format_amount( $grand_blocked_total ) ),
									number_format_i18n( $grand_blocked_count )
								)
							);
							?>
						</span>
					</div>
				</div>
			<?php endif; ?>
		</div>
		<?php
		$confirm_modal_content = ob_get_clean();

		/*
		 * custom_header replaces the trait's default centered/icon header so the title can be
		 * left-aligned without warning-icon chrome. The close button lives here because custom_header
		 * short-circuits the trait's default header.
		 */
		ob_start();
		?>
		<div class="mb-5 pr-8">
			<h3 class="text-lg font-semibold text-gray-900 m-0 leading-snug" id="<?php echo esc_attr( $confirm_modal_title_id ); ?>">
				<?php echo $confirm_modal_title; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- composed from translation + entity-encoded currency, both safe. ?>
			</h3>
		</div>
		<button type="button" @click="close()" class="cursor-pointer absolute right-0 top-0 p-4 text-gray-400 hover:text-gray-500">
			<span class="sr-only"><?php esc_html_e( 'Close', 'affiliate-wp' ); ?></span>
			<?php echo \AffiliateWP\Utils\Icons::get( 'x-mark', [ 'class' => 'size-6', 'aria-hidden' => 'true' ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Icons::get returns sanitized SVG. ?>
		</button>
		<?php
		$confirm_modal_header = ob_get_clean();

		/*
		 * Footer: both actions right-aligned with Cancel as a quiet link so the primary action keeps the
		 * gravity, instead of the 2-col grid (Cancel left / primary right) other AffWP modals use.
		 */
		ob_start();
		?>
		<div class="mt-6 flex items-center justify-end gap-5">
			<button type="button" @click="close()" class="cursor-pointer text-sm font-medium text-gray-600 hover:text-gray-900 hover:underline">
				<?php esc_html_e( 'Cancel', 'affiliate-wp' ); ?>
			</button>
			<button type="button" @click="document.getElementById('<?php echo esc_js( $submit_form_id ); ?>').submit()" autofocus class="cursor-pointer inline-flex items-center justify-center rounded-md bg-affwp-brand-500 hover:bg-affwp-brand-600 px-5 py-2.5 text-sm font-semibold text-white">
				<?php esc_html_e( 'Pay', 'affiliate-wp' ); ?>
			</button>
		</div>
		<?php
		$confirm_modal_footer = ob_get_clean();

		affwp_modal( [
			'id'                => 'affwp-confirm-payout-modal',
			'size'              => 'md',
			'variant'           => 'default',
			'custom_header'     => $confirm_modal_header,
			'content'           => $confirm_modal_content,
			'custom_footer'     => $confirm_modal_footer,
			'close_on_backdrop' => true,
			'close_on_escape'   => true,
		] );
		?>
	<?php endif; ?>

</div>

<?php if ( ! empty( $payouts ) ) : ?>
	<script>
	/*
	 * Anchor .affwp-payout-action-bar to the WP admin sidebar's actual right edge.
	 * Mirrors the same shim used on the Pay Affiliates form (Step 1); WP admin's
	 * body.auto-fold class doesn't reliably reflect sidebar width on resize-up,
	 * so we measure the sidebar directly via ResizeObserver + window resize.
	 */
	( function () {
		var bar = document.querySelector( '.affwp-payout-action-bar' );
		var sidebar = document.getElementById( 'adminmenuwrap' );
		if ( ! bar || ! sidebar ) {
			return;
		}
		var sync = function () {
			var right = Math.max( 0, sidebar.getBoundingClientRect().right );
			bar.style.setProperty( '--affwp-bar-left', right + 'px' );
		};
		sync();
		window.addEventListener( 'resize', sync );
		if ( window.ResizeObserver ) {
			new ResizeObserver( sync ).observe( sidebar );
		}
	} )();
	</script>
<?php endif; ?>
