<?php
/**
 * Scripts and Styles Bootstrap
 *
 * @package     AffiliateWP
 * @subpackage  Core
 * @copyright   Copyright (c) 2014, Sandhills Development, LLC
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0
 */

use AffWP\Components\Notifications\REST\v1\Notifications_Endpoints;

/**
 * Determines whether the current admin page is an AffiliateWP admin page.
 *
 * Only works after the `wp_loaded` hook, & most effective
 * starting on `admin_menu` hook.
 *
 * @since 1.0
 *
 * @param string $page Optional. Specific admin page to check for. Default empty (any).
 * @return bool True if AffiliateWP admin page.
 */
function affwp_is_admin_page( $page = '' ) {

	if ( ! is_admin() || ! did_action( 'wp_loaded' ) ) {
		$ret = false;
	}

	if ( empty( $page ) && isset( $_GET['page'] ) ) {
		$page = sanitize_text_field( $_GET['page'] );
	} else {
		$ret = false;
	}

	$pages = [
		'affiliate-wp',
		'affiliate-wp-affiliates',
		'affiliate-wp-referrals',
		'affiliate-wp-customers',
		'affiliate-wp-payouts',
		'affiliate-wp-visits',
		'affiliate-wp-creatives',
		'affiliate-wp-reports',
		'affiliate-wp-tools',
		'affiliate-wp-settings',
		'affwp-getting-started',
		'affwp-what-is-new',
		'affwp-credits',
		'affiliate-wp-add-ons',
		'affiliate-wp-wizard',
		'affiliate-wp-setup-screen',
		'affiliate-wp-about',
		'affiliate-wp-rewards',
		'affiliate-wp-smtp',
		'affiliate-wp-cookie-consent',
		'affiliate-wp-notifications',
	];

	if ( ! empty( $page ) && in_array( $page, $pages ) ) {
		$ret = true;
	} else {
		$ret = in_array( $page, $pages );
	}

	/**
	 * Filters whether the current page is an AffiliateWP admin page.
	 *
	 * @since 1.0
	 *
	 * @param bool $ret Whether the current page is either a given admin page
	 *                  or any whitelisted admin page.
	 */
	return apply_filters( 'affwp_is_admin_page', $ret );
}

/**
 *  Load the admin scripts
 *
 *  @since 1.0
 *  @return void
 */
