<?php
/**
 * Admin: Payout Schedule Drawer
 *
 * Slide-over create/edit drawer for a payout schedule, matching the Batches /
 * bulk-pay drawers. The form is rendered once (server-side) with the shared
 * design-system components (segmented control, select, input, toggle) bound to
 * the `scheduleDrawer` Alpine store via x-model; the store
 * (assets/js/affwp-schedule-drawer.js) populates it on open — blank for "Add",
 * or from the row's JSON for "Edit". Saving is a normal POST to the
 * `affwp_save_payout_schedule` handler (no AJAX), which redirects back to the tab.
 *
 * @package     AffiliateWP
 * @subpackage  Admin/Payouts
 * @copyright   Copyright (c) 2026, Awesome Motive, Inc
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       2.35.0
 */

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

/**
 * Renders the schedule drawer once per request.
 *
 * @since 2.35.0
 */
function affwp_render_schedule_drawer() {

	static $rendered = false;
	if ( $rendered ) {
		return;
	}
	$rendered = true;

	// Entitled users see the create/edit form; gated users see the upsell card.
	$can_access = ! function_exists( 'affwp_can_access_pro_features' ) || affwp_can_access_pro_features();

	affwp_drawer(
		array(
			'id'                => 'schedule-drawer',
			'alpine_store'      => 'scheduleDrawer',
			'width'             => 'xl',
			'header_style'      => 'flat',
			// Active/Paused state inline with the schedule name in the header (edit mode), matching the payout/batch/referral drawers.
			'title_badge_callback' => 'affwp_render_schedule_drawer_title_badge',
			/*
			 * dirty_check disables backdrop close (an accidental click outside a
			 * form drawer must not discard work) and routes Escape/X/Cancel
			 * through confirmClose(), which prompts only when the form is dirty.
			 * Reserved for the actual form: the gated upsell drawer has nothing to
			 * lose, so it allows click-away/Escape close and drops its footer (the
			 * upsell card's CTA + the header X are the only affordances it needs).
			 */
			'dirty_check'       => $can_access,
			'no_footer'         => ! $can_access,
			'close_on_escape'   => true,
			'content_callback'  => 'affwp_render_schedule_drawer_content',
			// Pinned recap between the scrolling body and the footer. Entitled users only — the gated view is the upsell, not a summary.
			'summary_callback'  => $can_access ? 'affwp_render_schedule_drawer_summary' : null,
			'footer_callback'   => 'affwp_render_schedule_drawer_footer',
		)
	);

	/*
	 * Dedicated Alpine store backing the shared affiliate/group picker (the same
	 * component the Pay Affiliates screen uses). Its selectedAffiliates/
	 * selectedGroups drive the drawer's hidden include_affiliates[]/
	 * include_groups[] inputs; the scheduleDrawer store seeds it on open.
	 */
	$picker_nonce = wp_create_nonce( 'affwp_affiliate_picker' );
	$picker       = affwp_affiliate_picker_alpine( $picker_nonce, array( 'include_groups' => true ) );
	?>
	<script>
	document.addEventListener( 'alpine:init', function () {
		if ( Alpine.store( 'schedulePicker' ) ) {
			return;
		}
		Alpine.store( 'schedulePicker', {
			<?php
			foreach ( $picker['state'] as $key => $value ) {
				echo "\t\t\t" . esc_js( $key ) . ': ' . wp_json_encode( $value ) . ",\n";
			}
			?>
			// Reset/seed the selection from the drawer (create or edit).
			seed( affiliates, groups ) {
				this.selectedAffiliates = Array.isArray( affiliates ) ? affiliates.slice() : [];
				this.selectedGroups     = Array.isArray( groups ) ? groups.slice() : [];
				this.affiliateSearch    = '';
				this.searchResults      = [];
			},
			// Picker methods call this on add/remove — forward it to the drawer's dirty flag.
			_markDirty() {
				var d = Alpine.store( 'scheduleDrawer' );
				if ( d && d._markDirty ) { d._markDirty(); }
			},
			<?php echo $picker['methods']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- server-built JS fragment. ?>
		} );
	} );
	</script>
	<?php

	affwp_toast();
}

/**
 * Drawer body: the schedule form, built from the shared components and bound to
 * the scheduleDrawer store.
 *
 * @since 2.35.0
 */
