<?php
/**
 * Storzen Module — Back in Stock Notifier
 *
 * When a product (simple or variable) is out of stock, shows an email
 * opt-in form on the product page. Subscriptions are stored in a custom
 * database table (`{prefix}storzen_bis_subscribers`). When WooCommerce
 * transitions a product's stock status from "outofstock" to "instock",
 * the module sends a batched notification email to all subscribers and
 * marks them as notified.
 *
 * Architecture decisions:
 *
 *  • CUSTOM TABLE (not post-meta / options):
 *    Subscriptions are high-write, high-read, and need indexed queries
 *    by product_id + email + status. A dedicated table is the only
 *    performant, WordPress.org-compliant approach.
 *
 *  • HPOS COMPATIBLE:
 *    No order queries; uses wc_get_product() for all stock checks.
 *
 *  • VARIABLE PRODUCTS:
 *    Subscribers can opt in to a specific variation. The JS detects the
 *    selected variation via WC's `found_variation` / `reset_data` events
 *    and updates the form's hidden `product_id` field in real time.
 *
 *  • EMAIL via wp_mail():
 *    No third-party mailer dependency. Uses WooCommerce email headers
 *    for correct "from" name/address and HTML formatting.
 *
 *  • DOUBLE OPT-IN (optional):
 *    When enabled, an unconfirmed record is created on subscribe and a
 *    confirmation email is sent. Only confirmed subscribers receive the
 *    "back in stock" notification.
 *
 *  • RATE LIMITING:
 *    One subscription per email per product_id per 24 hours, enforced
 *    server-side before any DB write.
 *
 *  • GDPR:
 *    Checkbox consent field (toggleable). Subscriber table includes a
 *    `consent_ip` column for audit. Data export/erasure hooks for
 *    WP's Personal Data tools are registered.
 *
 *  • TABLE CREATION:
 *    Created on module first-enable via dbDelta(). Safe to run multiple
 *    times (idempotent). Version tracked in option `storzen_bis_db_ver`.
 *
 * @package storzen
 * @since   1.3.0
 */

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