function affwp_admin_scripts() {

	if ( 'dashboard' === get_current_screen()->id ) {
		wp_register_script( 'affwp-dashboard-ajax', AFFILIATEWP_PLUGIN_URL . 'assets/js/dashboard-ajax.js', [ 'jquery' ], AFFILIATEWP_VERSION );
	}

	if ( ! affwp_is_admin_page() ) {
		return;
	}

	affwp_enqueue_admin_js();

	/*
	 * Version helper: in dev (SCRIPT_DEBUG) stamp assets by filemtime so edits
	 * bust the browser cache; in production use the plugin version.
	 */
	$affwp_js_ver = static function ( $rel ) {
		$path = AFFILIATEWP_PLUGIN_DIR . $rel;
		return ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $path ) )
			? (string) filemtime( $path )
			: AFFILIATEWP_VERSION;
	};

	/*
	 * Shared list-table row-click guard: one pointer tracker so every row-click
	 * handler (drawers, edit-navigation) can tell a real click from the mouseup
	 * that ends a text drag-select, in any column. Registered here and pulled in
	 * as a dependency by each consumer, so it loads first on any list screen.
	 */
	wp_register_script(
		'affwp-list-row-drag-guard',
		AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-list-row-drag-guard.js',
		[],
		$affwp_js_ver( 'assets/js/affwp-list-row-drag-guard.js' ),
		true
	);

	wp_enqueue_script( 'affwp-admin-modals' );
	wp_enqueue_script( 'affwp-admin-alpine-components' );

	// only enqueue for settings and creatives page
	if ( 'affiliate-wp-settings' === affwp_get_current_screen() || ( isset( $_GET['action'] ) && ( $_GET['action'] == 'add_creative' || $_GET['action'] == 'edit_creative' ) ) ) {
		wp_enqueue_media();
	}

	// Enqueue Select2 for Setting Screens.
	if ( affwp_is_admin_page() ) {
		affwp_enqueue_style( 'affwp-select2' );
		affwp_enqueue_script( 'affwp-select2' );
	}

	wp_enqueue_script( 'jquery-ui-datepicker' );

	// Enqueue postbox for core meta boxes.
	wp_enqueue_script( 'postbox' );

	/*
	 * Payout + Batch detail drawers. Originally gated to the Payouts page
	 * only ("affiliate-wp-payouts" covers both Payouts + Batches tabs
	 * via the same parent page slug), but the AffWP_Payouts_Table class is
	 * also re-used on the affiliate view's "Recent Payouts" section
	 * (includes/admin/affiliates/view.php). Without the drawer +
	 * popover-anchor scripts loading there, kebab popovers fall back to
	 * the UA top-layer default (top-left of viewport) and clicking View
	 * on a row navigates to the Payouts page instead of opening the
	 * drawer in place. Extending the gate to include the affiliate view
	 * action restores parity. Add a third condition any time another
	 * surface starts rendering this table.
	 */
	$current_screen     = affwp_get_current_screen();
	$is_payouts_screen  = ( 'affiliate-wp-payouts' === $current_screen );
	$is_affiliate_view  = (
		'affiliate-wp-affiliates' === $current_screen
		&& isset( $_GET['action'] )
		&& 'view_affiliate' === $_GET['action']
	);
	// The referrals list opens the payout drawer in-context (the "View payout" row action on paid referrals).
	$is_referrals_list_for_drawer = (
		'affiliate-wp-referrals' === $current_screen
		&& ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'edit_referral', 'add_referral' ), true ) )
	);

	if ( $is_payouts_screen || $is_affiliate_view || $is_referrals_list_for_drawer ) {
		/*
		 * Native HTML Popover anchored-positioning helper for list-table row kebabs.
		 * Any admin page using `<button popovertarget>` + `<div popover data-affwp-popover-anchor>`
		 * gets anchored positioning, animated entry, and close-on-scroll by enqueuing this.
		 */
		wp_enqueue_script(
			'affwp-popover-anchor',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-popover-anchor.js',
			[],
			AFFILIATEWP_VERSION,
			true
		);

		wp_enqueue_script(
			'affwp-payout-drawer',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-payout-drawer.js',
			[ 'affwp-admin', 'affwp-list-row-drag-guard' ], // affwp-admin bundles Alpine.js
			$affwp_js_ver( 'assets/js/affwp-payout-drawer.js' ),
			true
		);
		wp_localize_script( 'affwp-payout-drawer', 'affwpPayoutDrawerData', [
			// Per-verb nonces — a leaked nonce only unlocks the verb it was minted for.
			'nonces'  => [
				'affwp_get_payout_drawer_data'         => wp_create_nonce( 'affwp_get_payout_drawer_data' ),
				'affwp_payout_drawer_check_status'     => wp_create_nonce( 'affwp_payout_drawer_check_status' ),
				'affwp_payout_drawer_retry'            => wp_create_nonce( 'affwp_payout_drawer_retry' ),
				'affwp_payout_drawer_delete'           => wp_create_nonce( 'affwp_payout_drawer_delete' ),
				'affwp_payout_drawer_send_failure_email' => wp_create_nonce( 'affwp_payout_drawer_send_failure_email' ),
				'affwp_payout_drawer_switch_method'    => wp_create_nonce( 'affwp_payout_drawer_switch_method' ),
			],
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'listUrl' => affwp_admin_url( 'payouts' ),
			/*
			 * User-facing strings emitted from affwp-payout-drawer.js. Built
			 * server-side so they pass through __() and pick up site locale.
			 * Substitution tokens are documented per-string via translators
			 * comments; the JS replaces {token} placeholders inline.
			 */
			'labels'  => [
				/* translators: %d: payout ID. */
				'payoutTitle'                  => __( 'Payout #%d', 'affiliate-wp' ),
				/* translators: %d: count of Stripe transfers. */
				'stripeTransferSingular'       => __( '%d transfer', 'affiliate-wp' ),
				/* translators: %d: count of Stripe transfers. */
				'stripeTransferPlural'         => __( '%d transfers', 'affiliate-wp' ),
				'referralsHeadingPaidSingular' => __( '1 Referral Paid', 'affiliate-wp' ),
				/* translators: %d: count of paid referrals. */
				'referralsHeadingPaidPlural'   => __( '%d Referrals Paid', 'affiliate-wp' ),
				'referralsHeadingSingular'     => __( '1 Referral in this Payout', 'affiliate-wp' ),
				/* translators: %d: count of referrals in the payout. */
				'referralsHeadingPlural'       => __( '%d Referrals in this Payout', 'affiliate-wp' ),
				/* translators: %s: failure class label (e.g. "Affiliate action required"). */
				'retryDisabledWithLabel'       => __( '%s — Retry becomes available once the blocker is cleared.', 'affiliate-wp' ),
				'retryDisabledGeneric'         => __( "Retry isn't available for this failure class.", 'affiliate-wp' ),
				'loadFailed'                   => __( 'Failed to load payout.', 'affiliate-wp' ),
				'loadFailedNetwork'            => __( 'Failed to load payout (network error).', 'affiliate-wp' ),
				'switchMethodFailed'           => __( 'Failed to switch method.', 'affiliate-wp' ),
				'switchMethodNetworkError'     => __( 'Network error switching method.', 'affiliate-wp' ),
				/* translators: %s: human-readable recency, e.g. "5 minutes ago". */
				'emailSent'                    => __( 'Email sent.', 'affiliate-wp' ),
				'emailFailed'                  => __( 'Failed to send the email.', 'affiliate-wp' ),
				'emailNetworkError'            => __( 'Network error sending the email.', 'affiliate-wp' ),
				'statusRefreshed'              => __( 'Status refreshed.', 'affiliate-wp' ),
				'statusCheckFailed'            => __( 'Status check failed.', 'affiliate-wp' ),
				'statusCheckNetworkError'      => __( 'Status check failed (network error).', 'affiliate-wp' ),
				'retryConfirm'                 => __( 'Create a new batch to retry this payout?', 'affiliate-wp' ),
				'retryToastSuccess'            => __( 'Retry batch created.', 'affiliate-wp' ),
				'retryToastFailed'             => __( 'Retry failed.', 'affiliate-wp' ),
				'retryToastNetworkError'       => __( 'Retry failed (network error).', 'affiliate-wp' ),
				'deleteConfirm'                => __( 'Delete this payout? This cannot be undone.', 'affiliate-wp' ),
				'deleteToastSuccess'           => __( 'Payout deleted.', 'affiliate-wp' ),
				'deleteToastFailed'            => __( 'Delete failed.', 'affiliate-wp' ),
				'deleteToastNetworkError'      => __( 'Delete failed (network error).', 'affiliate-wp' ),
			],
			/*
			 * PayPal per-item status enum → localized labels. Two flavors:
			 * `humanize` is lowercase + spaced for sentence-form copy
			 * (warning summary headings); `format` is Title Case for
			 * badge pills aligned with our payout-status pills.
			 * Keep the keys in sync with affwp-payout-drawer.js's
			 * humanizeItemStatus() / formatStatusLabel() callers.
			 */
			'paypalItemStatus' => [
				'humanize' => [
					'UNCLAIMED' => __( 'unclaimed', 'affiliate-wp' ),
					'RETURNED'  => __( 'returned', 'affiliate-wp' ),
					'BLOCKED'   => __( 'blocked', 'affiliate-wp' ),
					'REFUNDED'  => __( 'refunded', 'affiliate-wp' ),
					'REVERSED'  => __( 'reversed', 'affiliate-wp' ),
					'DENIED'    => __( 'denied', 'affiliate-wp' ),
					'FAILED'    => __( 'failed', 'affiliate-wp' ),
					'PENDING'   => __( 'pending', 'affiliate-wp' ),
					'ONHOLD'    => __( 'on hold', 'affiliate-wp' ),
					'SUCCESS'   => __( 'successful', 'affiliate-wp' ),
				],
				'format'   => [
					'UNCLAIMED'  => __( 'Unclaimed', 'affiliate-wp' ),
					'RETURNED'   => __( 'Returned', 'affiliate-wp' ),
					'BLOCKED'    => __( 'Blocked', 'affiliate-wp' ),
					'REFUNDED'   => __( 'Refunded', 'affiliate-wp' ),
					'REVERSED'   => __( 'Reversed', 'affiliate-wp' ),
					'DENIED'     => __( 'Denied', 'affiliate-wp' ),
					'FAILED'     => __( 'Failed', 'affiliate-wp' ),
					'PENDING'    => __( 'Pending', 'affiliate-wp' ),
					'ONHOLD'     => __( 'On hold', 'affiliate-wp' ),
					'SUCCESS'    => __( 'Success', 'affiliate-wp' ),
					'PROCESSING' => __( 'Processing', 'affiliate-wp' ),
					'CANCELED'   => __( 'Canceled', 'affiliate-wp' ),
				],
			],
		] );

		wp_enqueue_script(
			'affwp-batch-drawer',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-batch-drawer.js',
			[ 'affwp-admin', 'affwp-payout-drawer', 'affwp-list-row-drag-guard' ], // payout drawer is a sibling — batch drawer calls into its store when an admin clicks a payout inside the batch
			$affwp_js_ver( 'assets/js/affwp-batch-drawer.js' ),
			true
		);
		wp_localize_script( 'affwp-batch-drawer', 'affwpBatchDrawerData', [
			'nonces'  => [
				'affwp_get_batch_drawer_data'      => wp_create_nonce( 'affwp_get_batch_drawer_data' ),
				'affwp_batch_drawer_retry_failed'  => wp_create_nonce( 'affwp_batch_drawer_retry_failed' ),
			],
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'listUrl' => affwp_admin_url( 'payouts', [ 'tab' => 'batches' ] ),
			/*
			 * Toast templates the drawer JS fires when a polled batch reaches a terminal state.
			 * Placeholders {id}, {total}, {paid}, {failed} are replaced JS-side.
			 */
			'labels'  => [
				'toastCompleted'         => __( 'Batch #{id} complete · {paid} of {total} paid', 'affiliate-wp' ),
				'toastPartial'           => __( 'Batch #{id} finished · {paid} paid, {failed} failed', 'affiliate-wp' ),
				'toastFailed'            => __( 'Batch #{id} failed · 0 of {total} paid', 'affiliate-wp' ),
				/* translators: %d: batch ID. */
				'batchTitle'             => __( 'Batch #%d', 'affiliate-wp' ),
				'loadFailed'             => __( 'Failed to load batch.', 'affiliate-wp' ),
				'loadFailedNetwork'      => __( 'Failed to load batch (network error).', 'affiliate-wp' ),
				'tabAll'                 => __( 'All', 'affiliate-wp' ),
				'tabPaid'                => __( 'Paid', 'affiliate-wp' ),
				'tabPending'             => __( 'Pending', 'affiliate-wp' ),
				'tabFailed'              => __( 'Failed', 'affiliate-wp' ),
				/* translators: %d: count of failed payouts being retried. */
				'retryConfirmSingular'   => __( 'Retry %d failed payout in a new batch? The originals will be preserved as an audit record.', 'affiliate-wp' ),
				/* translators: %d: count of failed payouts being retried. */
				'retryConfirmPlural'     => __( 'Retry %d failed payouts in a new batch? The originals will be preserved as an audit record.', 'affiliate-wp' ),
				/* translators: %d: count of payouts queued for retry. */
				'retryToastSingular'     => __( 'Retried %d payout in a new batch.', 'affiliate-wp' ),
				/* translators: %d: count of payouts queued for retry. */
				'retryToastPlural'       => __( 'Retried %d payouts in a new batch.', 'affiliate-wp' ),
				'retryToastFailed'       => __( 'Retry failed.', 'affiliate-wp' ),
				'retryToastNetworkError' => __( 'Retry failed (network error).', 'affiliate-wp' ),
			],
		] );

		// Bulk-pay drawer store (Referrals bulk "Pay"). Renders the shared Pay Affiliates
		// preview body, scoped to the selection, and pays from there.
		wp_enqueue_script(
			'affwp-bulk-pay-drawer',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-bulk-pay-drawer.js',
			[ 'affwp-admin', 'affwp-batch-drawer' ], // affwp-admin bundles Alpine.js; hands off to the batch drawer's store on submit
			AFFILIATEWP_VERSION,
			true
		);
		wp_localize_script( 'affwp-bulk-pay-drawer', 'affwpBulkPayDrawer', [
			'ajaxUrl'    => admin_url( 'admin-ajax.php' ),
			'nonce'      => wp_create_nonce( 'affwp_referral_row_action' ),
			// Fallback target if the batch-drawer store isn't present when the batch is created.
			'batchesUrl' => affwp_admin_url( 'payouts', [ 'tab' => 'batches' ] ),
			'labels'     => [
				'loadingTitle'   => __( 'Pay referrals', 'affiliate-wp' ),
				'confirmDefault' => __( 'Pay', 'affiliate-wp' ),
				'error'          => __( 'Something went wrong. Please try again.', 'affiliate-wp' ),
				'networkError'   => __( 'Network error. Please try again.', 'affiliate-wp' ),
			],
		] );

		// Automatic Payout schedule drawer store (Automatic Payouts tab create/edit).
		wp_enqueue_script(
			'affwp-schedule-drawer',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-schedule-drawer.js',
			[ 'affwp-admin', 'affwp-list-row-drag-guard' ], // affwp-admin bundles Alpine.js
			$affwp_js_ver( 'assets/js/affwp-schedule-drawer.js' ),
			true
		);
		wp_localize_script( 'affwp-schedule-drawer', 'affwpScheduleDrawer', [
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'nonce'   => wp_create_nonce( 'affwp_schedule_row_action' ),
			'labels'  => [
				'addTitle'     => __( 'New payout schedule', 'affiliate-wp' ),
				'editTitle'    => __( 'Edit payout schedule', 'affiliate-wp' ),
				// Header subtitle: describes the drawer on create; names the object's type on edit ("VIP" → a payout schedule). The "schedule" idea is already in the title, so this keeps only the "own method" differentiator (and stays one line).
				'addSubtitle'  => __( 'Pays each affiliate through their own method.', 'affiliate-wp' ),
				'editSubtitle' => __( 'Payout schedule', 'affiliate-wp' ),
				'discard'      => __( 'You have unsaved changes. Discard them?', 'affiliate-wp' ),
				'runningNow'   => __( 'Running now…', 'affiliate-wp' ),
				'pause'        => __( 'Pause', 'affiliate-wp' ),
				'resume'       => __( 'Resume', 'affiliate-wp' ),
				'error'        => __( 'Something went wrong. Please try again.', 'affiliate-wp' ),
				// Empty-selection auto-name (the mixed/multiple cases reuse the summary fragments below).
				'nameAll'      => __( 'All affiliates', 'affiliate-wp' ),
			],
			// Currency for the summary's minimum clause. Decoded to the raw symbol (affwp_currency_filter returns HTML entities that x-text would print literally).
			'currencySign' => html_entity_decode( affwp_currency_filter( '' ), ENT_QUOTES, 'UTF-8' ),
			'currencyPos'  => affiliate_wp()->settings->get( 'currency_position', 'before' ),
			// Day-name / ordinal maps so the JS summary reads the same as the drawer picker + list cadence.
			'weekdays'     => affwp_payout_schedule_weekday_labels(),
			'monthdays'    => affwp_payout_schedule_monthday_options(),
			// Fragments for the live summary sentence: "<who> will be paid <when>[, if …][, including …]".
			'summary'      => [
				'audienceAll'        => __( 'All eligible affiliates', 'affiliate-wp' ),
				/* translators: %s: affiliate group name, e.g. "VIP" → "VIP group". */
				'groupOne'           => __( '%s group', 'affiliate-wp' ),
				/* translators: %d: number of affiliate groups. */
				'groupsN'            => __( '%d groups', 'affiliate-wp' ),
				/* translators: %d: number of affiliates. */
				'affiliatesN'        => __( '%d affiliates', 'affiliate-wp' ),
				'join'               => __( ' and ', 'affiliate-wp' ),
				/* translators: %s: the audience when it's a single group, e.g. "VIP group". Adds the article so the sentence reads naturally. */
				'leadArticle'        => __( 'The %s', 'affiliate-wp' ),
				/* translators: %1$s: who is paid (audience); %2$s: when they are paid (cadence). */
				'base'               => __( '%1$s will be paid %2$s', 'affiliate-wp' ),
				/* translators: %s: weekday name, e.g. "Friday". */
				'cadenceWeekly'      => __( 'every %s', 'affiliate-wp' ),
				/* translators: %s: ordinal day of the month, e.g. "1st". */
				'cadenceMonthly'     => __( 'on the %s of each month', 'affiliate-wp' ),
				'cadenceMonthlyLast' => __( 'on the last day of each month', 'affiliate-wp' ),
				/* translators: %s: minimum earnings amount, e.g. "$50". */
				'minClause'          => __( ', if they’ve earned at least %s', 'affiliate-wp' ),
				'holdingClause'      => __( ', including commissions still within the holding period', 'affiliate-wp' ),
			],
		] );
	}

	/*
	 * Unified list-table row actions (sticky Actions column: hover icon rail
	 * + native-popover kebab + whole-row click to edit). Loads on the Referrals
	 * and Affiliates list views; excludes their edit/add/view sub-views, which
	 * render a form, not a table.
	 */
	$is_referrals_list = (
		'affiliate-wp-referrals' === $current_screen
		&& ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'edit_referral', 'add_referral' ), true ) )
	);
	$is_affiliates_list = (
		'affiliate-wp-affiliates' === $current_screen
		&& ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'view_affiliate', 'edit_affiliate', 'review_affiliate', 'new_affiliate', 'add_affiliate' ), true ) )
	);
	$is_creatives_list = (
		'affiliate-wp-creatives' === $current_screen
		&& ( ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], array( 'edit_creative', 'add_creative' ), true ) )
	);
	$is_referral_edit = (
		'affiliate-wp-referrals' === $current_screen
		&& isset( $_GET['action'] ) && 'edit_referral' === $_GET['action']
	);

	if ( $is_referrals_list || $is_affiliates_list || $is_creatives_list || $is_referral_edit ) {
		wp_enqueue_script(
			'affwp-popover-anchor',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-popover-anchor.js',
			[],
			AFFILIATEWP_VERSION,
			true
		);

		wp_enqueue_script(
			'affwp-list-row-actions',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-list-row-actions.js',
			[ 'affwp-list-row-drag-guard' ],
			$affwp_js_ver( 'assets/js/affwp-list-row-actions.js' ),
			true
		);
		wp_localize_script( 'affwp-list-row-actions', 'affwpListRowActions', [
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			// Per-entity nonces — minted per verb the rail can fire.
			'nonces'  => [
				'referral'  => wp_create_nonce( 'affwp_referral_row_action' ),
				'affiliate' => wp_create_nonce( 'affwp_affiliate_row_action' ),
			],
			'labels'  => [
				'error'                 => __( 'Something went wrong. Please try again.', 'affiliate-wp' ),
				'networkError'          => __( 'Network error. Please try again.', 'affiliate-wp' ),
				// Bulk-pay result toast (composed at submit, shown after the list reloads). %d placeholders.
				'toastPaidSingular'     => __( 'Paid %d referral.', 'affiliate-wp' ),
				'toastPaidPlural'       => __( 'Paid %d referrals.', 'affiliate-wp' ),
				'toastProcessingClause' => __( '%d sent and processing.', 'affiliate-wp' ),
				'toastFailedClause'     => __( "%d couldn't be paid.", 'affiliate-wp' ),
			],
		] );
	}

	$is_visits_list = ( 'affiliate-wp-visits' === $current_screen );


	// The referral edit screen reuses the "Pay" confirm modal (the list renders it inline via the table).
	if ( $is_referral_edit ) {
		add_action( 'admin_footer', 'affwp_render_pay_referral_modal' );
	}


	/*
	 * The Referral drawer's "Pay" affordance reuses the shared per-referral Pay
	 * machinery: the click handler in affwp-list-row-actions.js + the confirm
	 * modal. Those already load with the Referrals list (handler via the block
	 * above, modal rendered inline by the list table). On the *other* surfaces
	 * the drawer renders — Payouts list/Batches, the affiliate view's Recent
	 * Payouts, and the Visits list — neither is present, so enqueue both here
	 * (the surfaces are mutually exclusive with the list block, so no
	 * double-localize; the modal isn't rendered by a table here, so no dup).
	 */
	if ( $is_payouts_screen || $is_affiliate_view || $is_visits_list ) {
		wp_enqueue_script(
			'affwp-list-row-actions',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/affwp-list-row-actions.js',
			[],
			AFFILIATEWP_VERSION,
			true
		);
		wp_localize_script( 'affwp-list-row-actions', 'affwpListRowActions', [
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'nonces'  => [
				'referral'  => wp_create_nonce( 'affwp_referral_row_action' ),
				'affiliate' => wp_create_nonce( 'affwp_affiliate_row_action' ),
			],
			'labels'  => [
				'error'                 => __( 'Something went wrong. Please try again.', 'affiliate-wp' ),
				'networkError'          => __( 'Network error. Please try again.', 'affiliate-wp' ),
				/* translators: %d: count of referrals paid. */
				'toastPaidSingular'     => __( 'Paid %d referral.', 'affiliate-wp' ),
				/* translators: %d: count of referrals paid. */
				'toastPaidPlural'       => __( 'Paid %d referrals.', 'affiliate-wp' ),
				/* translators: %d: count sent and processing. */
				'toastProcessingClause' => __( '%d sent and processing.', 'affiliate-wp' ),
				/* translators: %d: count that could not be paid. */
				'toastFailedClause'     => __( "%d couldn't be paid.", 'affiliate-wp' ),
			],
		] );
		add_action( 'admin_footer', 'affwp_render_pay_referral_modal' );
	}

	if ( $is_payouts_screen && isset( $_GET['action'] ) && 'new_payout' === $_GET['action'] ) {
		wp_enqueue_script(
			'affwp-cally',
			AFFILIATEWP_PLUGIN_URL . 'assets/vendor/cally/cally.js',
			[],
			'0.8.0',
			true
		);
	}

	/*
	 * Preview Payout lazy-loader. Activates the "Show 100 more · N hidden" buttons
	 * on each per-method section + the "Cannot be paid this run" section.
	 * Without this script the buttons are inert; with it they POST to
	 * affwp_preview_payout_load_more and append the returned rows in place.
	 * Tiny dependency-free module — no jQuery, no Alpine.
	 */
	if ( $is_payouts_screen && isset( $_GET['action'] ) && 'preview_payout' === $_GET['action'] ) {
		wp_enqueue_script(
			'affwp-admin-preview-payout',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/admin-preview-payout.js',
			[],
			AFFILIATEWP_VERSION,
			true
		);

		// Mirror the initial render's GET params so the AJAX endpoint produces the same payouts/invalid arrays.
		$preview_include_affiliates = isset( $_GET['include_affiliates'] ) && is_array( $_GET['include_affiliates'] )
			? array_values( array_filter( array_map( 'absint', wp_unslash( $_GET['include_affiliates'] ) ) ) )
			: array();
		$preview_include_groups     = isset( $_GET['include_groups'] ) && is_array( $_GET['include_groups'] )
			? array_values( array_filter( array_map( 'absint', wp_unslash( $_GET['include_groups'] ) ) ) )
			: array();
		$preview_payout_methods     = isset( $_GET['payout_methods'] ) && is_array( $_GET['payout_methods'] )
			? array_values( array_filter( array_map( 'sanitize_key', wp_unslash( $_GET['payout_methods'] ) ) ) )
			: array();
		$preview_payout_method      = isset( $_GET['payout_method'] )
			? strtolower( sanitize_text_field( wp_unslash( $_GET['payout_method'] ) ) )
			: '';

		wp_localize_script(
			'affwp-admin-preview-payout',
			'affwpPreviewPayoutData',
			[
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'affwp_preview_payout_load_more' ),
				'filters' => [
					'include_affiliates' => $preview_include_affiliates,
					'include_groups'     => $preview_include_groups,
					'payout_methods'     => $preview_payout_methods,
					'payout_method'      => $preview_payout_method,
					'from'               => isset( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : '',
					'to'                 => isset( $_GET['to'] ) ? sanitize_text_field( wp_unslash( $_GET['to'] ) ) : '',
					'minimum'            => isset( $_GET['minimum'] ) ? sanitize_text_field( wp_unslash( $_GET['minimum'] ) ) : '',
					'bypass_holding'     => isset( $_GET['bypass_holding'] ) && ! empty( $_GET['bypass_holding'] ) ? 1 : 0,
				],
				'strings' => [
					/* translators: 1: rows the next click reveals, 2: total rows still hidden right now. */
					'showMoreWithHidden' => __( 'Show %1$d more · %2$d hidden', 'affiliate-wp' ),
					/* translators: %d: rows the final click reveals. */
					'showMore'           => __( 'Show %d more', 'affiliate-wp' ),
					'loadError'          => __( 'Could not load more — click to retry.', 'affiliate-wp' ),
				],
			]
		);
	}

	/*
	 * Destructive bulk-action confirm. Intercepts the Apply button on
	 * the Payouts, Batches, and Referrals filter forms when "Delete" is the
	 * selected bulk action and at least one row is checked. Surfaces a
	 * native confirm() with copy that warns about referral-revert for the
	 * Payouts surface. Tiny dependency-free module — no jQuery or Alpine —
	 * so it can be enqueued cheaply across all three list-table screens.
	 *
	 * Gate covers Batches indirectly: the Batches tab lives under the
	 * `affiliate-wp-payouts` parent screen slug, so the first condition below
	 * is enough. Add an explicit `$is_batches` check here only when Batches
	 * moves to its own parent page slug — at that point the JS module's
	 * `FORM_CONFIGS` entry for `affwp-payout-batches-filter` becomes
	 * unreachable without the new gate.
	 */
	if (
		'affiliate-wp-payouts' === $current_screen
		|| 'affiliate-wp-referrals' === $current_screen
	) {
		wp_enqueue_script(
			'affwp-admin-bulk-confirm',
			AFFILIATEWP_PLUGIN_URL . 'assets/js/admin-bulk-confirm.js',
			[],
			AFFILIATEWP_VERSION,
			true
		);
		/*
		 * Bulk-action confirm dialog copy. The Payouts message intentionally
		 * spells out the referral-revert side-effect — the destructive
		 * consequence is non-obvious. Batches + Referrals share a shorter
		 * "this is permanent" line.
		 */
		wp_localize_script(
			'affwp-admin-bulk-confirm',
			'affwpBulkConfirmData',
			[
				'labels' => [
					/* translators: %d: number of selected payouts. */
					'confirmPayoutSingular'  => __( 'Are you sure you want to delete the %d selected payout?' . "\n\n" . 'Deleting paid or processing payouts will revert their referrals to unpaid, which may cause double-payment if you re-run Pay Affiliates.', 'affiliate-wp' ),
					/* translators: %d: number of selected payouts. */
					'confirmPayoutPlural'    => __( 'Are you sure you want to delete the %d selected payouts?' . "\n\n" . 'Deleting paid or processing payouts will revert their referrals to unpaid, which may cause double-payment if you re-run Pay Affiliates.', 'affiliate-wp' ),
					/* translators: %d: number of selected batches. */
					'confirmBatchSingular'   => __( 'Are you sure you want to delete the %d selected batch? This is permanent.', 'affiliate-wp' ),
					/* translators: %d: number of selected batches. */
					'confirmBatchPlural'     => __( 'Are you sure you want to delete the %d selected batches? This is permanent.', 'affiliate-wp' ),
					/* translators: %d: number of selected referrals. */
					'confirmReferralSingular' => __( 'Are you sure you want to delete the %d selected referral? This is permanent.', 'affiliate-wp' ),
					/* translators: %d: number of selected referrals. */
					'confirmReferralPlural'   => __( 'Are you sure you want to delete the %d selected referrals? This is permanent.', 'affiliate-wp' ),
				],
			]
		);
	}
}
add_action( 'admin_enqueue_scripts', 'affwp_admin_scripts' );