function affwp_render_schedule_drawer_content() {

	$model = '$store.scheduleDrawer.form';

	/*
	 * Automatic Payouts is a Pro feature. Non-Pro admins still open the same
	 * drawer — the real form stays visible (blurred + inert) behind an upsell
	 * card, so the value is obvious. Gated server-side; the footer matches.
	 */
	$can_access = ! function_exists( 'affwp_can_access_pro_features' ) || affwp_can_access_pro_features();

	// A configured holding period gates the bypass control.
	$holding_period = (int) affiliate_wp()->settings->get( 'commission_holding_period', 0 );

	/*
	 * Weekday options: keys are the stored day number (0=Sun … 6=Sat, matching
	 * affwp_calculate_next_run()), ordered from the site's "Week starts on"
	 * setting and labelled with the shared localized day names.
	 */
	$weekday_labels  = affwp_payout_schedule_weekday_labels();
	$start_of_week   = (int) get_option( 'start_of_week', 1 );
	$weekday_options = array();
	for ( $i = 0; $i < 7; $i++ ) {
		$dow                     = ( $start_of_week + $i ) % 7;
		$weekday_options[ $dow ] = $weekday_labels[ $dow ];
	}

	// Day-of-month options as ordinals ("1st" … "28th", "Last day"), matching the list cadence + summary.
	$monthday_options = affwp_payout_schedule_monthday_options();

	?>
	<div class="affwp-ui relative">
	<form id="affwp-schedule-form" method="post" action="<?php echo esc_url( affwp_admin_url( 'payouts', array( 'tab' => 'schedules' ) ) ); ?>"
		class="space-y-6<?php echo $can_access ? '' : ' blur-xs pointer-events-none select-none'; ?>"<?php echo $can_access ? '' : ' inert'; ?>
		@input="<?php echo esc_attr( '$store.scheduleDrawer._markDirty()' ); ?>"
		@change="<?php echo esc_attr( '$store.scheduleDrawer._markDirty()' ); ?>">

		<input type="hidden" name="affwp_action" value="save_payout_schedule" />
		<input type="hidden" name="schedule_id" :value="<?php echo esc_attr( "{$model}.id" ); ?>" />
		<?php // Return to wherever the drawer was opened — the payout preview keeps the admin in context to finish paying (validated + fallback in the save handler). ?>
		<input type="hidden" name="redirect_to" value="<?php echo esc_url( add_query_arg( array() ) ); ?>" />
		<?php wp_nonce_field( 'affwp-save-payout-schedule', '_wpnonce' ); ?>

		<?php // Mirror the picker's union selection (affiliates + groups) as hidden inputs for the POST. ?>
		<template x-for="aff in <?php echo esc_attr( '$store.schedulePicker.selectedAffiliates' ); ?>" :key="'aff-' + aff.id">
			<input type="hidden" name="include_affiliates[]" :value="aff.id" />
		</template>
		<template x-for="grp in <?php echo esc_attr( '$store.schedulePicker.selectedGroups' ); ?>" :key="'grp-' + grp.id">
			<input type="hidden" name="include_groups[]" :value="grp.id" />
		</template>

		<!-- Status (edit only): the schedule's operational state, kept above the definition so "is this running?" is answered first, not buried. -->
		<div x-show="<?php echo esc_attr( '! $store.scheduleDrawer.isNew' ); ?>" x-cloak>
			<?php
			affwp_segmented_control( array(
				'name'         => 'status',
				'label'        => __( 'Status', 'affiliate-wp' ),
				'alpine_model' => "{$model}.status",
				'options'      => array(
					'active' => __( 'Active', 'affiliate-wp' ),
					'paused' => __( 'Paused', 'affiliate-wp' ),
				),
			) );
			?>
		</div>

		<?php
		/*
		 * Field order matches how an admin reasons about a schedule (pay who, how
		 * often, above what minimum) and how the Summary below reads it back. Name
		 * is last and auto-fills from the selection, so the drawer never opens on
		 * an abstract naming task.
		 */
		?>

		<!-- Affiliates (who): a union of individuals + groups (the shared picker). Empty = all eligible. -->
		<div>
			<span class="block text-sm font-medium text-gray-900 mb-2"><?php esc_html_e( 'Affiliates', 'affiliate-wp' ); ?></span>
			<?php
			affwp_affiliate_picker( array(
				'store'              => '$store.schedulePicker',
				'id'                 => 'schedule-include',
				'placeholder'        => __( 'Search affiliates or groups…', 'affiliate-wp' ),
				'include_groups'     => true,
				'show_group'         => true,
				'chips_inside_input' => true,
				'size'               => 'md',
			) );
			?>
			<p class="mt-1.5 text-sm text-gray-500 m-0"><?php esc_html_e( 'Leave empty to include every eligible affiliate. Manual-only affiliates are skipped.', 'affiliate-wp' ); ?></p>
		</div>

		<!-- Frequency (when) -->
		<div>
			<?php
			affwp_segmented_control( array(
				'name'         => 'frequency',
				'label'        => __( 'Frequency', 'affiliate-wp' ),
				'alpine_model' => "{$model}.frequency",
				'options'      => array(
					'weekly'  => __( 'Weekly', 'affiliate-wp' ),
					'monthly' => __( 'Monthly', 'affiliate-wp' ),
				),
			) );
			?>
			<div class="mt-3" x-show="<?php echo esc_attr( "{$model}.frequency === 'weekly'" ); ?>">
				<?php
				affwp_select( array(
					'name'         => 'frequency_day_week',
					'alpine_model' => "{$model}.frequency_day_week",
					'max_width'    => 'max-w-48',
					'options'      => $weekday_options,
				) );
				?>
			</div>
			<div class="mt-3" x-show="<?php echo esc_attr( "{$model}.frequency === 'monthly'" ); ?>" x-cloak>
				<?php
				affwp_select( array(
					'name'         => 'frequency_day_month',
					'alpine_model' => "{$model}.frequency_day_month",
					'max_width'    => 'max-w-48',
					'options'      => $monthday_options,
				) );
				?>
			</div>
		</div>

		<!-- Minimum -->
		<div>
			<label for="affwp-schedule-minimum" class="block text-sm font-medium text-gray-900 mb-2"><?php esc_html_e( 'Minimum earnings', 'affiliate-wp' ); ?></label>
			<?php
			affwp_input( array(
				'name'         => 'minimum_amount',
				'id'           => 'affwp-schedule-minimum',
				'type'         => 'number',
				'min'          => 0,
				// Whole dollars: a minimum-earnings threshold is a round-number floor, not a price. The server still compares cent-precise unpaid totals.
				'step'         => 1,
				'integer'      => true, // Block decimal entry up front (no step-validation bubble).
				'width'        => 'narrow',
				'placeholder'  => '0',
				'prefix'       => affwp_currency_filter( '' ),
				'alpine_model' => "{$model}.minimum",
			) );
			?>
			<p class="mt-1.5 text-sm text-gray-500 m-0"><?php esc_html_e( 'Only affiliates whose unpaid earnings reach this amount are paid. Smaller amounts roll over to a future run.', 'affiliate-wp' ); ?></p>
		</div>

		<?php if ( $holding_period > 0 ) : ?>
			<?php // Only shown when a holding period is configured. Toggle-left with the label beside it, matching the Pay Affiliates screen — a lone boolean reads best next to its label, not pushed to the far edge. ?>
			<div>
				<label for="affwp-schedule-bypass" class="flex items-start gap-3 text-sm cursor-pointer">
					<span class="flex h-lh items-center shrink-0">
						<?php
						affwp_toggle( array(
							'name'         => 'bypass_holding_period',
							'id'           => 'affwp-schedule-bypass',
							'label'        => __( 'Include commissions still in the holding period', 'affiliate-wp' ),
							'size'         => 'md',
							'color'        => 'blue',
							'alpine_model' => "{$model}.bypass",
						) );
						?>
					</span>
					<span class="font-medium text-gray-900">
						<?php
						printf(
							/* translators: %s: number of days in the commission holding period */
							esc_html( _n( 'Include commissions still in the %s-day holding period', 'Include commissions still in the %s-day holding period', $holding_period, 'affiliate-wp' ) ),
							esc_html( number_format_i18n( $holding_period ) )
						);
						?>
					</span>
				</label>
			</div>
		<?php endif; ?>

		<!-- Name (last): a label for the schedule you've defined. Auto-fills from the selection until the admin types their own. -->
		<div>
			<label for="affwp-schedule-name" class="block text-sm font-medium text-gray-900 mb-2"><?php esc_html_e( 'Schedule name', 'affiliate-wp' ); ?></label>
			<?php
			affwp_input( array(
				'name'         => 'name',
				'id'           => 'affwp-schedule-name',
				'type'         => 'text',
				'placeholder'  => __( 'Monthly payout', 'affiliate-wp' ),
				'alpine_model' => "{$model}.name",
				// maxlength mirrors the server cap in schedule-actions.php; @input stops the auto-fill once the admin types a name of their own.
				'attributes'   => array(
					'maxlength' => '45',
					'@input'    => '$store.scheduleDrawer.markNameTouched()',
				),
			) );
			?>
		</div>

	</form>
	<?php if ( ! $can_access ) : ?>
		<?php
		/*
		 * Non-Pro gate: shared overlay card over the blurred form, so the real UI
		 * stays visible and the value is obvious. The value line is a historical
		 * fact (runs the admin did themselves), never a per-run forecast.
		 */
		$run_days_line = affwp_get_manual_payout_run_days_line();

		affwp_pro_upsell_overlay(
			array(
				'icon_svg'    => \AffiliateWP\Utils\Icons::generate( 'clock-coins', '', array( 'aria-hidden' => 'true' ) ),
				'title'       => __( 'Pay affiliates automatically', 'affiliate-wp' ),
				'description' => __( 'Set a schedule once and every affiliate is paid on time, every time, each through their own payout method.', 'affiliate-wp' ),
				'value_line'  => '' !== $run_days_line ? array( 'text' => $run_days_line ) : array(),
				'utm_medium'  => 'automatic-payouts-drawer',
				'utm_content' => 'save-as-schedule',
			)
		);
		?>
	<?php endif; ?>
	</div>
	<?php
}