if ( ! class_exists( 'Storzen_Module_Back_In_Stock' ) ) {

	class Storzen_Module_Back_In_Stock {

		// ── Constants ─────────────────────────────────────────────────────────

		const DB_VERSION      = '1.0';
		const DB_OPTION       = 'storzen_bis_db_ver';
		const TABLE_SUFFIX    = 'storzen_bis_subscribers';

		const STATUS_PENDING   = 'pending';   // double-opt-in: awaiting email confirm
		const STATUS_CONFIRMED = 'confirmed'; // ready to receive notifications
		const STATUS_NOTIFIED  = 'notified';  // notification sent

		const AJAX_SUBSCRIBE   = 'storzen_bis_subscribe';
		const AJAX_CONFIRM     = 'storzen_bis_confirm';
		const AJAX_UNSUBSCRIBE = 'storzen_bis_unsubscribe';

		const NONCE_SUBSCRIBE   = 'storzen_bis_subscribe';
		const NONCE_CONFIRM     = 'storzen_bis_confirm';
		const NONCE_UNSUBSCRIBE = 'storzen_bis_unsub';

		const CONFIRM_QUERY_VAR = 'storzen_bis_confirm';
		const UNSUB_QUERY_VAR   = 'storzen_bis_unsub';

		/** @var array|null */
		private $settings = null;

		// ── Bootstrap ─────────────────────────────────────────────────────────

		public function __construct() {
			if ( ! function_exists( 'WC' ) ) {
				return;
			}

			// Ensure DB table exists (safe / idempotent).
			$this->maybe_create_table();

			// Front-end.
			add_action( 'wp_enqueue_scripts',             array( $this, 'enqueue_assets' ) );
			add_action( 'woocommerce_single_product_summary', array( $this, 'render_form' ), 31 );
			add_action( 'storzen_product_single_after_atc',   array( $this, 'render_form' ), 10, 1 );

			// Variable product: update form when variation changes (handled in JS).
			// We inject a variation-data map to the page for the JS to consume.
			add_action( 'wp_footer', array( $this, 'inject_variation_data' ) );

			// AJAX endpoints.
			add_action( 'wp_ajax_'        . self::AJAX_SUBSCRIBE,   array( $this, 'ajax_subscribe' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_SUBSCRIBE,   array( $this, 'ajax_subscribe' ) );
			add_action( 'wp_ajax_'        . self::AJAX_UNSUBSCRIBE, array( $this, 'ajax_unsubscribe' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_UNSUBSCRIBE, array( $this, 'ajax_unsubscribe' ) );

			// Email confirmation link handler (runs on init so we can redirect).
			add_action( 'init', array( $this, 'handle_confirm_link' ) );
			add_action( 'init', array( $this, 'handle_unsub_link' ) );

			// Stock-status transition watcher.
			// WooCommerce fires woocommerce_product_set_stock_status when any
			// product's stock status changes.
			add_action( 'woocommerce_product_set_stock_status', array( $this, 'on_stock_status_change' ), 10, 3 );
			// Also covers HPOS variation stock changes.
			add_action( 'woocommerce_variation_set_stock_status', array( $this, 'on_stock_status_change' ), 10, 3 );

			// Admin: subscriber list page.
			add_action( 'admin_menu',        array( $this, 'admin_menu' ) );
			add_action( 'admin_post_storzen_bis_delete_subscriber', array( $this, 'admin_delete_subscriber' ) );
			add_action( 'admin_post_storzen_bis_export_csv',        array( $this, 'admin_export_csv' ) );

			// GDPR: WP Personal Data tools.
			add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_privacy_exporter' ) );
			add_filter( 'wp_privacy_personal_data_erasers',   array( $this, 'register_privacy_eraser' ) );
		}

		// ── Settings ──────────────────────────────────────────────────────────

		/**
		 * Resolve module settings once per request with safe defaults.
		 *
		 * @return array
		 */
		private function get_settings() {
			if ( null !== $this->settings ) {
				return $this->settings;
			}

			$btn_text = (string) storzen_module_setting( 'back_in_stock', 'btn_text' );
			if ( '' === $btn_text ) {
				$btn_text = __( 'Notify Me When Available', 'storzen' );
			}

			$success_msg = (string) storzen_module_setting( 'back_in_stock', 'success_msg' );
			if ( '' === $success_msg ) {
				$success_msg = __( 'You\'re on the list! We\'ll email you when this product is back in stock.', 'storzen' );
			}

			$confirm_subject = (string) storzen_module_setting( 'back_in_stock', 'confirm_subject' );
			if ( '' === $confirm_subject ) {
				$confirm_subject = __( 'Confirm your back-in-stock notification', 'storzen' );
			}

			$notify_subject = (string) storzen_module_setting( 'back_in_stock', 'notify_subject' );
			if ( '' === $notify_subject ) {
				/* translators: %s: site name */
				$notify_subject = sprintf( __( '[%s] A product you wanted is back in stock!', 'storzen' ), get_bloginfo( 'name' ) );
			}

			$this->settings = array(
				'show_oos_only'       => (bool) storzen_module_setting( 'back_in_stock', 'show_oos_only' ),
				'show_for_variable'   => (bool) storzen_module_setting( 'back_in_stock', 'show_for_variable' ),
				'double_optin'        => (bool) storzen_module_setting( 'back_in_stock', 'double_optin' ),
				'show_consent'        => (bool) storzen_module_setting( 'back_in_stock', 'show_consent' ),
				'consent_text'        => ( '' !== (string) storzen_module_setting( 'back_in_stock', 'consent_text' ) )
					? (string) storzen_module_setting( 'back_in_stock', 'consent_text' )
					: __( 'I agree to be emailed when this product is back in stock.', 'storzen' ),
				'btn_text'            => $btn_text,
				'placeholder'         => ( '' !== (string) storzen_module_setting( 'back_in_stock', 'placeholder' ) )
					? (string) storzen_module_setting( 'back_in_stock', 'placeholder' )
					: __( 'Enter your email address', 'storzen' ),
				'success_msg'         => $success_msg,
				'emails_per_batch'    => max( 1, (int) storzen_module_setting( 'back_in_stock', 'emails_per_batch' ) ?: 50 ),
				'notify_delay_hours'  => max( 0, (int) storzen_module_setting( 'back_in_stock', 'notify_delay_hours' ) ),
				'confirm_subject'     => $confirm_subject,
				'notify_subject'      => $notify_subject,
				'bg_color'            => (string) storzen_module_setting( 'back_in_stock', 'bg_color' ) ?: '#f9fafb',
				'btn_color'           => (string) storzen_module_setting( 'back_in_stock', 'btn_color' ) ?: '#111827',
				'btn_text_color'      => (string) storzen_module_setting( 'back_in_stock', 'btn_text_color' ) ?: '#ffffff',
			);

			return $this->settings;
		}

		// ── Database ──────────────────────────────────────────────────────────

		/**
		 * Return the subscriber table name for the current blog.
		 *
		 * @return string
		 */
		public static function table() {
			global $wpdb;
			return $wpdb->prefix . self::TABLE_SUFFIX;
		}

		/**
		 * Create (or upgrade) the subscriber table using dbDelta().
		 * Safe to call on every request — dbDelta is idempotent.
		 *
		 * Columns:
		 *  id            — auto-increment primary key
		 *  product_id    — WC product ID (simple) or variation ID (variable)
		 *  email         — subscriber email address
		 *  status        — 'pending' | 'confirmed' | 'notified'
		 *  confirm_token — random hex used in the double-opt-in URL
		 *  unsub_token   — random hex used in the unsubscribe URL
		 *  consent       — 1/0 — did user tick the GDPR checkbox?
		 *  consent_ip    — IP address at time of subscription (audit trail)
		 *  created_at    — subscription timestamp
		 *  notified_at   — timestamp when notification was sent (nullable)
		 */
		public function maybe_create_table() {
			$version = get_option( self::DB_OPTION, '0' );
			if ( version_compare( $version, self::DB_VERSION, '>=' ) ) {
				return;
			}

			global $wpdb;
			$table      = self::table();
			$charset_co = $wpdb->get_charset_collate();

			$sql = "CREATE TABLE {$table} (
				id            BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
				product_id    BIGINT(20) UNSIGNED NOT NULL,
				email         VARCHAR(200) NOT NULL,
				status        VARCHAR(20)  NOT NULL DEFAULT 'confirmed',
				confirm_token VARCHAR(64)  NOT NULL DEFAULT '',
				unsub_token   VARCHAR(64)  NOT NULL DEFAULT '',
				consent       TINYINT(1)   NOT NULL DEFAULT 0,
				consent_ip    VARCHAR(100) NOT NULL DEFAULT '',
				created_at    DATETIME     NOT NULL,
				notified_at   DATETIME     DEFAULT NULL,
				PRIMARY KEY  (id),
				UNIQUE KEY   uq_product_email (product_id, email),
				KEY          idx_product_status (product_id, status),
				KEY          idx_email (email)
			) {$charset_co};";

			require_once ABSPATH . 'wp-admin/includes/upgrade.php';
			dbDelta( $sql );

			update_option( self::DB_OPTION, self::DB_VERSION, false );
		}

		// ── Front-end: Form Render ────────────────────────────────────────────

		/**
		 * Render the opt-in form on single product pages.
		 *
		 * For variable products we render a placeholder and let JS swap the
		 * product_id when a variation is selected. If no variation is selected
		 * yet, the form is hidden.
		 *
		 * @param \WC_Product|null $product_arg Optional product passed by Storzen hooks.
		 */
		public function render_form( $product_arg = null ) {
			$product = ( $product_arg instanceof WC_Product )
				? $product_arg
				: ( $GLOBALS['product'] ?? null );

			if ( ! $product instanceof WC_Product ) {
				return;
			}

			$s = $this->get_settings();

			// Simple product: show only when out of stock.
			if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) {
				if ( $s['show_oos_only'] && $product->is_in_stock() ) {
					return;
				}
				$this->output_form( (int) $product->get_id(), false );
				return;
			}

			// Variable product.
			if ( $product->is_type( 'variable' ) ) {
				if ( ! $s['show_for_variable'] ) {
					return;
				}
				// Render with variation_id=0 (hidden); JS fills it on selection.
				$this->output_form( (int) $product->get_id(), true );
			}
		}

		/**
		 * Build and echo the opt-in form HTML.
		 *
		 * @param int  $product_id  Product or parent-product ID.
		 * @param bool $is_variable True for variable products — form starts hidden.
		 */
		private function output_form( $product_id, $is_variable = false ) {
			$s       = $this->get_settings();
			$nonce   = wp_create_nonce( self::NONCE_SUBSCRIBE );
			$wrapper = $is_variable ? ' data-bis-variable="1" style="display:none;"' : '';

			$bg  = esc_attr( $s['bg_color'] );
			$bc  = esc_attr( $s['btn_color'] );
			$btc = esc_attr( $s['btn_text_color'] );

			?>
			<div class="sz-bis-wrap"
				data-product-id="<?php echo esc_attr( (string) $product_id ); ?>"
				data-nonce="<?php echo esc_attr( $nonce ); ?>"
				data-ajaxurl="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"
				style="--sz-bis-bg:<?php echo esc_attr( $bg ); ?>;--sz-bis-btn:<?php echo esc_attr( $bc ); ?>;--sz-bis-btn-text:<?php echo esc_attr( $btc ); ?>;"
				<?php echo $wrapper; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. ?>
			>
				<div class="sz-bis-form" role="form" aria-label="<?php esc_attr_e( 'Back in stock notification', 'storzen' ); ?>">

					<p class="sz-bis-intro">
						<?php echo wp_kses_post(
							apply_filters(
								'storzen_bis_intro_text',
								__( '<strong>Out of Stock</strong> — Enter your email and we\'ll notify you the moment it\'s available.', 'storzen' ),
								$product_id
							)
						); ?>
					</p>

					<div class="sz-bis-fields">
						<label for="sz-bis-email-<?php echo esc_attr( (string) $product_id ); ?>" class="screen-reader-text">
							<?php esc_html_e( 'Email address', 'storzen' ); ?>
						</label>
						<input
							type="email"
							id="sz-bis-email-<?php echo esc_attr( (string) $product_id ); ?>"
							class="sz-bis-email"
							placeholder="<?php echo esc_attr( $s['placeholder'] ); ?>"
							autocomplete="email"
							required
							aria-required="true"
						/>
						<button type="button" class="sz-bis-btn button">
							<?php echo esc_html( $s['btn_text'] ); ?>
						</button>
					</div>

					<?php if ( $s['show_consent'] ) : ?>
					<label class="sz-bis-consent">
						<input type="checkbox" class="sz-bis-consent-chk" required aria-required="true" />
						<span><?php echo esc_html( $s['consent_text'] ); ?></span>
					</label>
					<?php endif; ?>

					<div class="sz-bis-message" role="alert" aria-live="polite" hidden></div>

				</div><!-- .sz-bis-form -->
			</div><!-- .sz-bis-wrap -->
			<?php
		}

		// ── Front-end: Variable Product Support ───────────────────────────────

		/**
		 * Inject a JSON map of variation_id → is_out_of_stock for the JS to
		 * decide whether to show/hide the form on variation selection.
		 */
		public function inject_variation_data() {
			if ( ! function_exists( 'is_product' ) || ! is_product() ) {
				return;
			}

			$s = $this->get_settings();
			if ( ! $s['show_for_variable'] ) {
				return;
			}

			global $product; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WC global.
			if ( ! $product instanceof WC_Product || ! $product->is_type( 'variable' ) ) {
				return;
			}

			$map = array();
			foreach ( $product->get_children() as $child_id ) {
				$v = wc_get_product( $child_id );
				if ( ! $v instanceof WC_Product_Variation ) {
					continue;
				}
				$map[ $child_id ] = array(
					'oos' => ! $v->is_in_stock(),
				);
			}

			if ( empty( $map ) ) {
				return;
			}

			wp_localize_script(
				'storzen-back-in-stock',
				'szBisVariations',
				array(
					'map'          => $map,
					'parentId'     => $product->get_id(),
					'showOosOnly'  => $s['show_oos_only'] ? 1 : 0,
				)
			);
		}

		// ── AJAX: Subscribe ───────────────────────────────────────────────────

		/**
		 * Handle subscribe AJAX request.
		 * Validates input, rate-limits, inserts/upserts subscriber record,
		 * and sends confirmation or success email.
		 */
		public function ajax_subscribe() {
			// Nonce verification.
			if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), self::NONCE_SUBSCRIBE ) ) {
				wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh and try again.', 'storzen' ) ), 403 );
			}

			$product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
			$email      = isset( $_POST['email'] )      ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
			$consent    = isset( $_POST['consent'] )    ? (bool) $_POST['consent'] : false;

			// Validate.
			if ( ! $product_id || ! is_email( $email ) ) {
				wp_send_json_error( array( 'message' => __( 'Please enter a valid email address.', 'storzen' ) ), 400 );
			}

			$product = wc_get_product( $product_id );
			if ( ! $product ) {
				wp_send_json_error( array( 'message' => __( 'Product not found.', 'storzen' ) ), 404 );
			}

			// Consent required when enabled.
			$s = $this->get_settings();
			if ( $s['show_consent'] && ! $consent ) {
				wp_send_json_error( array( 'message' => __( 'Please tick the consent checkbox.', 'storzen' ) ), 400 );
			}

			// Rate limit: no duplicate within 24 hours.
			if ( $this->already_subscribed( $product_id, $email ) ) {
				wp_send_json_success( array(
					'message' => __( 'You\'re already subscribed for this product.', 'storzen' ),
				) );
			}

			// Insert subscriber.
			$result = $this->insert_subscriber( $product_id, $email, $consent, $s['double_optin'] );

			if ( is_wp_error( $result ) ) {
				wp_send_json_error( array( 'message' => $result->get_error_message() ), 500 );
			}

			// Send email.
			if ( $s['double_optin'] ) {
				$this->send_confirmation_email( $email, $product, $result['confirm_token'] );
				wp_send_json_success( array(
					'message' => __( 'Almost done! Please check your inbox and confirm your subscription.', 'storzen' ),
				) );
			} else {
				wp_send_json_success( array(
					'message' => $s['success_msg'],
				) );
			}
		}

		/**
		 * Check if an email is already subscribed (and not notified) for a product.
		 *
		 * @param int    $product_id
		 * @param string $email
		 * @return bool
		 */
		private function already_subscribed( $product_id, $email ) {
			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT id FROM %i WHERE product_id = %d AND email = %s AND status IN (%s, %s)',
					self::table(),
					$product_id,
					$email,
					self::STATUS_PENDING,
					self::STATUS_CONFIRMED
				)
			);
			return ! empty( $row );
		}

		/**
		 * Insert a new subscriber record.
		 *
		 * @param int    $product_id
		 * @param string $email
		 * @param bool   $consent
		 * @param bool   $double_optin
		 * @return array|WP_Error  Array with 'id' and 'confirm_token' on success.
		 */
		private function insert_subscriber( $product_id, $email, $consent, $double_optin ) {
			global $wpdb;

			$status        = $double_optin ? self::STATUS_PENDING : self::STATUS_CONFIRMED;
			$confirm_token = $double_optin ? bin2hex( random_bytes( 24 ) ) : '';
			$unsub_token   = bin2hex( random_bytes( 24 ) );
			$consent_ip    = $this->get_client_ip();

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$result = $wpdb->insert(
				self::table(),
				array(
					'product_id'    => $product_id,
					'email'         => $email,
					'status'        => $status,
					'confirm_token' => $confirm_token,
					'unsub_token'   => $unsub_token,
					'consent'       => $consent ? 1 : 0,
					'consent_ip'    => $consent_ip,
					'created_at'    => current_time( 'mysql', true ),
				),
				array( '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s' )
			);

			if ( false === $result ) {
				return new WP_Error( 'db_error', __( 'Could not save your subscription. Please try again.', 'storzen' ) );
			}

			return array(
				'id'            => (int) $wpdb->insert_id,
				'confirm_token' => $confirm_token,
				'unsub_token'   => $unsub_token,
			);
		}

		/**
		 * Get the client IP address (sanitised).
		 *
		 * @return string
		 */
		private function get_client_ip() {
			$keys = array( 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' );
			foreach ( $keys as $key ) {
				if ( ! empty( $_SERVER[ $key ] ) ) {
					$ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
					// X-Forwarded-For may contain a comma-separated list.
					$ip = trim( explode( ',', $ip )[0] );
					if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
						return $ip;
					}
				}
			}
			return '';
		}

		// ── AJAX: Unsubscribe ─────────────────────────────────────────────────

		/**
		 * Handle inline unsubscribe AJAX (from the "Unsubscribe" link in email).
		 */
		public function ajax_unsubscribe() {
			if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), self::NONCE_UNSUBSCRIBE ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid request.', 'storzen' ) ), 403 );
			}

			$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
			if ( '' === $token ) {
				wp_send_json_error( array( 'message' => __( 'Invalid token.', 'storzen' ) ), 400 );
			}

			$this->delete_by_token( 'unsub_token', $token );
			wp_send_json_success( array( 'message' => __( 'You have been unsubscribed.', 'storzen' ) ) );
		}

		// ── Confirm / Unsubscribe link handlers ───────────────────────────────

		/**
		 * Handle the double-opt-in confirmation link.
		 * URL: /?storzen_bis_confirm=TOKEN
		 */
		public function handle_confirm_link() {
			$token = isset( $_GET[ self::CONFIRM_QUERY_VAR ] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				? sanitize_text_field( wp_unslash( $_GET[ self::CONFIRM_QUERY_VAR ] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				: '';

			if ( '' === $token ) {
				return;
			}

			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Token lookup is unique per request; result is immediately invalidated by the UPDATE below, so caching would serve stale data.
			$row = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT id, status FROM %i WHERE confirm_token = %s LIMIT 1',
					self::table(),
					$token
				)
			);

			if ( $row && self::STATUS_PENDING === $row->status ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Immediate write after the read above; no cache to invalidate.
				$wpdb->update(
					self::table(),
					array( 'status' => self::STATUS_CONFIRMED, 'confirm_token' => '' ),
					array( 'id'     => (int) $row->id ),
					array( '%s', '%s' ),
					array( '%d' )
				);
			}

			// Redirect to a confirmation message page or the homepage.
			$redirect_url = apply_filters(
				'storzen_bis_confirm_redirect',
				add_query_arg( 'storzen_bis_confirmed', '1', home_url( '/' ) )
			);
			wp_safe_redirect( esc_url_raw( $redirect_url ) );
			exit;
		}

		/**
		 * Handle unsubscribe link click.
		 * URL: /?storzen_bis_unsub=TOKEN
		 */
		public function handle_unsub_link() {
			$token = isset( $_GET[ self::UNSUB_QUERY_VAR ] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				? sanitize_text_field( wp_unslash( $_GET[ self::UNSUB_QUERY_VAR ] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				: '';

			if ( '' === $token ) {
				return;
			}

			$this->delete_by_token( 'unsub_token', $token );

			$redirect_url = apply_filters(
				'storzen_bis_unsub_redirect',
				add_query_arg( 'storzen_bis_unsubscribed', '1', home_url( '/' ) )
			);
			wp_safe_redirect( esc_url_raw( $redirect_url ) );
			exit;
		}

		/**
		 * Delete a subscriber row by a token column value.
		 *
		 * @param string $column  'confirm_token' or 'unsub_token'
		 * @param string $token   The token value.
		 */
		private function delete_by_token( $column, $token ) {
			global $wpdb;
			$allowed = array( 'confirm_token', 'unsub_token' );
			if ( ! in_array( $column, $allowed, true ) ) {
				return;
			}
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Write (DELETE) operation; caching does not apply to destructive queries.
			$wpdb->delete(
				self::table(),
				array( $column => $token ),
				array( '%s' )
			);
		}

		// ── Stock Status Watcher ──────────────────────────────────────────────

		/**
		 * Fired by WooCommerce whenever a product's or variation's stock status
		 * transitions. We schedule the notification send via WP Cron so the
		 * response to the stock-update request isn't slowed down.
		 *
		 * @param int        $product_id  Product or variation ID.
		 * @param string     $status      New status: 'instock' | 'outofstock' | 'onbackorder'.
		 * @param WC_Product $product     Product object.
		 */
		public function on_stock_status_change( $product_id, $status, $product ) {
			if ( 'instock' !== $status ) {
				return;
			}

			// Check there are subscribers to notify.
			if ( ! $this->has_confirmed_subscribers( $product_id ) ) {
				return;
			}

			$s     = $this->get_settings();
			$delay = max( 0, (int) $s['notify_delay_hours'] ) * HOUR_IN_SECONDS;

			if ( ! wp_next_scheduled( 'storzen_bis_send_notifications', array( $product_id ) ) ) {
				wp_schedule_single_event(
					time() + $delay,
					'storzen_bis_send_notifications',
					array( $product_id )
				);
			}
		}

		/**
		 * Check if there are any confirmed subscribers for a product.
		 *
		 * @param int $product_id
		 * @return bool
		 */
		private function has_confirmed_subscribers( $product_id ) {
			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$count = (int) $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COUNT(*) FROM %i WHERE product_id = %d AND status = %s',
					self::table(),
					$product_id,
					self::STATUS_CONFIRMED
				)
			);
			return $count > 0;
		}

		// ── Notification Sender ───────────────────────────────────────────────

		/**
		 * Send back-in-stock notification emails.
		 * Hooked to the WP Cron event `storzen_bis_send_notifications`.
		 * Process in batches to avoid PHP timeout on large subscriber lists.
		 *
		 * @param int $product_id
		 */
		public function send_notifications( $product_id ) {
			$product = wc_get_product( $product_id );
			if ( ! $product ) {
				return;
			}

			$s     = $this->get_settings();
			$limit = (int) $s['emails_per_batch'];

			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$subscribers = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT id, email, unsub_token FROM %i WHERE product_id = %d AND status = %s LIMIT %d',
					self::table(),
					$product_id,
					self::STATUS_CONFIRMED,
					$limit
				),
				ARRAY_A
			);

			if ( empty( $subscribers ) ) {
				return;
			}

			$sent_ids = array();

			foreach ( $subscribers as $sub ) {
				if ( $this->send_notification_email( $sub['email'], $product, $sub['unsub_token'] ) ) {
					$sent_ids[] = (int) $sub['id'];
				}
			}

			if ( ! empty( $sent_ids ) ) {
				// absint() every ID — they come from our own SELECT, never user input.
				// wpdb->prepare() has no array placeholder, so we build the IN() list
				// from sanitised integers and suppress the interpolation sniff.
				$clean_ids       = implode( ', ', array_map( 'absint', $sent_ids ) );
				$notified_status = self::STATUS_NOTIFIED;
				$notified_at     = current_time( 'mysql', true );
				$table_name      = esc_sql( self::table() );

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$wpdb->query(
					$wpdb->prepare(
						"UPDATE `{$table_name}` SET status = %s, notified_at = %s WHERE id IN ({$clean_ids})", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
						$notified_status,
						$notified_at
					)
				);
			}

			// If there are more subscribers than the batch limit, re-schedule.
			if ( count( $sent_ids ) >= $limit ) {
				wp_schedule_single_event(
					time() + 60,
					'storzen_bis_send_notifications',
					array( $product_id )
				);
			}
		}

		// ── Email Sending ─────────────────────────────────────────────────────

		/**
		 * Send the back-in-stock notification email to one subscriber.
		 *
		 * @param string     $email       Subscriber email.
		 * @param WC_Product $product     The product that is back in stock.
		 * @param string     $unsub_token Unsubscribe token.
		 * @return bool
		 */
		private function send_notification_email( $email, $product, $unsub_token ) {
			$s       = $this->get_settings();
			$subject = wp_strip_all_tags( $s['notify_subject'] );

			$product_url  = get_permalink( $product->get_id() );
			$product_name = $product->get_name();
			$unsub_url    = add_query_arg( self::UNSUB_QUERY_VAR, rawurlencode( $unsub_token ), home_url( '/' ) );
			$site_name    = get_bloginfo( 'name' );
			$product_img  = '';
			if ( has_post_thumbnail( $product->get_id() ) ) {
				$img_url     = get_the_post_thumbnail_url( $product->get_id(), 'woocommerce_thumbnail' );
				$product_img = sprintf(
					'<p><a href="%s"><img src="%s" alt="%s" style="max-width:200px;border-radius:4px;" /></a></p>',
					esc_url( $product_url ),
					esc_url( $img_url ),
					esc_attr( $product_name )
				);
			}

			ob_start();
			?>
<!DOCTYPE html>
<html lang="<?php echo esc_attr( get_locale() ); ?>">
<head><meta charset="UTF-8"><title><?php echo esc_html( $subject ); ?></title></head>
<body style="font-family:sans-serif;background:#f3f4f6;margin:0;padding:24px;">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width:580px;margin:0 auto;">
<tr><td style="background:#ffffff;border-radius:8px;padding:32px;">
	<?php echo $product_img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. ?>
	<h2 style="margin:0 0 16px;font-size:20px;color:#111827;">
		<?php
		echo esc_html(
			sprintf(
				/* translators: %s: product name */
				__( '%s is back in stock!', 'storzen' ),
				$product_name
			)
		);
		?>
	</h2>
	<p style="color:#4b5563;margin:0 0 24px;">
		<?php
		echo wp_kses(
			sprintf(
				/* translators: %s: product name */
				__( 'Great news! %s is now available. Don\'t miss out — stock may be limited.', 'storzen' ),
				'<strong>' . esc_html( $product_name ) . '</strong>'
			),
			array( 'strong' => array() )
		);
		?>
	</p>
	<p>
		<a href="<?php echo esc_url( $product_url ); ?>"
		   style="display:inline-block;padding:12px 28px;background:#111827;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;">
			<?php esc_html_e( 'Shop Now', 'storzen' ); ?>
		</a>
	</p>
	<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;" />
	<p style="font-size:12px;color:#9ca3af;margin:0;">
		<?php
		echo esc_html(
			sprintf(
				/* translators: %s: site name */
				__( 'You\'re receiving this email because you subscribed to stock notifications on %s.', 'storzen' ),
				$site_name
			)
		);
		?>
		<br>
		<a href="<?php echo esc_url( $unsub_url ); ?>" style="color:#6b7280;"><?php esc_html_e( 'Unsubscribe', 'storzen' ); ?></a>
	</p>
</td></tr>
</table>
</body>
</html>
			<?php
			$body = ob_get_clean();

			$headers = array(
				'Content-Type: text/html; charset=UTF-8',
				'From: ' . wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) . ' <' . get_option( 'admin_email' ) . '>',
			);

			return wp_mail(
				sanitize_email( $email ),
				wp_specialchars_decode( $subject, ENT_QUOTES ),
				$body,
				$headers
			);
		}

		/**
		 * Send double-opt-in confirmation email.
		 *
		 * @param string     $email         Subscriber email.
		 * @param WC_Product $product       Product subscribed to.
		 * @param string     $confirm_token Confirmation token.
		 * @return bool
		 */
		private function send_confirmation_email( $email, $product, $confirm_token ) {
			$s            = $this->get_settings();
			$subject      = wp_strip_all_tags( $s['confirm_subject'] );
			$confirm_url  = add_query_arg( self::CONFIRM_QUERY_VAR, rawurlencode( $confirm_token ), home_url( '/' ) );
			$product_name = $product->get_name();
			$site_name    = get_bloginfo( 'name' );

			ob_start();
			?>
<!DOCTYPE html>
<html lang="<?php echo esc_attr( get_locale() ); ?>">
<head><meta charset="UTF-8"><title><?php echo esc_html( $subject ); ?></title></head>
<body style="font-family:sans-serif;background:#f3f4f6;margin:0;padding:24px;">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width:580px;margin:0 auto;">
<tr><td style="background:#ffffff;border-radius:8px;padding:32px;">
	<h2 style="margin:0 0 16px;font-size:20px;color:#111827;"><?php esc_html_e( 'Confirm your notification', 'storzen' ); ?></h2>
	<p style="color:#4b5563;margin:0 0 24px;">
		<?php
		echo wp_kses(
			sprintf(
				/* translators: %s: product name */
				__( 'Please confirm that you\'d like to be notified when %s is back in stock.', 'storzen' ),
				'<strong>' . esc_html( $product_name ) . '</strong>'
			),
			array( 'strong' => array() )
		);
		?>
	</p>
	<p>
		<a href="<?php echo esc_url( $confirm_url ); ?>"
		   style="display:inline-block;padding:12px 28px;background:#111827;color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;">
			<?php esc_html_e( 'Confirm Subscription', 'storzen' ); ?>
		</a>
	</p>
	<p style="font-size:12px;color:#9ca3af;margin:16px 0 0;">
		<?php
		echo esc_html(
			sprintf(
				/* translators: %s: site name */
				__( 'Sent from %s. If you did not request this, you can safely ignore this email.', 'storzen' ),
				$site_name
			)
		);
		?>
	</p>
</td></tr>
</table>
</body>
</html>
			<?php
			$body = ob_get_clean();

			$headers = array(
				'Content-Type: text/html; charset=UTF-8',
				'From: ' . wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) . ' <' . get_option( 'admin_email' ) . '>',
			);

			return wp_mail(
				sanitize_email( $email ),
				wp_specialchars_decode( $subject, ENT_QUOTES ),
				$body,
				$headers
			);
		}

		// ── Admin: Subscriber List ────────────────────────────────────────────

		/**
		 * Register the admin sub-menu under WooCommerce.
		 */
		public function admin_menu() {
			add_submenu_page(
				'woocommerce',
				__( 'Back in Stock Subscribers', 'storzen' ),
				__( 'Back in Stock', 'storzen' ),
				'manage_woocommerce',
				'storzen-bis-subscribers',
				array( $this, 'admin_page_subscribers' )
			);
		}

		/**
		 * Render the admin subscriber list page.
		 */
		public function admin_page_subscribers() {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				wp_die( esc_html__( 'You do not have permission to view this page.', 'storzen' ) );
			}

			global $wpdb;

			$per_page   = 50;
			$paged      = max( 1, isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$offset     = ( $paged - 1 ) * $per_page;
			$filter_pid = isset( $_GET['product_id'] ) ? absint( $_GET['product_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

			if ( $filter_pid ) {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin-only paginated read on a custom plugin table; no WP cache layer exists for this data.
				$rows = $wpdb->get_results(
					$wpdb->prepare(
						'SELECT * FROM %i WHERE product_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d',
						self::table(), $filter_pid, $per_page, $offset
					),
					ARRAY_A
				);
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Same justification as get_results above; COUNT query on custom table.
				$total = (int) $wpdb->get_var(
					$wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE product_id = %d', self::table(), $filter_pid )
				);
			} else {
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Admin-only paginated read on a custom plugin table; no WP cache layer exists for this data.
				$rows = $wpdb->get_results(
					$wpdb->prepare(
						'SELECT * FROM %i ORDER BY created_at DESC LIMIT %d OFFSET %d',
						self::table(), $per_page, $offset
					),
					ARRAY_A
				);
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Same justification as get_results above; COUNT query on custom table.
				$total = (int) $wpdb->get_var(
					$wpdb->prepare( 'SELECT COUNT(*) FROM %i', self::table() )
				);
			}

			$export_url = wp_nonce_url(
				admin_url( 'admin-post.php?action=storzen_bis_export_csv' ),
				'storzen_bis_export_csv'
			);
			?>
			<div class="wrap">
				<h1 class="wp-heading-inline"><?php esc_html_e( 'Back in Stock Subscribers', 'storzen' ); ?></h1>
				<a href="<?php echo esc_url( $export_url ); ?>" class="page-title-action"><?php esc_html_e( 'Export CSV', 'storzen' ); ?></a>
				<hr class="wp-header-end">

				<p>
					<?php
					echo esc_html(
						sprintf(
							/* translators: %d: total subscriber count */
							__( 'Total: %d subscribers', 'storzen' ),
							(int) $total
						)
					);
					?>
				</p>

				<table class="wp-list-table widefat fixed striped">
					<thead>
						<tr>
							<th><?php esc_html_e( 'Product', 'storzen' ); ?></th>
							<th><?php esc_html_e( 'Email', 'storzen' ); ?></th>
							<th><?php esc_html_e( 'Status', 'storzen' ); ?></th>
							<th><?php esc_html_e( 'Subscribed', 'storzen' ); ?></th>
							<th><?php esc_html_e( 'Notified', 'storzen' ); ?></th>
							<th><?php esc_html_e( 'Actions', 'storzen' ); ?></th>
						</tr>
					</thead>
					<tbody>
					<?php if ( empty( $rows ) ) : ?>
						<tr><td colspan="6"><?php esc_html_e( 'No subscribers found.', 'storzen' ); ?></td></tr>
					<?php else : ?>
						<?php foreach ( $rows as $row ) :
							$product     = wc_get_product( $row['product_id'] );
							$product_name = $product ? $product->get_name() : '(#' . $row['product_id'] . ')';
							$delete_url  = wp_nonce_url(
								admin_url( 'admin-post.php?action=storzen_bis_delete_subscriber&id=' . $row['id'] ),
								'storzen_bis_delete_' . $row['id']
							);
						?>
						<tr>
							<td><?php echo esc_html( $product_name ); ?></td>
							<td><?php echo esc_html( $row['email'] ); ?></td>
							<td><span class="sz-bis-status sz-bis-status--<?php echo esc_attr( $row['status'] ); ?>"><?php echo esc_html( ucfirst( $row['status'] ) ); ?></span></td>
							<td><?php echo esc_html( $row['created_at'] ); ?></td>
							<td><?php echo $row['notified_at'] ? esc_html( $row['notified_at'] ) : '—'; ?></td>
							<td>
								<a href="<?php echo esc_url( $delete_url ); ?>"
								   onclick="return confirm('<?php esc_attr_e( 'Delete this subscriber?', 'storzen' ); ?>');"
								   style="color:#dc2626;">
									<?php esc_html_e( 'Delete', 'storzen' ); ?>
								</a>
							</td>
						</tr>
						<?php endforeach; ?>
					<?php endif; ?>
					</tbody>
				</table>
			</div>
			<?php
		}

		/**
		 * Handle single subscriber deletion from admin.
		 */
		public function admin_delete_subscriber() {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				wp_die( esc_html__( 'Insufficient permissions.', 'storzen' ) );
			}

			$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
			if ( ! $id || ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'storzen_bis_delete_' . $id ) ) {
				wp_die( esc_html__( 'Invalid request.', 'storzen' ) );
			}

			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->delete( self::table(), array( 'id' => $id ), array( '%d' ) );

			wp_safe_redirect( admin_url( 'admin.php?page=storzen-bis-subscribers&deleted=1' ) );
			exit;
		}

		/**
		 * Export all subscribers as a CSV download.
		 */
		public function admin_export_csv() {
			if ( ! current_user_can( 'manage_woocommerce' ) ) {
				wp_die( esc_html__( 'Insufficient permissions.', 'storzen' ) );
			}
			check_admin_referer( 'storzen_bis_export_csv' );

			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results(
				$wpdb->prepare( 'SELECT product_id, email, status, created_at, notified_at FROM %i ORDER BY created_at DESC', self::table() ),
				ARRAY_A
			);

			header( 'Content-Type: text/csv; charset=UTF-8' );
			header( 'Content-Disposition: attachment; filename="storzen-bis-' . gmdate( 'Y-m-d' ) . '.csv"' );
			header( 'Pragma: no-cache' );

			$out = fopen( 'php://output', 'w' );
			fputcsv( $out, array( 'Product ID', 'Email', 'Status', 'Subscribed', 'Notified' ) );
			foreach ( $rows as $row ) {
				fputcsv( $out, array(
					$row['product_id'],
					$row['email'],
					$row['status'],
					$row['created_at'],
					$row['notified_at'] ?? '',
				) );
			}
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output is not a real file; WP_Filesystem does not apply.
			fclose( $out );
			exit;
		}

		// ── GDPR: Personal Data Tools ─────────────────────────────────────────

		/**
		 * Register with WP's Privacy Data Exporter.
		 *
		 * @param array $exporters
		 * @return array
		 */
		public function register_privacy_exporter( $exporters ) {
			$exporters['storzen-bis'] = array(
				'exporter_friendly_name' => __( 'Storzen Back in Stock Subscriptions', 'storzen' ),
				'callback'               => array( $this, 'privacy_export' ),
			);
			return $exporters;
		}

		/**
		 * Export subscriber data for a given email (WP privacy exporter callback).
		 *
		 * @param string $email_address
		 * @param int    $page          Not used — we return all records at once.
		 * @return array
		 */
		public function privacy_export( $email_address, $page = 1 ) {
			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT product_id, status, created_at FROM %i WHERE email = %s',
					self::table(),
					sanitize_email( $email_address )
				),
				ARRAY_A
			);

			$export_items = array();
			foreach ( $rows as $row ) {
				$export_items[] = array(
					'group_id'    => 'storzen-bis',
					'group_label' => __( 'Back in Stock Subscriptions', 'storzen' ),
					'item_id'     => 'bis-' . $row['product_id'],
					'data'        => array(
						array( 'name' => __( 'Product ID', 'storzen' ),       'value' => $row['product_id'] ),
						array( 'name' => __( 'Subscription Status', 'storzen' ), 'value' => $row['status'] ),
						array( 'name' => __( 'Subscribed At', 'storzen' ),    'value' => $row['created_at'] ),
					),
				);
			}

			return array( 'data' => $export_items, 'done' => true );
		}

		/**
		 * Register with WP's Privacy Data Eraser.
		 *
		 * @param array $erasers
		 * @return array
		 */
		public function register_privacy_eraser( $erasers ) {
			$erasers['storzen-bis'] = array(
				'eraser_friendly_name' => __( 'Storzen Back in Stock Subscriptions', 'storzen' ),
				'callback'             => array( $this, 'privacy_erase' ),
			);
			return $erasers;
		}

		/**
		 * Erase subscriber data for a given email (WP privacy eraser callback).
		 *
		 * @param string $email_address
		 * @param int    $page
		 * @return array
		 */
		public function privacy_erase( $email_address, $page = 1 ) {
			global $wpdb;
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$deleted = $wpdb->delete(
				self::table(),
				array( 'email' => sanitize_email( $email_address ) ),
				array( '%s' )
			);

			return array(
				'items_removed'  => (int) $deleted > 0,
				'items_retained' => false,
				'messages'       => array(),
				'done'           => true,
			);
		}

		// ── Assets ────────────────────────────────────────────────────────────

		/**
		 * Enqueue front-end CSS and JS on single product pages only.
		 */
		public function enqueue_assets() {
			if ( is_admin() ) {
				return;
			}

			$on_product   = function_exists( 'is_product' ) && is_product();
			$on_elementor = class_exists( '\Elementor\Plugin' );

			if ( ! $on_product && ! $on_elementor ) {
				return;
			}

			wp_enqueue_style(
				'storzen-back-in-stock',
				STORZEN_ROOT_URL . 'assets/css/modules/back-in-stock.css',
				array(),
				STORZEN_VERSION
			);

			wp_enqueue_script(
				'storzen-back-in-stock',
				STORZEN_ROOT_URL . 'assets/js/modules/back-in-stock.js',
				array(),
				STORZEN_VERSION,
				true
			);

			$s = $this->get_settings();

			wp_localize_script(
				'storzen-back-in-stock',
				'szBisConfig',
				array(
					'ajaxUrl'     => esc_url( admin_url( 'admin-ajax.php' ) ),
					'action'      => self::AJAX_SUBSCRIBE,
					'showConsent' => $s['show_consent'] ? 1 : 0,
					'showOosOnly' => $s['show_oos_only'] ? 1 : 0,
					'i18n'        => array(
						'submitting' => __( 'Submitting…', 'storzen' ),
						'error'      => __( 'Something went wrong. Please try again.', 'storzen' ),
					),
				)
			);
		}

	} // end class

	// Register the cron callback so it works even if module is reinstantiated.
	add_action( 'storzen_bis_send_notifications', array( new Storzen_Module_Back_In_Stock(), 'send_notifications' ), 10, 1 );

} // end class_exists