/**
 * Mark the Cally script tag as an ES module. Cally ships ESM-only and
 * defines custom elements via `customElements.define()`, which requires
 * module evaluation. wp_enqueue_script_module() would be cleaner but is
 * WP 6.5+; this filter works on all recent WP versions.
 *
 * @since 2.33.0
 *
 * @param string $tag    The <script> tag for the enqueued script.
 * @param string $handle The script's registered handle.
 * @return string
 */
function affwp_make_cally_a_module( $tag, $handle ) {
	if ( 'affwp-cally' !== $handle ) {
		return $tag;
	}
	return str_replace( '<script ', '<script type="module" ', $tag );
}
add_filter( 'script_loader_tag', 'affwp_make_cally_a_module', 10, 2 );

/**
 * Enqueue scripts on the Add New User (user-new.php) Page.
 *
 * @since  2.9.6
 * @return void Early bail when not the `user-new.php` screen.
 */
function affwp_register_dependant_fields() {

	$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

	wp_register_script( 'affiliate-wp-dependant-fields', AFFILIATEWP_PLUGIN_URL . "assets/js/admin-dependant-fields{$suffix }.js", [ 'jquery' ], AFFILIATEWP_VERSION, true );

	if ( ! affwp_is_add_user_screen() ) {
		return;
	}

	wp_enqueue_script( 'affiliate-wp-dependant-fields' );
}
add_action( 'admin_enqueue_scripts', 'affwp_register_dependant_fields' );