/**
 * Drawer title badge: the schedule's Active/Paused state, inline with its name
 * in the header (edit mode only), matching the payout/batch/referral drawers.
 * Display-only — it reflects form.status live; editing happens via the body
 * control. New schedules show nothing (they're created active).
 *
 * @since 2.35.0
 */
function affwp_render_schedule_drawer_title_badge() {

	$model = '$store.scheduleDrawer.form';
	?>
	<template x-if="<?php echo esc_attr( '! $store.scheduleDrawer.isNew' ); ?>">
		<span>
			<span x-show="<?php echo esc_attr( "{$model}.status === 'active'" ); ?>"><?php echo affwp_get_status_badge( 'active' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge helper returns trusted HTML. ?></span>
			<span x-show="<?php echo esc_attr( "{$model}.status !== 'active'" ); ?>" x-cloak><?php echo affwp_get_status_badge( 'inactive', array( 'text' => __( 'Paused', 'affiliate-wp' ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge helper returns trusted HTML. ?></span>
		</span>
	</template>
	<?php
}

/**
 * Drawer summary: a fixed, always-visible recap of what the schedule will do.
 *
 * Rendered in the shell's pinned summary slot (between the scrolling body and
 * the footer) so it stays in view while the fields above scroll. The sentence
 * is composed live in JS ($store.scheduleDrawer.summary) from the same audience
 * phrase the auto-name uses, so the two never disagree.
 *
 * @since 2.35.0
 */
function affwp_render_schedule_drawer_summary() {
	?>
	<div class="shrink-0 border-t border-gray-200 bg-gray-50 px-6 py-4">
		<p class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 m-0 mb-1"><?php esc_html_e( 'Summary', 'affiliate-wp' ); ?></p>
		<p class="text-sm/6 text-gray-900 m-0" x-text="<?php echo esc_attr( '$store.scheduleDrawer.summary' ); ?>"></p>
	</div>
	<?php
}

/**
 * Drawer footer: Cancel + Save (submits the drawer form).
 *
 * @since 2.35.0
 */
function affwp_render_schedule_drawer_footer() {

	// Match the body's gate: no submit for non-Pro (the form is inert; the upsell card carries the CTA).
	$can_access = ! function_exists( 'affwp_can_access_pro_features' ) || affwp_can_access_pro_features();
	?>
	<div x-data="{ get d() { return $store.scheduleDrawer; } }" class="flex items-center justify-end gap-2">
		<button type="button" @click="d.confirmClose()"
			class="cursor-pointer rounded-md bg-transparent border-0 px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700 focus:outline-hidden">
			<?php echo $can_access ? esc_html__( 'Cancel', 'affiliate-wp' ) : esc_html__( 'Close', 'affiliate-wp' ); ?>
		</button>
		<?php if ( $can_access ) : ?>
			<button type="submit" form="affwp-schedule-form"
				:disabled="d.saving"
				class="cursor-pointer inline-flex items-center gap-2 rounded-md border border-transparent bg-affwp-brand-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-affwp-brand-600 focus:outline-hidden disabled:opacity-50 disabled:cursor-not-allowed">
				<?php // Spinner during the AJAX save — matches the bulk-pay / next-steps drawer buttons. ?>
				<svg x-show="d.saving" x-cloak class="animate-spin -ml-1 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
					<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
					<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
				</svg>
				<span x-text="<?php echo esc_attr( "d.saving ? ( d.isNew ? '" . esc_js( __( 'Creating…', 'affiliate-wp' ) ) . "' : '" . esc_js( __( 'Saving…', 'affiliate-wp' ) ) . "' ) : ( d.isNew ? '" . esc_js( __( 'Create schedule', 'affiliate-wp' ) ) . "' : '" . esc_js( __( 'Save changes', 'affiliate-wp' ) ) . "' )" ); ?>"></span>
			</button>
		<?php endif; ?>
	</div>
	<?php
}
