<?php
namespace Pay4All\Pro\Admin\Newsletter;

use Pay4All\Pro\Domain\Newsletter;
use Pay4All\Pro\Domain\NewsletterSendLog;
use Pay4All\Pro\PostTypes;
use Pay4All\Pro\Services\NewsletterSpooler;
use Pay4All\Pro\Services\NewsletterTemplates;

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

/**
 * Admin submenu for the newsletter feature. Owns :
 *
 *   - the sidebar entry (swapped from the free plugin's Pro teaser via
 *     Integrations\Pay4AllForm::swap_teaser_menus()),
 *   - the campaigns list screen (progress bar + actions),
 *   - the admin_post handler for queue / pause / test buttons rendered
 *     by MetaBoxes::render_status_panel(),
 *   - the AJAX endpoint that repopulates the token buttons when the
 *     merchant flips the template kind in the editor,
 *   - the CPT list columns customisation (status + progress).
 */
final class Page {

	public const SLUG                = 'pay4all-form-newsletters';
	public const BULK_ACTION         = 'p4all_nl_bulk';
	public const PER_PAGE_META       = 'p4all_nl_per_page';

	public static function register() : void {
		add_action( 'admin_menu',                        [ self::class, 'add_menu' ], 30 );
		add_action( 'admin_post_p4all_nl_action',        [ self::class, 'handle_action' ] );
		add_action( 'admin_post_' . self::BULK_ACTION,   [ self::class, 'handle_bulk' ] );
		add_action( 'admin_post_p4all_save_signup_button', [ self::class, 'handle_save_signup_button' ] );
		add_action( 'wp_ajax_p4all_nl_tokens',           [ self::class, 'ajax_tokens' ] );
		add_action( 'admin_notices',                     [ self::class, 'render_flash' ] );
		add_filter( 'set-screen-option',                 [ self::class, 'save_screen_option' ], 10, 3 );
		add_filter( 'set_screen_option_' . self::PER_PAGE_META, [ self::class, 'save_screen_option' ], 10, 3 );

		// Enrich the WP-native CPT list with a status + progress column
		// so the merchant sees at a glance which campaigns are still
		// running without opening each one.
		add_filter( 'manage_' . PostTypes::NEWSLETTER . '_posts_columns',        [ self::class, 'list_columns' ] );
		add_action( 'manage_' . PostTypes::NEWSLETTER . '_posts_custom_column',  [ self::class, 'render_column' ], 10, 2 );

		// Purge send-log rows when a campaign is hard-deleted (Trash → Delete Forever).
		add_action( 'before_delete_post', [ self::class, 'on_delete' ] );
	}

	public static function add_menu() : void {
		$menu_parent = defined( '\\Pay4All\\Form\\Admin\\Menu::SLUG_TOP' )
			? \Pay4All\Form\Admin\Menu::SLUG_TOP
			: 'pay4all-form-dashboard';
		$hook = add_submenu_page(
			$menu_parent,
			__( 'Newsletters', 'pay4all-pro' ),
			__( 'Newsletters', 'pay4all-pro' ),
			'manage_options',
			self::SLUG,
			[ self::class, 'render' ]
		);
		if ( $hook ) {
			add_action( 'load-' . $hook, [ self::class, 'on_load' ] );
		}
	}

	/**
	 * Screen Options wiring : per-page selector + column visibility.
	 * Runs on load-{page_hook} so WP grafts the built-in dropdown +
	 * checkboxes onto our custom page without any extra JS.
	 */
	public static function on_load() : void {
		add_screen_option( 'per_page', [
			'label'   => __( 'Newsletters per page', 'pay4all-pro' ),
			'default' => 20,
			'option'  => self::PER_PAGE_META,
		] );
		add_filter( 'manage_' . get_current_screen()->id . '_columns', [ self::class, 'screen_columns' ] );
	}

	/**
	 * Column catalogue for the Screen Options « Hide columns » checkbox
	 * list. Title + Actions are omitted from the toggle so the merchant
	 * always has a way to open a campaign.
	 *
	 * @return array<string,string>
	 */
	public static function screen_columns() : array {
		return [
			'type'        => __( 'Type',        'pay4all-pro' ),
			'status'      => __( 'Status',      'pay4all-pro' ),
			'progress'    => __( 'Progress',    'pay4all-pro' ),
			'started_at'  => __( 'Started at',  'pay4all-pro' ),
			'finished_at' => __( 'Finished at', 'pay4all-pro' ),
		];
	}

	/**
	 * Persists the merchant's per-page choice. Whitelists our option
	 * key so WP's default « unknown option » guard doesn't drop it.
	 *
	 * @param mixed  $status
	 * @param string $option
	 * @param mixed  $value
	 * @return mixed
	 */
	public static function save_screen_option( $status, $option, $value ) {
		if ( $option === self::PER_PAGE_META ) {
			$v = (int) $value;
			return $v > 0 && $v <= 200 ? $v : 20;
		}
		return $status;
	}

	private static function per_page() : int {
		$uid = get_current_user_id();
		if ( $uid <= 0 ) { return 20; }
		$v = (int) get_user_meta( $uid, self::PER_PAGE_META, true );
		return $v > 0 ? $v : 20;
	}

	private static function hidden_columns() : array {
		$screen = get_current_screen();
		if ( ! $screen ) { return []; }
		return array_values( array_filter( (array) get_hidden_columns( $screen ) ) );
	}