/**
 * Is this the add user screen in the admin?
 *
 * @since 2.9.6
 *
 * @return bool
 */
function affwp_is_add_user_screen() {

	if ( ! is_admin() ) {
		return false;
	}

	if ( ! function_exists( 'get_current_screen' ) ) {
		return false;
	}

	$screen = get_current_screen();

	return 'add' === $screen->action && 'user' === $screen->base;
}

/**
 * Add inline styles for AffiliateWP admin pages.
 *
 * @since 2.29.0
 * @return void
 */
function affwp_add_settings_inline_styles() {
	// Only load on AffiliateWP admin pages.
	if ( ! affwp_is_admin_page() ) {
		return;
	}

	// Add inline styles to the main admin stylesheet.
	$inline_css = '[x-cloak] { display: none !important; }';

	/*
	 * Filled-primary variant of the native WP `page-title-action` button.
	 * Header actions use `page-title-action` (the WP-standard size + vertical
	 * alignment with the page title); this modifier keeps one emphasized CTA
	 * (e.g. "Pay Affiliates") filled in the admin color scheme — reusing WP's
	 * own theme-color vars so it tracks whatever scheme the user runs — while
	 * inheriting the exact geometry of its outlined siblings so nothing
	 * misaligns. WP core ships no filled page-title-action, hence this rule.
	 */
	$inline_css .= '
		.wrap .page-title-action.affwp-page-title-action-primary,
		.wrap .page-title-action.affwp-page-title-action-primary:active {
			background: var(--wp-admin-theme-color, #2271b1);
			border-color: var(--wp-admin-theme-color, #2271b1);
			color: #fff;
		}
		.wrap .page-title-action.affwp-page-title-action-primary:hover,
		.wrap .page-title-action.affwp-page-title-action-primary:focus {
			background: var(--wp-admin-theme-color-darker-10, #135e96);
			border-color: var(--wp-admin-theme-color-darker-10, #135e96);
			color: #fff;
		}';

	wp_add_inline_style( 'affwp-admin', $inline_css );
}
add_action( 'admin_enqueue_scripts', 'affwp_add_settings_inline_styles', 20 );

/**
 *  Load the admin styles
 *
 *  @since 1.0
 *  @return void
 */
function affwp_admin_styles() {

	$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

	// Shared admin CSS needed for the admin menu icon, dashboard widgets etc.
	wp_enqueue_style( 'affwp-admin-shared', AFFILIATEWP_PLUGIN_URL . 'assets/css/admin-shared' . $suffix . '.css', [ 'dashicons' ], AFFILIATEWP_VERSION );

	// On non-AffiliateWP pages, add a fallback click handler for the Notifications
	// sub-menu that navigates to Overview (where the panel will auto-open).
	// On AffiliateWP pages, the admin-notifications.js init() handles this instead.
	if ( ! affwp_is_admin_page() ) {
		wp_add_inline_script( 'jquery-core', sprintf(
			'document.addEventListener("DOMContentLoaded",function(){var l=document.querySelector("#toplevel_page_affiliate-wp a[href*=affiliate-wp-notifications]");if(l){l.addEventListener("click",function(e){e.preventDefault();window.location.href=%s;});}});',
			wp_json_encode( admin_url( 'admin.php?page=affiliate-wp#notifications' ) )
		) );
		return;
	}

	wp_enqueue_style( 'affwp-admin', AFFILIATEWP_PLUGIN_URL . 'assets/css/admin' . $suffix . '.css', [], AFFILIATEWP_VERSION );

	// jQuery UI styles are loaded on our admin pages only
	$ui_style = ( 'classic' == get_user_option( 'admin_color' ) ) ? 'classic' : 'fresh';
	wp_enqueue_style( 'jquery-ui-css', AFFILIATEWP_PLUGIN_URL . 'assets/css/vendor/jquery-ui/jquery-ui-' . $ui_style . '.min.css' );

	// In-plugin notifications.
	wp_enqueue_script( 'affwp-admin-notifications' );
	wp_localize_script(
		'affwp-admin-notifications',
		'affwp_notifications_vars',
		[
			'restBase'     => rest_url( ( new Notifications_Endpoints() )->namespace ),
			'restNonce'    => wp_create_nonce( 'wp_rest' ),
			'overviewUrl'  => admin_url( 'admin.php?page=affiliate-wp' ),
		]
	);

	// Addons page style.
	wp_register_style( 'affwp_admin_addons', AFFILIATEWP_PLUGIN_URL . "assets/css/admin-addons{$suffix}.css", [], AFFILIATEWP_VERSION );
}
add_action( 'admin_enqueue_scripts', 'affwp_admin_styles' );

/**
 * Enqueues and localizes admin.js.
 *
 * This is separated so it can be selectively executed outside of affwp admin pages.
 *
 * @since 2.0
 * @since 2.17.0 Conditionally loads QR Code library if on a Creative page.
 */
function affwp_enqueue_admin_js() {

	$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
	wp_register_script( 'affwp-tooltips', AFFILIATEWP_PLUGIN_URL . 'assets/js/tooltips' . $suffix . '.js', [ 'jquery' ], AFFILIATEWP_VERSION );

	// Batch processing.
	wp_register_script( 'affwp-batch', AFFILIATEWP_PLUGIN_URL . 'assets/js/batch' . $suffix . '.js', [ 'jquery-form' ], AFFILIATEWP_VERSION );

	wp_localize_script(
		'affwp-batch',
		'affwp_batch_vars',
		[
			'unsupported_browser'   => __( 'We are sorry but your browser is not compatible with this kind of file upload. Please upgrade your browser.', 'affiliate-wp' ),
			'import_field_required' => __( 'This field must be mapped for the import to proceed.', 'affiliate-wp' ),
		]
	);

	$admin_deps = [ 'jquery', 'jquery-ui-autocomplete', 'affwp-batch', 'wp-util' ];

	wp_enqueue_script( 'affwp-admin', AFFILIATEWP_PLUGIN_URL . 'assets/js/admin' . $suffix . '.js', $admin_deps, AFFILIATEWP_VERSION );
	wp_localize_script(
		'affwp-admin',
		'affwp_vars',
		[
			'post_id'                   => isset( $post->ID ) ? $post->ID : null,
			'affwp_version'             => AFFILIATEWP_VERSION,
			'currency_sign'             => affwp_currency_filter( '' ),
			'currency_pos'              => affiliate_wp()->settings->get( 'currency_position', 'before' ),
			'confirm_delete_referral'   => __( 'Are you sure you want to delete this referral?', 'affiliate-wp' ),
			'holdingPeriodModal'        => [
				'rowActionsTitle'  => __( 'This referral is currently within the commission holding period.', 'affiliate-wp' ),
				'bulkActionsTitle' => __( 'A selected referral is currently within the commission holding period.', 'affiliate-wp' ),
				'content'          => __( 'Are you sure you want to pay this now?', 'affiliate-wp' ),
				'confirm'          => __( 'Yes, pay now', 'affiliate-wp' ),
				'cancel'           => __( 'Cancel', 'affiliate-wp' ),
			],
			'no_user_found'             => __( 'The user you entered does not exist. To create a new user and affiliate, continue filling out the form and click Add User & Affiliate.', 'affiliate-wp' ),
			'no_user_email_found'       => __( 'No user account is associated with this email address. To create a new user and affiliate, continue filling out the form and click Add User & Affiliate.', 'affiliate-wp' ),
			'user_and_affiliate_input'  => __( 'Add User & Affiliate', 'affiliate-wp' ),
			'valid_user_selected'       => __( 'You have selected a valid user account and may continue adding this user as an affiliate.', 'affiliate-wp' ),
			'existing_affiliate'        => __( 'An affiliate already exists for this username.', 'affiliate-wp' ),
			/* translators: Affiliate username */
			'user_email_exists'         => __( 'A user already exists for this email address, however they are not currently an affiliate. Their username is %s', 'affiliate-wp' ),
			'view_affiliate'            => __( 'View Affiliate', 'affiliate-wp' ),
			'creativeDefaultName'       => __( 'Creative', 'affiliate-wp' ),
			'creativeUpgradeNoticeNo'   => __( 'Review & Rename Creatives', 'affiliate-wp' ),
			'creativeUpgradeNoticeYes'  => __( 'Make Creative Names Visible', 'affiliate-wp' ),
			'creativeUpdateNameConfirm' => __( 'This creative’s name has not been updated and affiliates will see it as “Creative” on your website.', 'affiliate-wp' ),
			'creativeQRCodeFeatUpgReq'  => __( 'Please upgrade to PRO to use the QR Code type feature.', 'affiliate-wp' ),
			'proFeatureOnly'            => __( 'Upgrade to Pro to access this feature.', 'affiliate-wp' ),
			'proFeatureOnlyUnlicensed'  => __( 'Enter your license key to access this Pro feature.', 'affiliate-wp' ),
			'license'                   => [
				'isPro'        => affwp_is_upgrade_required( 'pro' ) !== true,
				'hasProAccess' => affwp_can_access_pro_features(),
			],
		]
	);

	// Admin notifications.
	wp_register_script( 'affwp-admin-notifications', AFFILIATEWP_PLUGIN_URL . 'assets/js/admin-notifications.js', [ 'affwp-admin' ], AFFILIATEWP_VERSION, false );

	// Register admin modals script.
	wp_register_script(
		'affwp-admin-modals',
		AFFILIATEWP_PLUGIN_URL . 'assets/js/admin-modals.js',
		[ 'affwp-admin' ],
		AFFILIATEWP_VERSION,
		false
	);

	// Register shared Alpine helper components.
	wp_register_script(
		'affwp-admin-alpine-components',
		AFFILIATEWP_PLUGIN_URL . 'assets/js/admin-alpine-components.js',
		[ 'affwp-admin' ],
		AFFILIATEWP_VERSION,
		false
	);

	// Addons page.
	wp_register_script( 'affwp_admin_addons', AFFILIATEWP_PLUGIN_URL . "assets/js/admin-addons{$suffix}.js", [ 'jquery' ], AFFILIATEWP_VERSION, true );

	// Setup Screen page.
	wp_register_script( 'affiliate-wp-setup-screen', AFFILIATEWP_PLUGIN_URL . "assets/js/setup-screen{$suffix}.js", [ 'jquery' ], AFFILIATEWP_VERSION, true );

	$screen = get_current_screen();

	if ( ! $screen || 'affiliates_page_affiliate-wp-creatives' !== $screen->id ) {
		return; // Bail if isn't the Creatives admin page.
	}

	// Loads the QR Code script to the Creative pages.
	affiliatewp_load_qrcode_admin_scripts();
}

/**
 * Load resources for QR Code features for the admin screens.
 *
 * @since 2.17.0
 *
 * @return void
 */
function affiliatewp_load_qrcode_admin_scripts() {

	// Enqueue the QR Code scripts.
	affiliate_wp()->scripts->enqueue( 'affiliatewp-qrcode' );

	$action = filter_input( INPUT_GET, 'action' );

	if ( ! in_array( $action, [ 'add_creative', 'edit_creative' ], true ) ) {
		return; // We don't need to load any other resources.
	}

	// Enqueue the color picker.
	wp_enqueue_style( 'wp-color-picker' );
	wp_enqueue_script( 'wp-color-picker' );

	$creative_url = '';

	if ( 'edit_creative' === $action ) {

		$creative     = affwp_get_creative( absint( $_GET['creative_id'] ?? 0 ) );
		$creative_url = $creative->url ?? '';
	}

	$urls = wp_json_encode(
		[
			'creativeUrl' => ! empty( $creative_url )
				? esc_js( $creative_url )
				: get_site_url(),
			'siteUrl'     => get_site_url(),
		]
	);

	wp_add_inline_script(
		'affwp-admin',
		"const affwpCreativeUrls = {$urls};",
		'before'
	);
}

/**
 * Global scripts.
 *
 * These scripts will always be registered for both
 * frontend and backend use.
 *
 * @since  2.12.0
 */
function affwp_register_global_scripts() {

	$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

	// Register select2 CSS.
	wp_register_style( 'affwp-select2', AFFILIATEWP_PLUGIN_URL . 'assets/css/select2' . $suffix . '.css', [], AFFILIATEWP_VERSION );

	// Register select2 JS lib.
	wp_register_script( 'affwp-select2', AFFILIATEWP_PLUGIN_URL . 'assets/js/select2' . $suffix . '.js', [ 'jquery' ], AFFILIATEWP_VERSION, false );
}
add_action( 'wp_enqueue_scripts', 'affwp_register_global_scripts' );
add_action( 'admin_enqueue_scripts', 'affwp_register_global_scripts' );

/**
 *  Load the frontend scripts and styles
 *
 *  @since 1.0
 *  @return void
 */
function affwp_frontend_scripts_and_styles() {

	global $post;

	if ( ! is_object( $post ) ) {
		return;
	}

	$script_deps = [ 'jquery' ];
	$style_deps  = [];

	if ( 'graphs' === affwp_get_active_affiliate_area_tab() || isset( $_REQUEST['tab'] ) && 'graphs' === sanitize_key( $_REQUEST['tab'] ) ) {
		$script_deps[] = 'jquery-ui-datepicker';
		$style_deps[]  = 'jquery-ui-css';
	}

	$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
	wp_register_style( 'affwp-forms', AFFILIATEWP_PLUGIN_URL . 'assets/css/forms' . $suffix . '.css', $style_deps, AFFILIATEWP_VERSION );

	wp_register_style( 'jquery-ui-css', AFFILIATEWP_PLUGIN_URL . 'assets/css/vendor/jquery-ui/jquery-ui-fresh.min.css' );

	$recaptcha_url  = 'https://www.google.com/recaptcha/api.js';
	$recaptcha_deps = [];

	// Configure reCAPTCHA script based on version
	$is_recaptcha_enabled = AffWP_Captcha_Manager::is_enabled( 'recaptcha' );
	$recaptcha_version    = affiliate_wp()->settings->get( 'recaptcha_type', '' );

	if ( $is_recaptcha_enabled ) {
		if ( 'v3' === $recaptcha_version ) {
			// reCAPTCHA v3 requires the site key in the render parameter
			$site_key = affiliate_wp()->settings->get( 'recaptcha_site_key', '' );
			if ( ! empty( $site_key ) ) {
				$recaptcha_url = add_query_arg(
					[
						'render' => $site_key,
					],
					$recaptcha_url
				);
			}
		} else {
			// reCAPTCHA v2 needs onload callback and explicit rendering
			$recaptcha_url = add_query_arg(
				[
					'onload' => 'affwpRecaptchaOnload',
					'render' => 'explicit',
				],
				$recaptcha_url
			);
			// Make sure frontend script loads first since it contains the callback
			$recaptcha_deps[] = 'affwp-frontend';
		}
	}
	wp_register_script( 'affwp-recaptcha', $recaptcha_url, $recaptcha_deps, AFFILIATEWP_VERSION );

	wp_register_script( 'affwp-turnstile', 'https://challenges.cloudflare.com/turnstile/v0/api.js', [], AFFILIATEWP_VERSION );

	wp_register_script( 'affwp-hcaptcha', 'https://js.hcaptcha.com/1/api.js', [], AFFILIATEWP_VERSION );

	wp_register_script( 'affwp-frontend', AFFILIATEWP_PLUGIN_URL . 'assets/js/frontend' . $suffix . '.js', $script_deps, AFFILIATEWP_VERSION );

	/*
	 * Alpine.js — used on the affiliate area Settings tab so the per-method UI
	 * (Stripe section, Payment Email field, Store Credit balance) can show/hide
	 * reactively as the affiliate changes the Payout Method dropdown. Loads in the
	 * footer with `defer` so it doesn't block first paint; the template enqueues
	 * it conditionally via affwp_enqueue_script().
	 */
	wp_register_script(
		'alpinejs',
		AFFILIATEWP_PLUGIN_URL . 'assets/js/vendor/alpine/alpine.min.js',
		[],
		AFFILIATEWP_VERSION,
		[
			'in_footer' => true,
			'strategy'  => 'defer',
		]
	);

	// Ensure frontend script is loaded when reCAPTCHA v2 is used.
	add_filter(
		'affwp_enqueue_script_affwp-recaptcha',
		function ( $enqueue ) {
			if ( $enqueue && AffWP_Captcha_Manager::is_enabled( 'recaptcha' ) && 'v2' === affiliate_wp()->settings->get( 'recaptcha_type', '' ) ) {
				// Also enqueue the frontend script which contains the callback.
				affwp_enqueue_script( 'affwp-frontend' );
			}
			return $enqueue;
		},
		10
	);

	// Add priority loading for AffiliateWP reCAPTCHA v3 to ensure it loads first.
	add_action(
		'wp_head',
		function () {
			if ( AffWP_Captcha_Manager::is_enabled( 'recaptcha' ) && 'v3' === affiliate_wp()->settings->get( 'recaptcha_type', '' ) ) {
				$site_key = affiliate_wp()->settings->get( 'recaptcha_site_key', '' );
				if ( ! empty( $site_key ) ) {
					// Preload the reCAPTCHA v3 script to ensure it loads before any conflicts.
					echo "<link rel='preload' href='https://www.google.com/recaptcha/api.js?render=" . esc_attr( $site_key ) . "' as='script'>\n";
				}
			}
		},
		1
	);

	wp_localize_script(
		'affwp-frontend',
		'affwp_vars',
		[
			'affwp_version'                  => AFFILIATEWP_VERSION,
			'permalinks'                     => get_option( 'permalink_structure' ),
			'pretty_affiliate_urls'          => affwp_is_pretty_referral_urls(),
			'currency_sign'                  => affwp_currency_filter( '' ),
			'currency_pos'                   => affiliate_wp()->settings->get( 'currency_position', 'before' ),
			'invalid_url'                    => __( 'Please enter a valid URL for this site', 'affiliate-wp' ),
			'personal_account_country_label' => __( 'Your Country of Residence', 'affiliate-wp' ),
			'business_account_country_label' => __( 'Country Where The Business Is Legally Established', 'affiliate-wp' ),
			'recaptcha_version'              => AffWP_Captcha_Manager::is_enabled( 'recaptcha' ) ? affiliate_wp()->settings->get( 'recaptcha_type', '' ) : '',
		]
	);

	/**
	 * Filters whether to force frontend scripts to be enqueued.
	 *
	 * @since 1.0
	 *
	 * @param bool $force Whether to force frontend scripts. Default false.
	 */
	if ( true === apply_filters( 'affwp_force_frontend_scripts', false ) ) {
		affwp_enqueue_script( 'affwp-frontend', 'force_frontend_scripts' );
	}

	// Always enqueue the 'affwp-forms' stylesheet.
	affwp_enqueue_style( 'affwp-forms' );

	if ( affwp_is_affiliate_area() ) {
		// Alpine powers the reactive dashboard areas; registered footer + defer, so loading it area-wide is non-blocking.
		wp_enqueue_script( 'alpinejs' );

		// Hide Alpine-cloaked elements until Alpine boots, preventing a flash of all x-show branches at once.
		wp_add_inline_style( 'affwp-forms', '#affwp-affiliate-dashboard [x-cloak] { display: none !important; }' );
	}
}
add_action( 'wp_enqueue_scripts', 'affwp_frontend_scripts_and_styles' );

/**
 * Filters whether to enqueue reCAPTCHA via AffiliateWP to maintain GravityForms compatibility.
 *
 * @since 1.9.8
 *
 * @param bool $enqueue Whether to enqueue the script. Default true.
 * @return bool Whether to enqueue the script.
 */
function affwp_enqueue_recaptcha_gravityforms_compat( $enqueue ) {
	// Check for Gravity Forms reCAPTCHA script handle variations
	if ( wp_script_is( 'gform-recaptcha', 'enqueued' ) || wp_script_is( 'gform_recaptcha', 'enqueued' ) ) {
		$enqueue = false;
	}

	// If AffiliateWP is using v3 and Gravity Forms is active, prevent v2 conflicts
	if ( $enqueue && AffWP_Captcha_Manager::is_enabled( 'recaptcha' ) && 'v3' === affiliate_wp()->settings->get( 'recaptcha_type', '' ) ) {
		// Check if Gravity Forms is likely to load reCAPTCHA v2
		global $wp_scripts;
		if ( isset( $wp_scripts->registered['gform_recaptcha'] ) ) {
			$gf_recaptcha_src = $wp_scripts->registered['gform_recaptcha']->src;
			// If GF is loading v2 (render=explicit), don't enqueue our v3 script
			if ( strpos( $gf_recaptcha_src, 'render=explicit' ) !== false ) {
				$enqueue = false;
			}
		}
	}

	return $enqueue;
}
add_filter( 'affwp_enqueue_script_affwp-recaptcha', 'affwp_enqueue_recaptcha_gravityforms_compat' );