	public static function render() : void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'pay4all-pro' ) );
		}
		// Sub-view routing : `?action=stats&campaign=ID` delegates to the
		// per-campaign statistics screen so we keep a single sidebar entry
		// rather than a second submenu the merchant has to hunt for.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view routing.
		$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( (string) $_GET['action'] ) ) : '';
		if ( $action === 'stats' ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view routing.
			$campaign = isset( $_GET['campaign'] ) ? absint( wp_unslash( $_GET['campaign'] ) ) : 0;
			StatsPage::render( $campaign );
			return;
		}
		$new_url  = admin_url( 'post-new.php?post_type=' . PostTypes::NEWSLETTER );
		$per_page = self::per_page();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only paginator.
		$paged    = isset( $_GET['paged'] ) ? max( 1, absint( wp_unslash( $_GET['paged'] ) ) ) : 1;
		$q = new \WP_Query( [
			'post_type'      => PostTypes::NEWSLETTER,
			'post_status'    => [ 'publish', 'draft' ],
			'posts_per_page' => $per_page,
			'paged'          => $paged,
			'orderby'        => 'date',
			'order'          => 'DESC',
		] );
		$campaigns   = $q->posts;
		$total_pages = (int) $q->max_num_pages;
		$hidden      = self::hidden_columns();
		$is_hidden   = static function ( string $col ) use ( $hidden ) : string {
			return in_array( $col, $hidden, true ) ? ' class="hidden"' : '';
		};

		$status_labels = [
			Newsletter::STATUS_DRAFT   => __( 'Draft',   'pay4all-pro' ),
			Newsletter::STATUS_QUEUED  => __( 'Queued',  'pay4all-pro' ),
			Newsletter::STATUS_SENDING => __( 'Sending', 'pay4all-pro' ),
			Newsletter::STATUS_DONE    => __( 'Done',    'pay4all-pro' ),
			Newsletter::STATUS_PAUSED  => __( 'Paused',  'pay4all-pro' ),
		];
		$audience_active = 0;
		$audience_optout = 0;
		if ( class_exists( '\\Pay4All\\Form\\Domain\\Customer' ) ) {
			$audience_active = \Pay4All\Form\Domain\Customer::count_active_recipients();
			$audience_total  = \Pay4All\Form\Domain\Customer::count_all();
			$audience_optout = max( 0, $audience_total - $audience_active );
		}
		?>
		<div class="wrap">
			<h1 class="wp-heading-inline"><?php esc_html_e( 'Newsletters', 'pay4all-pro' ); ?></h1>
			<a href="<?php echo esc_url( $new_url ); ?>" class="page-title-action"><?php esc_html_e( 'Create a newsletter', 'pay4all-pro' ); ?></a>
			<hr class="wp-header-end">

			<p class="description">
				<?php echo esc_html( sprintf(
					/* translators: %d = number of opted-in customers */
					__( 'Current audience : %d customers with promotional e-mails enabled.', 'pay4all-pro' ),
					$audience_active
				) ); ?>
				<br>
				<?php echo esc_html( sprintf(
					/* translators: %d = number of opted-out customers */
					__( 'Opted-out : %d.', 'pay4all-pro' ),
					$audience_optout
				) ); ?>
				<br>
				<?php
				// Server date + time — useful when comparing the merchant's
				// schedule pick against the actual clock the cron sees.
				// Uses WP's configured date / time formats + wp_date() so
				// the value matches every other timestamp on the page.
				$now_fmt = trim( (string) get_option( 'date_format', 'Y-m-d' ) . ' ' . (string) get_option( 'time_format', 'H:i' ) );
				echo esc_html( sprintf(
					/* translators: %s = server date + time */
					__( 'Server date and time : %s', 'pay4all-pro' ),
					wp_date( $now_fmt )
				) );
				?>
			</p>

			<?php self::render_shortcodes_panel(); ?>

			<?php if ( empty( $campaigns ) ) : ?>
				<div class="notice notice-info" style="padding:14px 16px;">
					<p style="margin:0;"><?php esc_html_e( 'No newsletter yet. Click "Create a newsletter" to write your first campaign.', 'pay4all-pro' ); ?></p>
				</div>
			<?php else :
				$fmt = trim( (string) get_option( 'date_format', 'Y-m-d' ) . ' ' . (string) get_option( 'time_format', 'H:i' ) );
				$bulk_form_id = 'p4all-nl-bulk-form';
			?>
				<?php // The bulk-delete form lives OUTSIDE the table (the row
					// checkboxes use `form=…` to enrol into it) so the outer
					// wp-admin form doesn't collide with a nested one. Two
					// submit buttons drive the two flavours of destruction:
					// trash (recoverable) or permanent delete. ?>
				<form id="<?php echo esc_attr( $bulk_form_id ); ?>"
				      method="post"
				      action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
					<input type="hidden" name="action" value="<?php echo esc_attr( self::BULK_ACTION ); ?>">
					<?php wp_nonce_field( self::BULK_ACTION, '_wpnonce' ); ?>
				</form>

				<div class="tablenav top" style="margin:8px 0;display:flex;align-items:center;gap:8px;">
					<button type="submit" form="<?php echo esc_attr( $bulk_form_id ); ?>"
					        name="p4all_nl_bulk_op" value="delete"
					        class="button button-link-delete" id="p4all-nl-bulk-delete" disabled
					        onclick="return p4allNlBulkConfirm('delete');">
						<?php esc_html_e( 'Delete permanently', 'pay4all-pro' ); ?>
						(<span class="p4all-nl-bulk-count">0</span>)
					</button>
				</div>

				<table class="wp-list-table widefat striped" id="p4all-nl-table">
					<thead>
						<tr>
							<td class="check-column" style="width:2.2em;">
								<input type="checkbox" id="p4all-nl-select-all" aria-label="<?php esc_attr_e( 'Select all', 'pay4all-pro' ); ?>">
							</td>
							<th><?php esc_html_e( 'Title',       'pay4all-pro' ); ?></th>
							<th<?php echo $is_hidden( 'type' );        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php esc_html_e( 'Type',        'pay4all-pro' ); ?></th>
							<th<?php echo $is_hidden( 'status' );      // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php esc_html_e( 'Status',      'pay4all-pro' ); ?></th>
							<th<?php echo $is_hidden( 'progress' );    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php esc_html_e( 'Progress',    'pay4all-pro' ); ?></th>
							<th<?php echo $is_hidden( 'started_at' );  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php esc_html_e( 'Started at',  'pay4all-pro' ); ?></th>
							<th<?php echo $is_hidden( 'finished_at' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php esc_html_e( 'Finished at', 'pay4all-pro' ); ?></th>
							<th><?php esc_html_e( 'Statistics',  'pay4all-pro' ); ?></th>
						</tr>
					</thead>
					<tbody>
					<?php foreach ( $campaigns as $post ) :
						$id       = (int) $post->ID;
						$status   = Newsletter::status( $id );
						$sent     = NewsletterSendLog::count_sent( $id );
						$total    = Newsletter::audience_total( $id );
						if ( $total <= 0 ) { $total = $audience_active; }
						$edit_url = admin_url( 'post.php?post=' . $id . '&action=edit' );
						$kind     = Newsletter::kind( $id );
						$started_iso  = (string) get_post_meta( $id, Newsletter::META_STARTED_AT,  true );
						$finished_iso = (string) get_post_meta( $id, Newsletter::META_FINISHED_AT, true );
						$started_lbl  = $started_iso  !== '' ? wp_date( $fmt, (int) strtotime( $started_iso  . ' UTC' ) ) : '-';
						$finished_lbl = $finished_iso !== '' ? wp_date( $fmt, (int) strtotime( $finished_iso . ' UTC' ) ) : '-';
						// Queued campaign with a future schedule : surface
						// the picked date in the « Started at » cell (in a
						// muted color) so the merchant sees when the
						// broadcast will actually start. Falls back to the
						// standard « - » when no schedule was set.
						$started_html = esc_html( $started_lbl );
						if ( $started_iso === '' && $status === Newsletter::STATUS_QUEUED ) {
							$sched_iso = (string) get_post_meta( $id, Newsletter::META_SCHEDULED_AT, true );
							if ( $sched_iso !== '' ) {
								$sched_lbl    = wp_date( $fmt, (int) strtotime( $sched_iso . ' UTC' ) );
								$started_html = '<span style="color:#8c8f94;">' . esc_html( $sched_lbl ) . '</span>';
							}
						}
						?>
						<tr>
							<td class="check-column">
								<input type="checkbox"
								       class="p4all-nl-row-cb"
								       name="ids[]"
								       value="<?php echo (int) $id; ?>"
								       form="<?php echo esc_attr( $bulk_form_id ); ?>"
								       style="margin-left:10px;"
								       aria-label="<?php echo esc_attr( sprintf(
								           /* translators: %s = newsletter title */
								           __( 'Select %s', 'pay4all-pro' ),
								           $post->post_title !== '' ? $post->post_title : '#' . $id
								       ) ); ?>">
							</td>
							<td><a href="<?php echo esc_url( $edit_url ); ?>"><strong><?php echo esc_html( $post->post_title !== '' ? $post->post_title : '#' . $id ); ?></strong></a></td>
							<td<?php echo $is_hidden( 'type' );        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
								<?php
								switch ( $kind ) {
									case Newsletter::KIND_PRODUCT: esc_html_e( 'Product', 'pay4all-pro' ); break;
									case Newsletter::KIND_PROMO:   esc_html_e( 'Promo code', 'pay4all-pro' ); break;
									default: esc_html_e( 'Free', 'pay4all-pro' );
								}
								?>
							</td>
							<td<?php echo $is_hidden( 'status' );      // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><code><?php echo esc_html( $status_labels[ $status ] ?? $status ); ?></code></td>
							<td<?php echo $is_hidden( 'progress' );    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo esc_html( sprintf( '%d / %d', $sent, $total ) ); ?></td>
							<td<?php echo $is_hidden( 'started_at' );  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo wp_kses( $started_html, [ 'span' => [ 'style' => [] ] ] ); ?></td>
							<td<?php echo $is_hidden( 'finished_at' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>><?php echo esc_html( $finished_lbl ); ?></td>
							<?php
							$stats_url = add_query_arg(
								[ 'page' => self::SLUG, 'action' => 'stats', 'campaign' => $id ],
								admin_url( 'admin.php' )
							);
							?>
							<td>
								<?php if ( $sent > 0 ) : ?>
									<a href="<?php echo esc_url( $stats_url ); ?>" class="button button-small">
										<span class="dashicons dashicons-chart-line" style="vertical-align:middle;font-size:14px;line-height:1.4;"></span>
										<?php esc_html_e( 'Statistics', 'pay4all-pro' ); ?>
									</a>
								<?php else : ?>
									<button type="button" class="button button-small" disabled
									        title="<?php esc_attr_e( 'Statistics will be available once the broadcast starts.', 'pay4all-pro' ); ?>">
										<span class="dashicons dashicons-chart-line" style="vertical-align:middle;font-size:14px;line-height:1.4;"></span>
										<?php esc_html_e( 'Statistics', 'pay4all-pro' ); ?>
									</button>
								<?php endif; ?>
							</td>
						</tr>
					<?php endforeach; ?>
					</tbody>
				</table>

				<?php
				if ( $total_pages > 1 ) {
					$base = add_query_arg( [ 'page' => self::SLUG ], admin_url( 'admin.php' ) );
					$links = paginate_links( [
						'base'      => add_query_arg( 'paged', '%#%', $base ),
						'format'    => '',
						'current'   => $paged,
						'total'     => $total_pages,
						'prev_text' => '&laquo;',
						'next_text' => '&raquo;',
					] );
					if ( $links ) {
						echo '<div class="tablenav bottom" style="margin-top:12px;"><div class="tablenav-pages">' . wp_kses_post( $links ) . '</div></div>';
					}
				}
				?>

				<script>
				(function () {
					var table    = document.getElementById('p4all-nl-table');
					if (!table) { return; }
					var master   = document.getElementById('p4all-nl-select-all');
					var rows     = table.querySelectorAll('.p4all-nl-row-cb');
					var delBtn   = document.getElementById('p4all-nl-bulk-delete');
					var counters = document.querySelectorAll('.p4all-nl-bulk-count');
					function refresh () {
						var n = 0;
						for (var i = 0; i < rows.length; i++) { if (rows[i].checked) { n++; } }
						for (var j = 0; j < counters.length; j++) { counters[j].textContent = String(n); }
						if (delBtn) { delBtn.disabled = (n === 0); }
						if (master) { master.checked  = (n > 0 && n === rows.length); }
					}
					if (master) {
						master.addEventListener('change', function () {
							for (var i = 0; i < rows.length; i++) { rows[i].checked = master.checked; }
							refresh();
						});
					}
					for (var i = 0; i < rows.length; i++) { rows[i].addEventListener('change', refresh); }
					window.p4allNlBulkConfirm = function (op) {
						var n = 0;
						for (var i = 0; i < rows.length; i++) { if (rows[i].checked) { n++; } }
						if (n === 0) { return false; }
						var trashMsg = <?php echo wp_json_encode( __( 'Move the selected newsletters to trash?', 'pay4all-pro' ) ); ?>;
						var delMsg   = <?php echo wp_json_encode( __( 'Delete the selected newsletters PERMANENTLY? This also wipes ALL statistics for the campaign (sent list, opens, clicks and unsubscribes). This action cannot be undone.', 'pay4all-pro' ) ); ?>;
						return window.confirm((op === 'delete' ? delMsg : trashMsg) + ' (' + n + ')');
					};
					refresh();
				})();
				</script>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Bulk trash / delete handler. Called via admin-post on the bulk form
	 * above ; the button's `value` picks between the two operations.
	 */
	public static function handle_bulk() : void {
		if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied.', 'pay4all-pro' ) ); }
		check_admin_referer( self::BULK_ACTION );
		$op = isset( $_POST['p4all_nl_bulk_op'] ) ? sanitize_key( wp_unslash( (string) $_POST['p4all_nl_bulk_op'] ) ) : '';
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- values coerced to int below.
		$ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? (array) wp_unslash( $_POST['ids'] ) : [];
		$done = 0;
		foreach ( $ids as $id ) {
			$id = (int) $id;
			if ( $id <= 0 ) { continue; }
			if ( get_post_type( $id ) !== PostTypes::NEWSLETTER ) { continue; }
			if ( $op === 'delete' ) {
				// Force = true so we skip WP's « trash first » routing.
				// The before_delete_post hook we registered earlier wipes
				// the send-log rows tied to this campaign as a side-effect.
				wp_delete_post( $id, true );
				$done++;
			} else {
				wp_trash_post( $id );
				$done++;
			}
		}
		set_transient(
			'p4all_nl_flash_' . get_current_user_id(),
			$op === 'delete'
				? sprintf( /* translators: %d = number of deleted newsletters */
					_n( '%d newsletter deleted permanently.', '%d newsletters deleted permanently.', $done, 'pay4all-pro' ),
					$done
				)
				: sprintf( /* translators: %d = number of trashed newsletters */
					_n( '%d newsletter moved to trash.', '%d newsletters moved to trash.', $done, 'pay4all-pro' ),
					$done
				),
			60
		);
		wp_safe_redirect( admin_url( 'admin.php?page=' . self::SLUG ) );
		exit;
	}

	// ---------------------------------------------------------------
	// Custom columns on the wp-admin edit.php list for newsletters
	// ---------------------------------------------------------------
	public static function list_columns( array $cols ) : array {
		$new = [];
		foreach ( $cols as $key => $label ) {
			$new[ $key ] = $label;
			if ( $key === 'title' ) {
				$new['p4all_nl_status']   = __( 'Status',   'pay4all-pro' );
				$new['p4all_nl_progress'] = __( 'Progress', 'pay4all-pro' );
			}
		}
		return $new;
	}

	public static function render_column( string $col, int $post_id ) : void {
		if ( $col === 'p4all_nl_status' ) {
			echo esc_html( Newsletter::status( $post_id ) );
			return;
		}
		if ( $col === 'p4all_nl_progress' ) {
			$sent = NewsletterSendLog::count_sent( $post_id );
			$tot  = Newsletter::audience_total( $post_id );
			if ( $tot <= 0 && class_exists( '\\Pay4All\\Form\\Domain\\Customer' ) ) {
				$tot = \Pay4All\Form\Domain\Customer::count_active_recipients();
			}
			echo esc_html( sprintf( '%d / %d', $sent, $tot ) );
		}
	}

	// ---------------------------------------------------------------
	// Actions (queue / pause / test) from the side metabox form
	// ---------------------------------------------------------------
	public static function handle_action() : void {
		if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied.', 'pay4all-pro' ) ); }
		// Buttons are rendered as nonced links now (see MetaBoxes::render_status_box)
		// so the parent post-edit <form> is never disrupted by a nested one.
		// Fall back to $_POST for callers that still submit via form.
		$campaign_id = isset( $_REQUEST['campaign_id'] )              ? absint( wp_unslash( $_REQUEST['campaign_id'] ) )                              : 0;
		$op          = isset( $_REQUEST['p4all_nl_op'] )              ? sanitize_key( wp_unslash( (string) $_REQUEST['p4all_nl_op'] ) )               : '';
		$nonce       = isset( $_REQUEST['_p4all_nl_action_nonce'] )   ? sanitize_text_field( wp_unslash( (string) $_REQUEST['_p4all_nl_action_nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'p4all_nl_action_' . $campaign_id ) ) { wp_die( esc_html__( 'Invalid nonce.', 'pay4all-pro' ) ); }
		if ( $campaign_id <= 0 ) { wp_die( esc_html__( 'Missing campaign.', 'pay4all-pro' ) ); }

		$msg = '';
		switch ( $op ) {
			case 'queue':
				// Optional scheduling : the merchant may have picked a
				// future date + time in the confirmation modal (format
				// « YYYY-MM-DDTHH:mm »). We parse it in the WP timezone
				// so « 14:00 » means 14:00 merchant-local, then persist
				// the equivalent UTC gmdate() so the spooler cursor stays
				// tz-agnostic.
				$sched_raw = isset( $_REQUEST['scheduled_at'] ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['scheduled_at'] ) ) : '';
				$now_ts    = time();
				$sched_utc = '';
				if ( $sched_raw !== '' && preg_match( '/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})$/', $sched_raw, $m ) ) {
					try {
						$tz     = wp_timezone();
						$picked = new \DateTimeImmutable( $m[1] . 'T' . $m[2] . ':00', $tz );
						if ( $picked->getTimestamp() > $now_ts + 60 ) {
							$sched_utc = gmdate( 'Y-m-d H:i:s', $picked->getTimestamp() );
						}
					} catch ( \Throwable $e ) {
						// Malformed date : ignore + fall back to « send now ».
					}
				}
				if ( $sched_utc !== '' ) {
					update_post_meta( $campaign_id, Newsletter::META_SCHEDULED_AT, $sched_utc );
					Newsletter::set_status( $campaign_id, Newsletter::STATUS_QUEUED );
					$msg = __( 'Broadcast scheduled. Waiting for the chosen date.', 'pay4all-pro' );
				} else {
					// Send-now : clear any stale schedule so the spooler
					// picks it up immediately.
					delete_post_meta( $campaign_id, Newsletter::META_SCHEDULED_AT );
					Newsletter::set_status( $campaign_id, Newsletter::STATUS_QUEUED );
					// Trigger an immediate first tick so the merchant sees
					// progress even before the next cron run.
					NewsletterSpooler::process_batch( $campaign_id );
					$msg = __( 'Broadcast scheduled. First batch dispatched.', 'pay4all-pro' );
				}
				break;
			case 'pause':
				Newsletter::set_status( $campaign_id, Newsletter::STATUS_PAUSED );
				$msg = __( 'Broadcast paused.', 'pay4all-pro' );
				break;
			case 'test':
				$sent = self::send_test( $campaign_id );
				$msg = sprintf(
					/* translators: %d = number of test emails sent */
					_n( 'Test email sent (%d).', 'Test emails sent (%d).', $sent, 'pay4all-pro' ),
					$sent
				);
				break;
		}
		set_transient( 'p4all_nl_flash_' . get_current_user_id(), $msg, 60 );
		wp_safe_redirect( admin_url( 'post.php?post=' . $campaign_id . '&action=edit' ) );
		exit;
	}

	private static function send_test( int $campaign_id ) : int {
		$user = wp_get_current_user();
		$to   = $user && $user->user_email ? $user->user_email : (string) get_option( 'admin_email' );
		if ( ! $to || ! is_email( $to ) ) { return 0; }

		// Fake customer row : ID=0 so send_one() doesn't try to write a
		// send-log entry against a real customer. First / last name are
		// the WP user's for a realistic preview.
		$fake_customer = [
			'id'                 => 0,
			'email'              => $to,
			'firstname'          => $user ? (string) $user->first_name : '',
			'lastname'           => $user ? (string) $user->last_name  : '',
			'lang'               => '',
			'unsubscribe_token'  => str_repeat( '0', 40 ),
		];
		$languages = self::active_languages();
		$sent = 0;
		foreach ( $languages as $lang ) {
			$content = Newsletter::content_for_lang( $campaign_id, $lang );
			if ( trim( $content['subject'] ) === '' && trim( $content['body'] ) === '' ) { continue; }
			if ( NewsletterSpooler::send_one( $campaign_id, $fake_customer, $lang, true ) ) {
				$sent++;
			}
		}
		return $sent;
	}

	private static function active_languages() : array {
		if ( ! class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' ) ) { return [ 'en' ]; }
		$langs = (array) \Pay4All\Form\Admin\Settings\Schema::get( 'languages', [] );
		$langs = array_values( array_intersect( $langs, \Pay4All\Form\Admin\Settings\Schema::SUPPORTED_LANGUAGES ) );
		if ( empty( $langs ) ) { $langs = \Pay4All\Form\Admin\Settings\Schema::default_languages(); }
		return $langs;
	}

	// ---------------------------------------------------------------
	// AJAX : re-render the token catalogue when the kind picker flips
	// ---------------------------------------------------------------
	public static function ajax_tokens() : void {
		if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error(); }
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only endpoint (returns the localised token catalogue) gated by manage_options ; no state mutation, no nonce needed.
		$kind = isset( $_GET['kind'] ) ? sanitize_key( wp_unslash( (string) $_GET['kind'] ) ) : Newsletter::KIND_FREE;
		if ( ! in_array( $kind, Newsletter::KINDS, true ) ) { $kind = Newsletter::KIND_FREE; }
		$bag = NewsletterTemplates::available_tokens( $kind );
		wp_send_json_success( [ 'html' => array_merge( $bag['common'], $bag['extra'], $bag['system'] ) ] );
	}

	public static function render_flash() : void {
		$screen = get_current_screen();
		if ( ! $screen ) { return; }
		// Match either the CPT edit screens OR our own admin page slug —
		// bulk trash / delete redirects lands on the latter.
		$sid    = (string) $screen->id;
		$is_ours = strpos( $sid, PostTypes::NEWSLETTER ) !== false
			|| strpos( $sid, self::SLUG ) !== false;
		if ( ! $is_ours ) { return; }
		$key = 'p4all_nl_flash_' . get_current_user_id();
		$msg = get_transient( $key );
		if ( ! $msg ) { return; }
		delete_transient( $key );
		echo '<div class="notice notice-success is-dismissible" style="margin-top:12px;"><p>' . esc_html( (string) $msg ) . '</p></div>';
	}

	public static function on_delete( int $post_id ) : void {
		if ( get_post_type( $post_id ) !== PostTypes::NEWSLETTER ) { return; }
		NewsletterSendLog::delete_for_campaign( $post_id );
	}

	/**
	 * Persists the signup-button customisation (label + bg + text colours)
	 * into the free plugin's `p4all_form_settings` option. The Newsletters
	 * accordion posts here ; on success we redirect back with a flash
	 * flag so the merchant sees the confirmation banner.
	 */
	public static function handle_save_signup_button() : void {
		if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied.', 'pay4all-pro' ) ); }
		check_admin_referer( 'p4all_save_signup_button' );
		if ( ! class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' ) ) {
			wp_die( esc_html__( 'Pay4All Shop is required.', 'pay4all-pro' ) );
		}

		// Per-language label map. Empty entries drop out so the resolver
		// in Signup.php falls back to the gettext default in that locale.
		$labels = [];
		if ( isset( $_POST['signup_button_label'] ) && is_array( $_POST['signup_button_label'] ) ) {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- values sanitized in the loop below.
			foreach ( wp_unslash( $_POST['signup_button_label'] ) as $lang => $raw ) {
				$lang = strtolower( sanitize_key( (string) $lang ) );
				if ( ! in_array( $lang, \Pay4All\Form\Admin\Settings\Schema::SUPPORTED_LANGUAGES, true ) ) { continue; }
				$val = trim( sanitize_text_field( (string) $raw ) );
				if ( $val === '' ) { continue; }
				$labels[ $lang ] = mb_substr( $val, 0, 60 );
			}
		}

		$bg   = isset( $_POST['signup_button_bg'] )
			? ( sanitize_hex_color( (string) wp_unslash( (string) $_POST['signup_button_bg'] ) ) ?: '' )
			: '';
		$text = isset( $_POST['signup_button_text'] )
			? ( sanitize_hex_color( (string) wp_unslash( (string) $_POST['signup_button_text'] ) ) ?: '' )
			: '';
		$qr_bg = isset( $_POST['qr_page_bg'] )
			? ( sanitize_hex_color( (string) wp_unslash( (string) $_POST['qr_page_bg'] ) ) ?: '' )
			: '';
		$qr_head = isset( $_POST['qr_heading_color'] )
			? ( sanitize_hex_color( (string) wp_unslash( (string) $_POST['qr_heading_color'] ) ) ?: '' )
			: '';

		// Checkboxes : browsers omit unchecked checkboxes from the POST
		// entirely, so absence = 0. We always write both keys so a
		// merchant un-ticking a box actually persists.
		$show_logo = ! empty( $_POST['qr_show_logo'] )          ? 1 : 0;
		$show_name = ! empty( $_POST['qr_show_merchant_name'] ) ? 1 : 0;

		$current = (array) get_option( 'p4all_form_settings', [] );
		$current['signup_button_label']   = $labels;
		$current['signup_button_bg']      = $bg;
		$current['signup_button_text']    = $text;
		$current['qr_page_bg']            = $qr_bg;
		$current['qr_heading_color']      = $qr_head;
		$current['qr_show_logo']          = $show_logo;
		$current['qr_show_merchant_name'] = $show_name;
		update_option( 'p4all_form_settings', $current, false );

		$redirect = add_query_arg( [
			'page' => self::SLUG,
			'p4all_signup_btn_saved' => '1',
		], admin_url( 'admin.php' ) );
		wp_safe_redirect( $redirect );
		exit;
	}

	// ---------------------------------------------------------------
	// Standalone signup shortcodes reference + QR download tool
	// ---------------------------------------------------------------

	/**
	 * Renders a collapsible box under the audience description :
	 *
	 *   1. Copy-to-clipboard reference table for [pay4all_signup] and
	 *      [pay4all_signup_qr] with common attribute presets so the
	 *      merchant can grab the exact shortcode for their use case.
	 *
	 *   2. Live QR builder — pick a URL + size, the api.qrserver.com
	 *      preview refreshes in place and download links are updated
	 *      to point at the current PNG / SVG so the merchant can save
	 *      a print-ready copy without leaving wp-admin.
	 */
	private static function render_shortcodes_panel() : void {
		$examples = [
			[
				'code'  => '[pay4all_signup]',
				'label' => __( 'Minimal signup form (e-mail only).', 'pay4all-pro' ),
			],
			[
				'code'  => '[pay4all_signup fields="firstname,lastname"]',
				'label' => __( 'E-mail + first name + last name.', 'pay4all-pro' ),
			],
			[
				'code'  => '[pay4all_signup fields="firstname,lastname,phone"]',
				'label' => __( 'E-mail + first name + last name + phone (all fields).', 'pay4all-pro' ),
			],
			[
				'code'  => '[pay4all_signup fields="firstname,lastname,phone" title="Rejoignez notre newsletter" button="Je m\'inscris"]',
				'label' => __( 'Full form with a custom title and button label.', 'pay4all-pro' ),
			],
		];
		?>
		<details class="p4all-nl-shortcodes" style="margin:16px 0;background:#fff;border:1px solid #dcdcde;border-radius:6px;padding:0;">
			<summary style="cursor:pointer;padding:12px 16px;font-weight:600;user-select:none;">
				<span class="dashicons dashicons-shortcode" style="vertical-align:middle;color:#2271b1;"></span>
				<?php esc_html_e( 'Signup shortcodes & QR code download', 'pay4all-pro' ); ?>
			</summary>
			<div style="padding:12px 16px 18px;border-top:1px solid #f0f0f1;">
				<p class="description" style="margin:0 0 12px;">
					<?php esc_html_e( 'Drop these shortcodes on any page or widget to let visitors sign up as customers without placing an order. The language is auto-detected from the page (WPML / Polylang / site locale). A notification e-mail is sent to the address configured in Settings › General › Notification e-mail, in the visitor\'s language.', 'pay4all-pro' ); ?>
				</p>

				<table class="widefat striped" style="margin-bottom:16px;">
					<thead>
						<tr>
							<th style="width:60%;"><?php esc_html_e( 'Shortcode', 'pay4all-pro' ); ?></th>
							<th><?php esc_html_e( 'Description', 'pay4all-pro' ); ?></th>
							<th style="width:110px;"><?php esc_html_e( 'Copy', 'pay4all-pro' ); ?></th>
						</tr>
					</thead>
					<tbody>
					<?php foreach ( $examples as $i => $ex ) :
						$code_id = 'p4all-nl-sc-' . $i;
					?>
						<tr>
							<td>
								<code id="<?php echo esc_attr( $code_id ); ?>"
								      style="display:block;padding:6px 8px;background:#f6f7f7;word-break:break-all;font-size:12px;line-height:1.4;">
									<?php echo esc_html( $ex['code'] ); ?>
								</code>
							</td>
							<td style="font-size:12px;color:#50575e;"><?php echo esc_html( $ex['label'] ); ?></td>
							<td>
								<button type="button" class="button button-small p4all-nl-copy-btn"
								        data-copy-target="<?php echo esc_attr( $code_id ); ?>">
									<span class="dashicons dashicons-clipboard" style="vertical-align:middle;font-size:14px;line-height:1.4;"></span>
									<?php esc_html_e( 'Copy', 'pay4all-pro' ); ?>
								</button>
							</td>
						</tr>
					<?php endforeach; ?>
					</tbody>
				</table>

				<?php
				// --------------------------------------------------------
				// Signup button customisation (applies to every
				// [pay4all_signup] instance + the QR endpoint).
				// --------------------------------------------------------
				$btn_settings_ok = class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' );
				if ( $btn_settings_ok ) {
					$btn_labels_raw = (array) \Pay4All\Form\Admin\Settings\Schema::get( 'signup_button_label', [] );
					$btn_bg         = (string) \Pay4All\Form\Admin\Settings\Schema::get( 'signup_button_bg', '' );
					$btn_text       = (string) \Pay4All\Form\Admin\Settings\Schema::get( 'signup_button_text', '' );
					$activated_lang = (array) \Pay4All\Form\Admin\Settings\Schema::get( 'languages', [] );
					$activated_lang = array_values( array_intersect(
						\Pay4All\Form\Admin\Settings\Schema::SUPPORTED_LANGUAGES,
						$activated_lang
					) );
					// Fallback : if the merchant hasn't activated any
					// language yet (typical on free plugin — multilingual
					// is Pro-only), offer the primary lang alone so the
					// UI always renders at least one input.
					if ( empty( $activated_lang ) ) {
						$primary = class_exists( '\\Pay4All\\Form\\Domain\\Language' )
							? \Pay4All\Form\Domain\Language::primary()
							: 'fr';
						$activated_lang = [ $primary ];
					}
				} else {
					$btn_labels_raw = []; $btn_bg = ''; $btn_text = '';
					$activated_lang = [ 'fr' ];
				}
				$btn_bg_preview   = $btn_bg   !== '' ? $btn_bg   : '#2271b1';
				$btn_text_preview = $btn_text !== '' ? $btn_text : '#ffffff';
				$qr_page_bg       = $btn_settings_ok ? (string) \Pay4All\Form\Admin\Settings\Schema::get( 'qr_page_bg', '' ) : '';
				$qr_page_bg_val   = $qr_page_bg !== '' ? $qr_page_bg : '#f5f5f5';
				// H1 (merchant name) colour on the QR page. Fallback
				// chain : dedicated setting → button text colour → #fff.
				$qr_heading       = $btn_settings_ok ? (string) \Pay4All\Form\Admin\Settings\Schema::get( 'qr_heading_color', '' ) : '';
				$qr_heading_val   = $qr_heading !== '' ? $qr_heading : $btn_text_preview;
				// Header toggles for the QR page (logo / merchant name).
				$qr_show_logo  = $btn_settings_ok ? ( (int) \Pay4All\Form\Admin\Settings\Schema::get( 'qr_show_logo', 1 ) === 1 ) : true;
				$qr_show_name  = $btn_settings_ok ? ( (int) \Pay4All\Form\Admin\Settings\Schema::get( 'qr_show_merchant_name', 1 ) === 1 ) : true;
				// Fetch the site's custom logo HTML once for the preview
				// + the checkbox grey-out. Empty = merchant has no logo
				// configured under Appearance › Customize › Site Identity.
				$logo_html_prev = get_custom_logo();
				$has_logo       = trim( (string) $logo_html_prev ) !== '';
				// Preview label : first non-empty stored label (in activated
				// order), else the gettext default in the site locale.
				$btn_label_prev = '';
				foreach ( $activated_lang as $lc ) {
					if ( ! empty( $btn_labels_raw[ $lc ] ) ) { $btn_label_prev = (string) $btn_labels_raw[ $lc ]; break; }
				}
				if ( $btn_label_prev === '' ) { $btn_label_prev = __( 'Sign up', 'pay4all-pro' ); }
				$lang_names = [ 'fr' => 'Français', 'de' => 'Deutsch', 'it' => 'Italiano', 'en' => 'English' ];
				// Success flash if we just came back from the save handler.
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only flash flag.
				$btn_saved = isset( $_GET['p4all_signup_btn_saved'] ) && $_GET['p4all_signup_btn_saved'] === '1';
				?>

				<h3 style="margin:16px 0 8px;">
					<span class="dashicons dashicons-admin-appearance" style="vertical-align:middle;color:#2271b1;"></span>
					<?php esc_html_e( 'Signup button customisation', 'pay4all-pro' ); ?>
				</h3>
				<p class="description" style="margin:0 0 10px;">
					<?php esc_html_e( 'Set the label and colours of the « Sign up » button. Applies to every [pay4all_signup] instance on your site AND to the standalone QR-code signup page.', 'pay4all-pro' ); ?>
				</p>

				<?php if ( $btn_saved ) : ?>
					<div class="notice notice-success inline" style="margin:0 0 10px;padding:8px 12px;">
						<p style="margin:0;"><?php esc_html_e( 'Signup button saved.', 'pay4all-pro' ); ?></p>
					</div>
				<?php endif; ?>

				<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
				      style="display:grid;grid-template-columns:1fr 240px;gap:20px;align-items:start;margin-bottom:20px;">
					<input type="hidden" name="action" value="p4all_save_signup_button">
					<?php wp_nonce_field( 'p4all_save_signup_button' ); ?>

					<div style="display:flex;flex-direction:column;gap:12px;">
						<div>
							<div style="font-size:12px;font-weight:600;margin-bottom:6px;"><?php esc_html_e( 'Button label', 'pay4all-pro' ); ?></div>
							<div style="display:flex;flex-direction:column;gap:6px;max-width:460px;">
								<?php
								// Compute the pay4all-form « Sign up » default
								// translated into each activated language so the
								// input is pre-filled with the value the visitor
								// would see today. Uses switch_to_locale so the
								// current admin locale isn't disturbed by the
								// gettext lookups below.
								$lang_wp_map = [ 'fr' => 'fr_FR', 'de' => 'de_DE', 'it' => 'it_IT', 'en' => 'en_US' ];
								foreach ( $activated_lang as $lc ) :
									$val    = isset( $btn_labels_raw[ $lc ] ) ? (string) $btn_labels_raw[ $lc ] : '';
									$label  = $lang_names[ $lc ] ?? strtoupper( $lc );
									$wp     = $lang_wp_map[ $lc ] ?? '';
									$switch = ( $wp !== '' && $wp !== 'en_US' && function_exists( 'switch_to_locale' ) )
										? switch_to_locale( $wp ) : false;
									// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- deliberately reading the pay4all-form textdomain since that's where the visitor-facing button label lives.
									$default_here = __( 'Sign up', 'pay4all-form' );
									if ( $switch && function_exists( 'restore_previous_locale' ) ) {
										restore_previous_locale();
									}
									// Pre-fill both value + placeholder with the
									// per-lang translated default. If the merchant
									// hasn't customised anything, the form saves
									// the translated string as-is, which is the
									// same visible outcome as leaving it empty +
									// falling back at render time.
									$value_display = $val !== '' ? $val : $default_here;
								?>
									<label style="display:flex;align-items:center;gap:8px;">
										<span style="min-width:80px;font-size:12px;color:#50575e;"><?php echo esc_html( $label ); ?></span>
										<input type="text"
										       class="p4all-signup-btn-label-input"
										       data-lang="<?php echo esc_attr( $lc ); ?>"
										       name="signup_button_label[<?php echo esc_attr( $lc ); ?>]"
										       value="<?php echo esc_attr( $value_display ); ?>"
										       placeholder="<?php echo esc_attr( $default_here ); ?>"
										       maxlength="60"
										       style="flex:1;">
									</label>
								<?php endforeach; ?>
							</div>
						</div>

						<div>
							<div style="font-size:12px;font-weight:600;margin-bottom:6px;"><?php esc_html_e( 'Button colours', 'pay4all-pro' ); ?></div>
							<label style="display:inline-block;margin-right:18px;">
								<input type="color" id="p4all-signup-btn-bg" name="signup_button_bg"
								       value="<?php echo esc_attr( $btn_bg !== '' ? $btn_bg : '#2271b1' ); ?>"
								       style="vertical-align:middle;height:34px;width:48px;padding:2px;border:1px solid #ccc;border-radius:4px;">
								<?php esc_html_e( 'Background', 'pay4all-pro' ); ?>
							</label>
							<label style="display:inline-block;">
								<input type="color" id="p4all-signup-btn-text" name="signup_button_text"
								       value="<?php echo esc_attr( $btn_text !== '' ? $btn_text : '#ffffff' ); ?>"
								       style="vertical-align:middle;height:34px;width:48px;padding:2px;border:1px solid #ccc;border-radius:4px;">
								<?php esc_html_e( 'Text', 'pay4all-pro' ); ?>
							</label>
						</div>

						<div>
							<div style="font-size:12px;font-weight:600;margin-bottom:6px;"><?php esc_html_e( 'QR-code page background', 'pay4all-pro' ); ?></div>
							<label style="display:inline-block;margin-right:18px;">
								<input type="color" id="p4all-signup-qr-bg" name="qr_page_bg"
								       value="<?php echo esc_attr( $qr_page_bg_val ); ?>"
								       style="vertical-align:middle;height:34px;width:48px;padding:2px;border:1px solid #ccc;border-radius:4px;">
								<?php esc_html_e( 'Page background', 'pay4all-pro' ); ?>
							</label>
							<label style="display:inline-block;">
								<input type="color" id="p4all-signup-qr-heading" name="qr_heading_color"
								       value="<?php echo esc_attr( $qr_heading_val ); ?>"
								       style="vertical-align:middle;height:34px;width:48px;padding:2px;border:1px solid #ccc;border-radius:4px;">
								<?php esc_html_e( 'Merchant name', 'pay4all-pro' ); ?>
							</label>
						</div>

						<div style="display:flex;flex-direction:column;gap:6px;margin-top:6px;">
							<label style="display:flex;align-items:center;gap:8px;font-size:13px;<?php echo $has_logo ? '' : 'opacity:.55;'; ?>">
								<input type="checkbox" id="p4all-signup-show-logo" name="qr_show_logo" value="1"
								       <?php checked( $qr_show_logo ); ?>
								       <?php disabled( ! $has_logo ); ?>>
								<span>
									<?php esc_html_e( 'Display the logo on the QR page', 'pay4all-pro' ); ?>
									<?php if ( ! $has_logo ) : ?>
										<em style="font-size:11px;color:#8c8f94;font-weight:400;display:block;margin-top:2px;">
											<?php esc_html_e( '(no logo configured in Appearance › Customize › Site Identity)', 'pay4all-pro' ); ?>
										</em>
									<?php endif; ?>
								</span>
							</label>
							<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
								<input type="checkbox" id="p4all-signup-show-name" name="qr_show_merchant_name" value="1"
								       <?php checked( $qr_show_name ); ?>>
								<span><?php esc_html_e( 'Display the merchant name on the QR page', 'pay4all-pro' ); ?></span>
							</label>
						</div>

						<div style="margin-top:6px;">
							<button type="submit" class="button button-primary">
								<?php esc_html_e( 'Save signup button', 'pay4all-pro' ); ?>
							</button>
						</div>
					</div>

					<div style="text-align:center;">
						<?php
						// Preview canvas mirrors the standalone QR endpoint :
						// background = qr_page_bg, email input with the
						// translated placeholder, then the CTA button. So
						// the merchant sees the exact combination they'll
						// serve to visitors as they tweak the pickers.
						$email_ph = __( 'E-Mail', 'pay4all-form' );
						?>
						<?php
						// Merchant name at the top of the preview matches
						// what the QR endpoint renders as its <h1>. Colour
						// is pulled from the signup-button text setting so
						// the merchant sees the pair « heading + button »
						// as a coherent brand block.
						$merchant_prev = class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' )
							? trim( (string) \Pay4All\Form\Admin\Settings\Schema::get( 'merchant_name', '' ) )
							: '';
						if ( $merchant_prev === '' ) { $merchant_prev = (string) get_bloginfo( 'name' ); }
						?>
						<div id="p4all-signup-preview-canvas"
						     style="background:<?php echo esc_attr( $qr_page_bg_val ); ?>;border:1px solid #dcdcde;border-radius:6px;padding:20px 14px;">
							<?php if ( $has_logo ) : ?>
								<div id="p4all-signup-logo-preview"
								     style="text-align:center;margin:0 0 10px;<?php echo $qr_show_logo ? '' : 'display:none;'; ?>">
									<?php
									// Constrain the preview logo to a reasonable
									// size regardless of what the merchant uploaded.
									echo wp_kses_post( str_replace( '<img ', '<img style="max-height:48px;max-width:180px;width:auto;height:auto;" ', (string) $logo_html_prev ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted core-generated markup, style attribute injected on our own preview surface.
									?>
								</div>
							<?php endif; ?>
							<h4 id="p4all-signup-merchant-preview"
							    style="margin:0 0 14px;text-align:center;font-size:16px;font-weight:600;line-height:1.3;color:<?php echo esc_attr( $qr_heading_val ); ?>;<?php echo $qr_show_name ? '' : 'display:none;'; ?>">
								<?php echo esc_html( $merchant_prev ); ?>
							</h4>
							<div style="display:flex;flex-direction:column;gap:10px;align-items:stretch;">
								<input type="email" id="p4all-signup-email-preview" disabled
								       value=""
								       placeholder="<?php echo esc_attr( $email_ph ); ?>"
								       style="padding:12px 14px;min-height:44px;border:1px solid rgba(0,0,0,.15);border-radius:6px;background:#f5f5f5;color:#333;font-size:16px;box-sizing:border-box;width:100%;">
								<button type="button" id="p4all-signup-btn-preview" disabled
								        style="background:<?php echo esc_attr( $btn_bg_preview ); ?>;color:<?php echo esc_attr( $btn_text_preview ); ?>;border:0;border-radius:6px;padding:12px 22px;min-height:48px;font-size:16px;font-weight:600;cursor:not-allowed;opacity:1;">
									<?php echo esc_html( $btn_label_prev ); ?>
								</button>
							</div>
						</div>
					</div>
				</form>

				<script>
				(function () {
					var prev    = document.getElementById('p4all-signup-btn-preview');
					var canvas  = document.getElementById('p4all-signup-preview-canvas');
					var merch   = document.getElementById('p4all-signup-merchant-preview');
					var bg      = document.getElementById('p4all-signup-btn-bg');
					var text    = document.getElementById('p4all-signup-btn-text');
					var qrBg    = document.getElementById('p4all-signup-qr-bg');
					var qrHead  = document.getElementById('p4all-signup-qr-heading');
					var labels  = document.querySelectorAll('.p4all-signup-btn-label-input');
					var fallback = <?php echo wp_json_encode( __( 'Sign up', 'pay4all-pro' ) ); ?>;
					if (!prev) { return; }

					function refreshLabel () {
						var picked = '';
						for (var i = 0; i < labels.length; i++) {
							var v = (labels[i].value || '').trim();
							if (v !== '') { picked = v; break; }
						}
						prev.textContent = picked || fallback;
					}
					function refreshColors () {
						if (bg)   { prev.style.background = bg.value   || '#2271b1'; }
						if (text) { prev.style.color      = text.value || '#ffffff'; }
					}
					function refreshCanvas () {
						if (canvas && qrBg) { canvas.style.background = qrBg.value || '#f5f5f5'; }
					}
					function refreshHeading () {
						// Dedicated H1 picker wins ; if empty (fresh install),
						// fall back to the button-text colour so the preview
						// mirrors the endpoint fallback chain.
						if (!merch) { return; }
						var col = qrHead ? qrHead.value : '';
						if (!col && text) { col = text.value; }
						merch.style.color = col || '#ffffff';
					}
					for (var i = 0; i < labels.length; i++) {
						labels[i].addEventListener('input', refreshLabel);
					}
					if (bg)     { bg.addEventListener('input',     refreshColors); }
					if (text)   { text.addEventListener('input',   function () { refreshColors(); refreshHeading(); }); }
					if (qrBg)   { qrBg.addEventListener('input',   refreshCanvas); }
					if (qrHead) { qrHead.addEventListener('input', refreshHeading); }

					// Header toggles : show / hide logo + merchant name
					// blocks in the preview to mirror the endpoint.
					var showLogo = document.getElementById('p4all-signup-show-logo');
					var showName = document.getElementById('p4all-signup-show-name');
					var logoPrev = document.getElementById('p4all-signup-logo-preview');
					function refreshHeader () {
						if (logoPrev && showLogo) { logoPrev.style.display = showLogo.checked ? '' : 'none'; }
						if (merch    && showName) { merch.style.display    = showName.checked ? '' : 'none'; }
					}
					if (showLogo) { showLogo.addEventListener('change', refreshHeader); }
					if (showName) { showName.addEventListener('change', refreshHeader); }

					refreshLabel();
					refreshColors();
					refreshCanvas();
					refreshHeading();
					refreshHeader();
				})();
				</script>

				<hr style="margin:18px 0;">

				<h3 style="margin:12px 0 8px;">
					<span class="dashicons dashicons-image-filter" style="vertical-align:middle;color:#2271b1;"></span>
					<?php esc_html_e( 'Download a QR code', 'pay4all-pro' ); ?>
				</h3>
				<p class="description" style="margin:0 0 10px;">
					<?php esc_html_e( 'Pick a language and a size, then click Download. The QR points to a plugin endpoint on your site that renders the full signup form in the chosen language — no need to create a WordPress page. The QR image itself is generated by api.qrserver.com (no data stored on your server). PNG for print, SVG for infinite scaling.', 'pay4all-pro' ); ?>
				</p>

				<?php
				// Language list : activated ones from Pay4All Shop settings,
				// fallback to all 4 supported languages when nothing is
				// activated (typical on the free plugin, since the multilingual
				// tab is Pro-only).
				$supported     = [ 'fr' => 'Français', 'de' => 'Deutsch', 'it' => 'Italiano', 'en' => 'English' ];
				$activated_raw = class_exists( '\\Pay4All\\Form\\Admin\\Settings\\Schema' )
					? (array) \Pay4All\Form\Admin\Settings\Schema::get( 'languages', [] )
					: [];
				$activated     = array_values( array_intersect( array_keys( $supported ), $activated_raw ) );
				if ( empty( $activated ) ) { $activated = array_keys( $supported ); }
				$endpoint_base = home_url( '/' );
				?>

				<div id="p4all-nl-qr-tool" style="display:grid;grid-template-columns:1fr 240px;gap:20px;align-items:start;">
					<div style="display:flex;flex-direction:column;gap:10px;">
						<label style="display:flex;flex-direction:column;gap:4px;max-width:260px;">
							<span style="font-size:12px;font-weight:600;"><?php esc_html_e( 'Language', 'pay4all-pro' ); ?></span>
							<select id="p4all-nl-qr-lang">
								<?php foreach ( $activated as $lang_code ) : ?>
									<option value="<?php echo esc_attr( $lang_code ); ?>">
										<?php echo esc_html( $supported[ $lang_code ] ); ?>
									</option>
								<?php endforeach; ?>
							</select>
						</label>

						<label style="display:flex;flex-direction:column;gap:4px;max-width:260px;">
							<span style="font-size:12px;font-weight:600;"><?php esc_html_e( 'Size (px)', 'pay4all-pro' ); ?></span>
							<select id="p4all-nl-qr-size">
								<option value="200">200 &times; 200 <?php esc_html_e( '(screen)', 'pay4all-pro' ); ?></option>
								<option value="400" selected>400 &times; 400 <?php esc_html_e( '(medium)', 'pay4all-pro' ); ?></option>
								<option value="600">600 &times; 600 <?php esc_html_e( '(large)', 'pay4all-pro' ); ?></option>
								<option value="1000">1000 &times; 1000 <?php esc_html_e( '(print A4)', 'pay4all-pro' ); ?></option>
							</select>
						</label>

						<p style="margin:2px 0 0;font-size:11px;color:#50575e;">
							<?php esc_html_e( 'Target URL:', 'pay4all-pro' ); ?>
							<code id="p4all-nl-qr-target" style="background:#f6f7f7;padding:2px 6px;border-radius:3px;font-size:11px;word-break:break-all;"></code>
							<a id="p4all-nl-qr-preview-link" href="#" target="_blank" rel="noopener" style="font-size:11px;margin-left:6px;">
								<?php esc_html_e( 'Open in new tab', 'pay4all-pro' ); ?>
							</a>
						</p>

						<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px;">
							<a id="p4all-nl-qr-dl-png" href="#" target="_blank" rel="noopener" class="button button-primary">
								<span class="dashicons dashicons-download" style="vertical-align:middle;"></span>
								<?php esc_html_e( 'Download PNG', 'pay4all-pro' ); ?>
							</a>
							<a id="p4all-nl-qr-dl-svg" href="#" target="_blank" rel="noopener" class="button">
								<span class="dashicons dashicons-download" style="vertical-align:middle;"></span>
								<?php esc_html_e( 'Download SVG', 'pay4all-pro' ); ?>
							</a>
						</div>
					</div>

					<div style="text-align:center;">
						<div style="background:#fff;border:1px solid #dcdcde;border-radius:6px;padding:10px;display:inline-block;">
							<img id="p4all-nl-qr-preview" src="" alt="QR" width="200" height="200"
							     style="display:block;max-width:100%;height:auto;background:#f6f7f7;">
						</div>
					</div>
				</div>
			</div>
		</details>

		<script>
		(function () {
			// Copy-to-clipboard buttons — one shared listener via delegation.
			document.querySelectorAll('.p4all-nl-copy-btn').forEach(function (btn) {
				btn.addEventListener('click', function () {
					var id = btn.getAttribute('data-copy-target');
					var el = id ? document.getElementById(id) : null;
					if (!el) { return; }
					var text = el.textContent || '';
					var done = function () {
						var original = btn.innerHTML;
						btn.innerHTML = '<span class="dashicons dashicons-yes" style="vertical-align:middle;color:#00a32a;font-size:14px;line-height:1.4;"></span> ' + <?php echo wp_json_encode( __( 'Copied!', 'pay4all-pro' ) ); ?>;
						setTimeout(function () { btn.innerHTML = original; }, 1500);
					};
					if (navigator.clipboard && navigator.clipboard.writeText) {
						navigator.clipboard.writeText(text).then(done).catch(function () {
							// Fallback below.
							selectRange();
						});
					} else {
						selectRange();
					}
					function selectRange () {
						var range = document.createRange();
						range.selectNodeContents(el);
						var sel = window.getSelection();
						sel.removeAllRanges();
						sel.addRange(range);
						try { document.execCommand('copy'); done(); } catch (e) {}
						sel.removeAllRanges();
					}
				});
			});

			// QR builder — the target URL is built from the site's home
			// URL + a query string that the pay4all-form plugin's
			// SignupEndpoint recognises (`?p4all_signup=1&lang=xx`).
			// The merchant only picks a language + a size — the URL is
			// derived, not typed.
			var langEl   = document.getElementById('p4all-nl-qr-lang');
			var sizeEl   = document.getElementById('p4all-nl-qr-size');
			var prev     = document.getElementById('p4all-nl-qr-preview');
			var dlPng    = document.getElementById('p4all-nl-qr-dl-png');
			var dlSvg    = document.getElementById('p4all-nl-qr-dl-svg');
			var targetEl = document.getElementById('p4all-nl-qr-target');
			var openLink = document.getElementById('p4all-nl-qr-preview-link');
			var qrBase   = 'https://api.qrserver.com/v1/create-qr-code/';
			var siteBase = <?php echo wp_json_encode( $endpoint_base ); ?>;

			function endpointUrl (lang) {
				// Preserve a possible trailing slash on the home URL and
				// use the correct separator (? vs &) so it works on sites
				// that host WordPress in a subfolder / with pretty perms.
				var sep = siteBase.indexOf('?') === -1 ? '?' : '&';
				return siteBase + sep + 'p4all_signup=1&lang=' + encodeURIComponent(lang);
			}
			function refresh () {
				var lang = (langEl.value || 'fr').trim();
				var size = parseInt(sizeEl.value, 10) || 400;
				var url  = endpointUrl(lang);
				if (targetEl) { targetEl.textContent = url; }
				if (openLink) { openLink.href = url; }
				// Preview stays at 200 px so the panel stays compact,
				// but the download links honour the chosen size.
				var previewSrc = qrBase + '?size=200x200&margin=8&ecc=M&data=' + encodeURIComponent(url);
				var pngHref    = qrBase + '?size=' + size + 'x' + size + '&margin=8&ecc=M&format=png&download=1&data=' + encodeURIComponent(url);
				var svgHref    = qrBase + '?size=' + size + 'x' + size + '&margin=8&ecc=M&format=svg&download=1&data=' + encodeURIComponent(url);
				prev.src   = previewSrc;
				dlPng.href = pngHref;
				dlSvg.href = svgHref;
			}
			if (langEl && sizeEl && prev) {
				langEl.addEventListener('change', refresh);
				sizeEl.addEventListener('change', refresh);
				refresh();
			}
		})();
		</script>
		<?php
	}
}
