<?php
/**
 * Conversion Busters Admin
 *
 * Admin settings and AJAX endpoints for the Conversion Busters feature.
 *
 * @package Niwa_AI
 * @since   0.2.0
 */

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

class Niwa_AI_Conversion_Busters {

	const OPTION_KEY     = Niwa_AI_Constants::OPT_CONVERSION_BUSTERS;
	const ANALYTICS_TABLE = 'niwa_ai_conversion_analytics';
	const THROTTLE_PREFIX = 'niwa_ai_cb_throttle_';
	const CLEANUP_HOOK    = 'niwa_ai_cb_cleanup_expired';
	const REMINDER_HOOK   = 'niwa_ai_cb_coupon_reminder';
	const PRUNE_KEY_SEPARATOR = '::';

	/**
	 * Register hooks.
	 */
	public function register_hooks() {
		add_action( 'admin_menu', array( $this, 'add_submenu' ) );
		add_action( 'admin_init', array( $this, 'register_settings' ) );

		// AJAX endpoints.
		add_action( 'wp_ajax_niwa_ai_cb_check_cart', array( $this, 'ajax_check_cart' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_cb_check_cart', array( $this, 'ajax_check_cart' ) );

		add_action( 'wp_ajax_niwa_ai_cb_create_coupon', array( $this, 'ajax_create_coupon' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_cb_create_coupon', array( $this, 'ajax_create_coupon' ) );

		add_action( 'wp_ajax_niwa_ai_cb_detect_response', array( $this, 'ajax_detect_response' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_cb_detect_response', array( $this, 'ajax_detect_response' ) );

		add_action( 'wp_ajax_niwa_ai_cb_append_history', array( $this, 'ajax_append_history' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_cb_append_history', array( $this, 'ajax_append_history' ) );

		add_action( 'wp_ajax_niwa_ai_cb_prune_history', array( $this, 'ajax_prune_history' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_cb_prune_history', array( $this, 'ajax_prune_history' ) );

		add_action( 'wp_ajax_niwa_ai_cb_get_initial_message', array( $this, 'ajax_get_initial_message' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_cb_get_initial_message', array( $this, 'ajax_get_initial_message' ) );

		add_action( 'woocommerce_order_status_processing', array( $this, 'track_coupon_usage' ), 10, 1 );
		add_action( 'woocommerce_order_status_completed', array( $this, 'track_coupon_usage' ), 10, 1 );
		add_action( 'woocommerce_order_status_on-hold', array( $this, 'track_coupon_usage' ), 10, 1 );
		add_action( 'woocommerce_new_order', array( $this, 'track_coupon_usage' ), 10, 1 );
		add_action( self::CLEANUP_HOOK, array( $this, 'cleanup_expired_coupons' ) );
		add_action( self::REMINDER_HOOK, array( $this, 'send_coupon_reminder' ), 10, 6 );

		if ( ! wp_next_scheduled( self::CLEANUP_HOOK ) ) {
			wp_schedule_event( time(), 'hourly', self::CLEANUP_HOOK );
		}
	}

	/**
	 * Add submenu page.
	 */
	public function add_submenu() {
		add_submenu_page(
			'niwa-ai',
			__( 'Conversion Busters', 'niwa-ai' ),
			__( 'Conversion Busters', 'niwa-ai' ),
			'manage_options',
			'niwa-ai-conversion-busters',
			array( $this, 'render_page' )
		);
	}

	/**
	 * Register settings.
	 */
	public function register_settings() {
		register_setting( 'niwa_ai_cb_group', self::OPTION_KEY, array( $this, 'sanitize' ) );
	}

	/**
	 * Sanitize settings.
	 *
	 * @param array $input Raw input.
	 * @return array Sanitized.
	 */
	public function sanitize( $input ) {
		$sanitized = array();

		$sanitized['enabled']          = ! empty( $input['enabled'] ) ? 1 : 0;
		$sanitized['cart_threshold']   = isset( $input['cart_threshold'] ) ? max( 1, absint( $input['cart_threshold'] ) ) : 2;
		$sanitized['discount_type']    = isset( $input['discount_type'] ) ? sanitize_text_field( $input['discount_type'] ) : 'percent';
		$sanitized['discount_amount']  = isset( $input['discount_amount'] ) ? floatval( $input['discount_amount'] ) : 10;
		$sanitized['coupon_duration']  = isset( $input['coupon_duration'] ) ? max( 1, min( 1440, absint( $input['coupon_duration'] ) ) ) : 10;
		$sanitized['coupon_prefix']    = isset( $input['coupon_prefix'] ) ? sanitize_text_field( $input['coupon_prefix'] ) : 'NIWA';
		$sanitized['max_uses']         = isset( $input['max_uses'] ) ? max( 1, absint( $input['max_uses'] ) ) : 1;
		$sanitized['minimum_amount']   = isset( $input['minimum_amount'] ) ? floatval( $input['minimum_amount'] ) : 0;
		$sanitized['session_cooldown'] = isset( $input['session_cooldown'] ) ? max( 1, absint( $input['session_cooldown'] ) ) : 30;

		$sanitized['initial_message']      = isset( $input['initial_message'] ) ? sanitize_textarea_field( $input['initial_message'] ) : '';
		$sanitized['confirmation_message'] = isset( $input['confirmation_message'] ) ? sanitize_textarea_field( $input['confirmation_message'] ) : '';

		$sanitized['clarify_message'] = isset( $input['clarify_message'] ) ? sanitize_textarea_field( $input['clarify_message'] ) : '';

		return $sanitized;
	}

	/**
	 * Get saved options with defaults.
	 *
	 * @return array
	 */
	public function get_opts() {
		$defaults = array(
			'enabled'          => 0,
			'cart_threshold'   => 2,
			'discount_type'    => 'percent',
			'discount_amount'  => 10,
			'coupon_duration'  => 10,
			'coupon_prefix'    => 'NIWA',
			'max_uses'         => 1,
			'minimum_amount'   => 0,
			'session_cooldown' => 30,
			'initial_message'      => "I see you've been browsing! Would you like a special discount?",
			'confirmation_message' => $this->get_default_confirmation_message(),
			'clarify_message'  => '',
		);

		$stored = get_option( self::OPTION_KEY, array() );
		if ( ! is_array( $stored ) ) {
			$stored = array();
		}

		$opts = wp_parse_args( $stored, $defaults );

		// One-time migration: if unified fields are empty, pull from legacy
		// language-specific keys, persist the result, and clean up.
		$migrated = false;
		if ( ! array_key_exists( 'initial_message', $stored ) ) {
			$legacy_val = $this->find_legacy_language_value( $stored, 'initial_message' );
			if ( '' !== $legacy_val ) {
				$opts['initial_message'] = $legacy_val;
				$migrated = true;
			}
		}
		if ( ! array_key_exists( 'confirmation_message', $stored ) ) {
			$legacy_val = $this->find_legacy_language_value( $stored, 'confirmation_message' );
			if ( '' !== $legacy_val ) {
				$opts['confirmation_message'] = $legacy_val;
				$migrated = true;
			}
		}
		if ( $migrated ) {
			// Persist migrated values and remove legacy language keys.
			$clean = $opts;
			$legacy_keys = array();
			foreach ( array( 'initial_message', 'confirmation_message' ) as $base ) {
				foreach ( array( 'en', 'sr', 'hr', 'de', 'it', 'ru' ) as $lang ) {
					$legacy_keys[] = $base . '_' . $lang;
				}
			}
			foreach ( $legacy_keys as $lk ) {
				unset( $clean[ $lk ] );
			}
			update_option( self::OPTION_KEY, $clean );
		}

		return $opts;
	}

	/**
	 * Resolve app UI language from settings.
	 *
	 * @return string
	 */
	private function get_ui_language() {
		$settings = get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() );
		if ( ! is_array( $settings ) ) {
			return 'en';
		}

		$lang = isset( $settings['ui_language'] ) ? sanitize_text_field( $settings['ui_language'] ) : 'en';
		return '' !== $lang ? $lang : 'en';
	}

	/**
	 * Get the default localized coupon confirmation message.
	 *
	 * Uses an explicit "label: value" format so the frontend can attach
	 * the copy button without guessing from arbitrary prose.
	 *
	 * @return string
	 */
	private function get_default_confirmation_message() {
		$lang = $this->get_ui_language();

		if ( 0 === strpos( $lang, 'sr' ) || 0 === strpos( $lang, 'bs' ) ) {
			return 'Kod: {coupon_code}. Iskoristite ga na kasi u narednih {duration} minuta.';
		}

		if ( 0 === strpos( $lang, 'hr' ) ) {
			return 'Kod: {coupon_code}. Iskoristite ga na blagajni u sljedecih {duration} minuta.';
		}

		if ( 0 === strpos( $lang, 'de' ) ) {
			return 'Code: {coupon_code}. Sie koennen ihn in den naechsten {duration} Minuten an der Kasse verwenden.';
		}

		if ( 0 === strpos( $lang, 'es' ) ) {
			return 'Codigo: {coupon_code}. Puedes usarlo al finalizar la compra durante los proximos {duration} minutos.';
		}

		if ( 0 === strpos( $lang, 'fr' ) ) {
			return 'Code: {coupon_code}. Vous pouvez l utiliser au paiement pendant les {duration} prochaines minutes.';
		}

		return 'Code: {coupon_code}. You can use it at checkout in the next {duration} minutes.';
	}

	/**
	 * Find the best legacy language-specific value during one-time migration.
	 *
	 * @param array  $source   Stored options.
	 * @param string $base_key Message base key (initial_message / confirmation_message).
	 * @return string
	 */
	private function find_legacy_language_value( $source, $base_key ) {
		$lang = $this->get_ui_language();
		$keys = array(
			$base_key . '_' . $lang,
			$base_key . '_en',
			$base_key . '_sr',
			$base_key . '_hr',
		);

		foreach ( $keys as $key ) {
			if ( isset( $source[ $key ] ) && '' !== trim( (string) $source[ $key ] ) ) {
				return sanitize_textarea_field( (string) $source[ $key ] );
			}
		}

		return '';
	}

	/**
	 * Get localized admin labels for universal message fields.
	 *
	 * @return array
	 */
	private function get_message_field_labels() {
		$lang = $this->get_ui_language();
		$labels = array(
			'messages_heading'   => 'Messages',
			'initial_label'      => 'Initial Message',
			'initial_desc'       => 'Message shown when Conversion Busters starts the offer flow.',
			'confirmation_label' => 'Confirmation Message',
			'confirmation_desc'  => 'Message shown after coupon creation. You can use placeholders: {coupon_code}, {duration}, {discount_amount}.',
			'clarify_label'      => 'Clarify Message (fallback when user is undecided)',
			'clarify_desc'       => 'If empty, Attention Capture OS3 Clarify Message will be used automatically.',
		);

		$i18n = array(
			'sr' => array(
				'messages_heading'   => 'Poruke',
				'initial_label'      => 'Pocetna poruka',
				'initial_desc'       => 'Poruka koja se prikazuje kada Conversion Busters pokrene ponudu.',
				'confirmation_label' => 'Potvrdna poruka',
				'confirmation_desc'  => 'Poruka posle kreiranja kupona. Mozete koristiti placeholder-e: {coupon_code}, {duration}, {discount_amount}.',
				'clarify_label'      => 'Poruka za razjasnjenje (kada korisnik nije odlucan)',
				'clarify_desc'       => 'Ako je prazno, automatski se koristi Attention Capture OS3 poruka za razjasnjenje.',
			),
			'hr' => array(
				'messages_heading'   => 'Poruke',
				'initial_label'      => 'Pocetna poruka',
				'initial_desc'       => 'Poruka koja se prikazuje kada Conversion Busters pokrene ponudu.',
				'confirmation_label' => 'Potvrdna poruka',
				'confirmation_desc'  => 'Poruka nakon kreiranja kupona. Mozete koristiti placeholdere: {coupon_code}, {duration}, {discount_amount}.',
				'clarify_label'      => 'Poruka za pojasnjenje (kada korisnik nije odlucan)',
				'clarify_desc'       => 'Ako je prazno, automatski se koristi Attention Capture OS3 poruka za pojasnjenje.',
			),
			'de' => array(
				'messages_heading'   => 'Nachrichten',
				'initial_label'      => 'Startnachricht',
				'initial_desc'       => 'Nachricht, die angezeigt wird, wenn Conversion Busters das Angebot startet.',
				'confirmation_label' => 'Bestaetigungsnachricht',
				'confirmation_desc'  => 'Nachricht nach der Gutschein-Erstellung. Platzhalter: {coupon_code}, {duration}, {discount_amount}.',
				'clarify_label'      => 'Klaerungsnachricht (Fallback bei unklarer Antwort)',
				'clarify_desc'       => 'Wenn leer, wird automatisch die Attention Capture OS3 Klaerungsnachricht verwendet.',
			),
			'es' => array(
				'messages_heading'   => 'Mensajes',
				'initial_label'      => 'Mensaje inicial',
				'initial_desc'       => 'Mensaje que se muestra cuando Conversion Busters inicia la oferta.',
				'confirmation_label' => 'Mensaje de confirmacion',
				'confirmation_desc'  => 'Mensaje despues de crear el cupon. Placeholders: {coupon_code}, {duration}, {discount_amount}.',
				'clarify_label'      => 'Mensaje de aclaracion (fallback si el usuario duda)',
				'clarify_desc'       => 'Si esta vacio, se usara automaticamente el mensaje de aclaracion de Attention Capture OS3.',
			),
			'fr' => array(
				'messages_heading'   => 'Messages',
				'initial_label'      => 'Message initial',
				'initial_desc'       => 'Message affiche lorsque Conversion Busters demarre l offre.',
				'confirmation_label' => 'Message de confirmation',
				'confirmation_desc'  => 'Message apres creation du coupon. Placeholders: {coupon_code}, {duration}, {discount_amount}.',
				'clarify_label'      => 'Message de clarification (fallback si reponse incertaine)',
				'clarify_desc'       => 'Si vide, le message de clarification Attention Capture OS3 sera utilise automatiquement.',
			),
		);

		if ( isset( $i18n[ $lang ] ) ) {
			$labels = array_merge( $labels, $i18n[ $lang ] );
		}

		return $labels;
	}

	/**
	 * Build coupon confirmation message from template.
	 *
	 * @param array  $opts           Settings.
	 * @param string $coupon_code    Generated coupon code.
	 * @param int    $duration       Coupon duration in minutes.
	 * @param string $discount_label Discount label.
	 * @return string
	 */
	private function build_confirmation_message( $opts, $coupon_code, $duration, $discount_label ) {
		$template = isset( $opts['confirmation_message'] ) ? trim( (string) $opts['confirmation_message'] ) : '';
		if ( '' === $template ) {
			$template = $this->get_default_confirmation_message();
		}

		return strtr(
			$template,
			array(
				'{coupon_code}'        => (string) $coupon_code,
				'{duration}'           => (string) $duration,
				'{discount_amount}'    => (string) $discount_label,
				'{discount_amount_rsd}' => (string) $discount_label,
			)
		);
	}

	/**
	 * Get lead capture options used by the CB email hunt flow.
	 *
	 * @return array
	 */
	private function get_lead_capture_opts() {
		return class_exists( 'Niwa_AI_Lead_Capture_Settings' ) ? Niwa_AI_Lead_Capture_Settings::get_settings() : array();
	}

	/**
	 * Whether the CB code email hunt module is active.
	 *
	 * @return bool
	 */
	private function is_code_email_hunt_enabled() {
		$lead_opts = $this->get_lead_capture_opts();
		return ! empty( $lead_opts['cb_code_email_hunt_enabled'] );
	}

	/**
	 * Replace email template placeholders.
	 *
	 * @param string $template       Template text.
	 * @param array  $replacements   Placeholder replacements.
	 * @return string
	 */
	private function render_email_template( $template, $replacements ) {
		$base = array(
			'{site_name}' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
		);
		return strtr( (string) $template, array_merge( $base, $replacements ) );
	}

	/**
	 * Build the cart/recovery context used by CB coupon emails.
	 *
	 * @param string $session Session id from the current visitor.
	 * @param string $email   Captured shopper email.
	 * @return array<string, string>
	 */
	private function build_coupon_email_cart_context( $session = '', $email = '' ) {
		$cart_url     = function_exists( 'wc_get_cart_url' ) ? (string) wc_get_cart_url() : home_url( '/cart/' );
		$checkout_url = function_exists( 'wc_get_checkout_url' ) ? (string) wc_get_checkout_url() : home_url( '/checkout/' );
		$context      = array(
			'cart_url'        => $cart_url,
			'checkout_url'    => $checkout_url,
			'recovery_url'    => '',
			'cta_url'         => '' !== $checkout_url ? $checkout_url : $cart_url,
			'cta_label'       => $this->coupon_email_cta_label( false ),
			'cart_items'      => '',
			'cart_items_html' => '',
		);

		$row = null;
		if ( class_exists( 'Niwa_AI_Cart_Recovery_Repository' ) ) {
			$repo = new Niwa_AI_Cart_Recovery_Repository();
			if ( '' !== $session && '' !== $email ) {
				$row = $repo->find_by_session_and_email( (string) $session, (string) $email );
			}
			if ( ! is_array( $row ) && '' !== $email ) {
				$row = $repo->find_latest_by_email( (string) $email );
			}

			if (
				is_array( $row )
				&& ! empty( $row['unsubscribe_token'] )
				&& class_exists( 'Niwa_AI_Cart_Recovery_Template_Wrapper' )
			) {
				$context['recovery_url'] = ( new Niwa_AI_Cart_Recovery_Template_Wrapper() )->build_cart_recovery_url( (string) $row['unsubscribe_token'] );
				$context['cta_url']      = $context['recovery_url'];
				$context['cta_label']    = $this->coupon_email_cta_label( true );
			}
		}

		$snapshot = null;
		if ( class_exists( 'Niwa_AI_Cart_Recovery_Snapshot_Builder' ) ) {
			$candidate = ( new Niwa_AI_Cart_Recovery_Snapshot_Builder() )->build_from_current_cart();
			if ( $candidate instanceof Niwa_AI_Cart_Recovery_Cart_Snapshot && ! $candidate->is_empty() ) {
				$snapshot = $candidate;
			}
		}
		if (
			null === $snapshot
			&& is_array( $row )
			&& ! empty( $row['cart_snapshot_json'] )
			&& class_exists( 'Niwa_AI_Cart_Recovery_Cart_Snapshot' )
		) {
			$candidate = Niwa_AI_Cart_Recovery_Cart_Snapshot::from_json( (string) $row['cart_snapshot_json'] );
			if ( $candidate instanceof Niwa_AI_Cart_Recovery_Cart_Snapshot && ! $candidate->is_empty() ) {
				$snapshot = $candidate;
			}
		}

		if (
			$snapshot instanceof Niwa_AI_Cart_Recovery_Cart_Snapshot
			&& class_exists( 'Niwa_AI_Cart_Recovery_Composition_Context' )
			&& class_exists( 'Niwa_AI_Cart_Recovery_Placeholder_Renderer' )
		) {
			$ctx = new Niwa_AI_Cart_Recovery_Composition_Context(
				'mail1',
				function_exists( 'determine_locale' ) ? (string) determine_locale() : '',
				'',
				$snapshot,
				'',
				wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
				false,
				24
			);
			$map                        = Niwa_AI_Cart_Recovery_Placeholder_Renderer::build_map( $ctx );
			$context['cart_items']      = (string) ( $map['{cart_items}'] ?? '' );
			$context['cart_items_html'] = (string) ( $map['{cart_items_html}'] ?? '' );
		}

		return $context;
	}

	private function coupon_email_cta_label( bool $has_recovery_url ) {
		return $has_recovery_url ? __( 'Vrati me u korpu', 'niwa-ai' ) : __( 'Nastavi na plaćanje', 'niwa-ai' );
	}

	/**
	 * @param array<string, string> $cart_context Cart/recovery context.
	 * @return array<string, string>
	 */
	private function coupon_email_common_replacements( $coupon_code, array $cart_context ) {
		return array(
			'{coupon_code}'       => (string) $coupon_code,
			'{cart_url}'          => (string) ( $cart_context['cart_url'] ?? '' ),
			'{checkout_url}'      => (string) ( $cart_context['checkout_url'] ?? '' ),
			'{cart_recovery_url}' => (string) ( $cart_context['recovery_url'] ?? '' ),
			'{recovery_url}'      => (string) ( $cart_context['cta_url'] ?? '' ),
			'{cart_items}'        => (string) ( $cart_context['cart_items'] ?? '' ),
		);
	}

	/**
	 * Render a CB coupon email as HTML while preserving simple text templates.
	 *
	 * @param string                $template      Plain text admin/default body.
	 * @param array<string, string> $replacements  Placeholder map.
	 * @param array<string, string> $cart_context  Cart/recovery context.
	 * @return string
	 */
	private function render_coupon_email_html( $template, array $replacements, array $cart_context ) {
		$items_sentinel = '__NIWA_CB_CART_ITEMS_HTML__';
		$body           = $this->render_email_template(
			$template,
			array_merge(
				$replacements,
				array(
					'{cart_items_html}' => $items_sentinel,
				)
			)
		);

		$html = '<p>' . nl2br( esc_html( $body ) ) . '</p>';
		$items_html = (string) ( $cart_context['cart_items_html'] ?? '' );
		if ( '' !== $items_html ) {
			$html = str_replace( esc_html( $items_sentinel ), $items_html, $html );
			if ( ! str_contains( $html, 'data-niwa-cart-items="1"' ) ) {
				$html .= $items_html;
			}
		} else {
			$html = str_replace( esc_html( $items_sentinel ), '', $html );
		}

		$cta_url = (string) ( $cart_context['cta_url'] ?? '' );
		if ( '' !== $cta_url && ! str_contains( $html, $cta_url ) ) {
			$html .= '<p><a href="' . esc_url( $cta_url ) . '" style="display:inline-block;background:#111827;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:6px;font-weight:700;">' . esc_html( (string) ( $cart_context['cta_label'] ?? __( 'Nastavi na plaćanje', 'niwa-ai' ) ) ) . '</a></p>';
		}

		return $html;
	}

	/**
	 * @return array<int, string>
	 */
	private function coupon_email_headers() {
		return array( 'Content-Type: text/html; charset=UTF-8' );
	}

	/**
	 * Send generated coupon to the shopper.
	 *
	 * @param string $email          Shopper email.
	 * @param string $coupon_code    Coupon code.
	 * @param int    $duration       Coupon duration in minutes.
	 * @param string $discount_label Discount label.
	 * @return bool
	 */
	private function send_coupon_email( $email, $coupon_code, $duration, $discount_label, array $cart_context = array() ) {
		$lead_opts = $this->get_lead_capture_opts();
		$subject   = isset( $lead_opts['cb_code_email_hunt_email_subject'] ) ? trim( (string) $lead_opts['cb_code_email_hunt_email_subject'] ) : '';
		$body      = isset( $lead_opts['cb_code_email_hunt_email_body'] ) ? trim( (string) $lead_opts['cb_code_email_hunt_email_body'] ) : '';

		if ( '' === $subject ) {
			$subject = 'Vas kod za popust';
		}
		if ( '' === $body ) {
			$body = "Pozdrav,\n\nVas kod za popust je {coupon_code}.\nMozete ga iskoristiti na checkout stranici u narednih {duration} minuta.\n\nKliknite na dugme ispod da nastavite kupovinu.\n\nPozdrav od Niwe";
		}

		$cart_context = array_merge( $this->build_coupon_email_cart_context(), $cart_context );
		$body         = $this->render_coupon_email_html(
			$body,
			array_merge(
				$this->coupon_email_common_replacements( $coupon_code, $cart_context ),
				array(
					'{duration}'        => (string) $duration,
					'{discount_amount}' => $discount_label,
				)
			),
			$cart_context
		);

		return (bool) wp_mail( $email, $subject, $body, $this->coupon_email_headers() );
	}

	/**
	 * Schedule reminder after 80% of coupon lifetime if the code remains unused.
	 *
	 * @param string $coupon_code Coupon code.
	 * @param string $email       Shopper email.
	 * @param int    $duration    Coupon duration in minutes.
	 */
	private function schedule_coupon_reminder( $coupon_code, $email, $duration, array $cart_context = array() ) {
		$duration_seconds  = max( 60, absint( $duration ) * MINUTE_IN_SECONDS );
		$delay_seconds     = max( 1, (int) floor( $duration_seconds * 0.8 ) );
		$remaining_minutes = max( 1, (int) ceil( ( $duration_seconds - $delay_seconds ) / MINUTE_IN_SECONDS ) );

		wp_schedule_single_event(
			time() + $delay_seconds,
			self::REMINDER_HOOK,
			array( $coupon_code, $email, $remaining_minutes, $this->get_ui_language(), time(), $cart_context )
		);
	}

	/**
	 * Send unused-code reminder.
	 *
	 * @param string $coupon_code       Coupon code.
	 * @param string $email             Shopper email.
	 * @param int    $remaining_minutes Remaining minutes.
	 * @param string $language          UI language at scheduling time.
	 * @param int    $created_at        Scheduling timestamp.
	 */
	public function send_coupon_reminder( $coupon_code, $email, $remaining_minutes = 2, $language = '', $created_at = 0, $cart_context = array() ) {
		$coupon_code = sanitize_text_field( (string) $coupon_code );
		$email       = sanitize_email( (string) $email );
		$cart_context = is_array( $cart_context ) ? $cart_context : array();

		if ( '' === $coupon_code || ! is_email( $email ) || ! $this->is_coupon_unused( $coupon_code ) ) {
			return;
		}

		$lead_opts = $this->get_lead_capture_opts();
		$subject   = isset( $lead_opts['cb_code_email_hunt_reminder_subject'] ) ? trim( (string) $lead_opts['cb_code_email_hunt_reminder_subject'] ) : '';
		$body      = isset( $lead_opts['cb_code_email_hunt_reminder_body'] ) ? trim( (string) $lead_opts['cb_code_email_hunt_reminder_body'] ) : '';

		if ( '' === $subject ) {
			$subject = 'Kod za popust uskoro istice';
		}
		if ( '' === $body ) {
			$body = "Vidim da niste iskoristili kod koji sam vam poslala, molim vas pozurite jer vazi jos samo {remaining_minutes} minuta.\n\nKod: {coupon_code}\n\nKliknite na dugme ispod da se vratite u kupovinu.";
		}

		$cart_context = array_merge( $this->build_coupon_email_cart_context( '', $email ), $cart_context );
		$body         = $this->render_coupon_email_html(
			$body,
			array_merge(
				$this->coupon_email_common_replacements( $coupon_code, $cart_context ),
				array(
					'{remaining_minutes}' => (string) max( 1, absint( $remaining_minutes ) ),
				)
			),
			$cart_context
		);

		wp_mail( $email, $subject, $body, $this->coupon_email_headers() );
	}

	/**
	 * Check coupon analytics and Woo coupon usage before reminder email.
	 *
	 * @param string $coupon_code Coupon code.
	 * @return bool
	 */
	private function is_coupon_unused( $coupon_code ) {
		global $wpdb;

		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) !== $table_name ) {
			return true;
		}
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$status = $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$table_name} WHERE coupon_code = %s ORDER BY id DESC LIMIT 1", $coupon_code ) );
		if ( $status && 'created' !== $status ) {
			return false;
		}

		if ( class_exists( 'WC_Coupon' ) ) {
			$coupon = new \WC_Coupon( $coupon_code );
			if ( $coupon->get_id() && (int) $coupon->get_usage_count() > 0 ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Create the analytics table.
	 */
	public function create_analytics_table() {
		global $wpdb;

		$table_name      = $wpdb->prefix . self::ANALYTICS_TABLE;
		$charset_collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			coupon_code varchar(50) NOT NULL DEFAULT '',
			user_session varchar(100) NOT NULL DEFAULT '',
			user_ip varchar(45) NOT NULL DEFAULT '',
			created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
			used_at datetime DEFAULT NULL,
			order_id bigint(20) unsigned DEFAULT NULL,
			order_total decimal(10,2) DEFAULT NULL,
			discount_amount decimal(10,2) DEFAULT NULL,
			status varchar(20) DEFAULT 'created' NOT NULL,
			PRIMARY KEY  (id),
			KEY coupon_code (coupon_code),
			KEY user_session (user_session),
			KEY user_ip (user_ip)
		) {$charset_collate};";

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

	// ---- AJAX Handlers ----

	/**
	 * Check cart status.
	 */
	public function ajax_check_cart() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );
		$session = $this->require_verified_frontend_session( true );
		$this->enforce_public_endpoint_limits( 'coupon_cart_check', $session, 60, 2000 );

		if ( ! class_exists( 'WooCommerce' ) || ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_success( array(
				'cart_count'   => 0,
				'cart_total'   => 0,
				'threshold'    => 0,
				'minimum_amount' => 0,
				'cooldown_active' => false,
				'should_trigger' => false,
				'should_offer' => false,
			) );
			return;
		}

		$opts = $this->get_opts();
		if ( empty( $opts['enabled'] ) ) {
			wp_send_json_success( array(
				'cart_count'      => 0,
				'cart_total'      => 0,
				'threshold'       => (int) ( $opts['cart_threshold'] ?? 2 ),
				'minimum_amount'  => (float) ( $opts['minimum_amount'] ?? 0 ),
				'cooldown_active' => false,
				'should_trigger'  => false,
				'should_offer'    => false,
			) );
			return;
		}

		$cart_count     = WC()->cart->get_cart_contents_count();
		$totals         = WC()->cart->get_totals();
		$cart_total     = isset( $totals['subtotal'] ) ? (float) $totals['subtotal'] : 0.0;
		$threshold      = (int) ( $opts['cart_threshold'] ?? 2 );
		$minimum_amount = (float) ( $opts['minimum_amount'] ?? 0 );
		$user_ip        = Niwa_AI_Session_Utils::get_client_ip();
		$retry_after    = $this->coupon_retry_after_seconds( $opts, $session, $user_ip );

		$should_trigger = $cart_count >= $threshold;
		if ( $should_trigger && $minimum_amount > 0 ) {
			$should_trigger = $cart_total >= $minimum_amount;
		}
		if ( $should_trigger && $retry_after > 0 ) {
			$should_trigger = false;
		}

		$offer_text = isset( $opts['initial_message'] ) ? trim( (string) $opts['initial_message'] ) : '';

		wp_send_json_success( array(
			'cart_count'      => (int) $cart_count,
			'cart_total'      => $cart_total,
			'threshold'       => $threshold,
			'minimum_amount'  => $minimum_amount,
			'cooldown_active' => $retry_after > 0,
			'retry_after'     => $retry_after,
			'offer_text'      => $offer_text,
			'should_trigger'  => $should_trigger,
			'should_offer'    => $should_trigger,
			'offer_token'     => $should_trigger ? $this->create_coupon_offer_token( $session, 'cart' ) : '',
		) );
	}

	/**
	 * Detect yes/no response.
	 */
	public function ajax_detect_response() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

		$raw_message = isset( $_POST['message'] ) && is_scalar( $_POST['message'] ) ? (string) wp_unslash( $_POST['message'] ) : '';
		if ( ( isset( $_SERVER['CONTENT_LENGTH'] ) && (int) $_SERVER['CONTENT_LENGTH'] > 8192 ) || strlen( $raw_message ) > 2000 ) {
			wp_send_json_error( array( 'message' => 'Offer response payload is too large.' ), 413 );
		}
		$message = sanitize_text_field( $raw_message );
		$mode    = isset( $_POST['mode'] ) && 'mini' === sanitize_key( wp_unslash( $_POST['mode'] ) ) ? 'mini' : 'cart';
		$session = $this->require_verified_frontend_session( true );
		$ip      = Niwa_AI_Session_Utils::get_client_ip();
		foreach (
			array(
				array( 'coupon_intent_global_minute', 'global', 300 ),
				array( 'coupon_intent_ip_minute', $ip, 20 ),
				array( 'coupon_intent_session_minute', $session, 12 ),
			) as $limit
		) {
			if ( ! Niwa_AI_Session_Utils::consume_fixed_window_limit( $limit[0], $limit[1], $limit[2], MINUTE_IN_SECONDS ) ) {
				wp_send_json_error( array( 'message' => 'Offer response request limit reached.' ), 429 );
			}
		}
		$result = $this->classify_offer_response_intent( $message );

		wp_send_json_success(
			array(
				'intent'     => $result['intent'],
				'confidence' => $result['confidence'],
				'is_yes'     => 'yes' === $result['intent'],
				'is_no'      => 'no' === $result['intent'],
				'offer_token' => 'yes' === $result['intent'] ? $this->create_coupon_offer_token( $session, $mode ) : '',
			)
		);
	}

	/**
	 * Append Conversion Busters transcript entries to the main frontend conversation.
	 */
	public function ajax_append_history() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );
		$session = $this->require_verified_frontend_session( false );
		$this->enforce_public_endpoint_limits( 'coupon_history', $session, 60, 1000 );

		if ( ! class_exists( 'Niwa_AI_Conversation_Store' ) ) {
			wp_send_json_error(
				array(
					'error'   => 'Conversation store is not available.',
					'message' => 'Conversation store is not available.',
				),
				500
			);
		}

		$raw_entries = isset( $_POST['entries'] ) ? wp_unslash( $_POST['entries'] ) : '';
		if ( ! is_string( $raw_entries ) || strlen( $raw_entries ) > 32768 ) {
			wp_send_json_error( array( 'message' => 'History payload is too large.' ), 413 );
		}
		$entries     = is_string( $raw_entries ) ? json_decode( $raw_entries, true ) : array();
		if ( ! is_array( $entries ) || empty( $entries ) ) {
			wp_send_json_success(
				array(
					'history' => array(),
				)
			);
		}

		$store            = new Niwa_AI_Conversation_Store();
		$history          = $store->get( $session );

		foreach ( array_slice( $entries, 0, 20 ) as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}

			$role = isset( $entry['role'] ) ? sanitize_key( (string) $entry['role'] ) : '';
			$text = isset( $entry['text'] ) ? sanitize_textarea_field( (string) $entry['text'] ) : '';
			$text = function_exists( 'mb_substr' ) ? mb_substr( $text, 0, 2000 ) : substr( $text, 0, 2000 );

			if ( '' === $text || ! in_array( $role, array( 'user', 'bot', 'assistant' ), true ) ) {
				continue;
			}

			$history = $store->append( $session, $role, $text );
		}

		wp_send_json_success(
			array(
				'session' => $session,
				'history' => is_array( $history ) ? $history : array(),
			)
		);
	}

	/**
	 * Remove stale Conversion Busters prompts from the main frontend conversation.
	 */
	public function ajax_prune_history() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );
		$session = $this->require_verified_frontend_session( false );
		$this->enforce_public_endpoint_limits( 'coupon_history', $session, 60, 1000 );

		if ( ! class_exists( 'Niwa_AI_Conversation_Store' ) ) {
			wp_send_json_error(
				array(
					'error'   => 'Conversation store is not available.',
					'message' => 'Conversation store is not available.',
				),
				500
			);
		}

		$raw_entries = isset( $_POST['entries'] ) ? wp_unslash( $_POST['entries'] ) : '';
		if ( ! is_string( $raw_entries ) || strlen( $raw_entries ) > 32768 ) {
			wp_send_json_error( array( 'message' => 'History payload is too large.' ), 413 );
		}
		$entries     = is_string( $raw_entries ) ? json_decode( $raw_entries, true ) : array();
		$targets     = $this->sanitize_history_prune_entries( $entries );
		if ( empty( $targets ) ) {
			wp_send_json_success(
				array(
					'removed' => 0,
					'history' => array(),
				)
			);
		}

		$store            = new Niwa_AI_Conversation_Store();
		$history          = $store->get( $session );
		$filtered         = array();
		$removed          = 0;

		foreach ( (array) $history as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$role = isset( $item['role'] ) ? sanitize_key( (string) $item['role'] ) : 'bot';
			$text = isset( $item['text'] ) ? (string) $item['text'] : ( isset( $item['content'] ) ? (string) $item['content'] : '' );
			$key  = $role . self::PRUNE_KEY_SEPARATOR . $this->normalize_history_prune_text( $text );

			if ( isset( $targets[ $key ] ) ) {
				$removed++;
				continue;
			}

			$filtered[] = $item;
		}

		if ( $removed > 0 ) {
			$history = $store->replace( $session, $filtered );
		}

		wp_send_json_success(
			array(
				'session' => $session,
				'removed' => $removed,
				'history' => is_array( $history ) ? $history : array(),
			)
		);
	}

	/**
	 * Resolve coupon settings for the OS3 "CB Mini" flow.
	 *
	 * OS3 lives in the Attention Capture module and offers a coupon independently
	 * from this cart-abandonment module. We map its settings into the same shape
	 * the coupon creator expects, reusing the CB cooldown only for anti-abuse
	 * throttling (so OS3 needs no separate cooldown field).
	 *
	 * @param array $cb_opts Conversion Busters settings (for the shared cooldown).
	 * @return array<string, mixed>
	 */
	private function get_cb_mini_coupon_opts( $cb_opts ) {
		$os3 = class_exists( 'Niwa_AI_Attention_Capture' )
			? Niwa_AI_Attention_Capture::get_os3_coupon_settings()
			: array();

		return array(
			'enabled'              => ! empty( $os3['enabled'] ),
			'discount_type'        => isset( $os3['discount_type'] ) ? (string) $os3['discount_type'] : 'percent',
			'discount_amount'      => isset( $os3['discount_amount'] ) ? (float) $os3['discount_amount'] : 10,
			'coupon_duration'      => isset( $os3['coupon_duration'] ) ? (int) $os3['coupon_duration'] : 10,
			'coupon_prefix'        => isset( $os3['coupon_prefix'] ) ? (string) $os3['coupon_prefix'] : 'NIWA',
			'max_uses'             => isset( $os3['max_uses'] ) ? (int) $os3['max_uses'] : 1,
			'minimum_amount'       => isset( $os3['minimum_amount'] ) ? (float) $os3['minimum_amount'] : 0,
			'confirmation_message' => isset( $os3['confirmation_message'] ) ? (string) $os3['confirmation_message'] : '',
			'session_cooldown'     => isset( $cb_opts['session_cooldown'] ) ? (int) $cb_opts['session_cooldown'] : 30,
		);
	}

	/**
	 * Create a coupon code.
	 */
	public function ajax_create_coupon() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );
		if ( isset( $_SERVER['CONTENT_LENGTH'] ) && (int) $_SERVER['CONTENT_LENGTH'] > 16384 ) {
			wp_send_json_error( array( 'message' => 'Coupon request payload is too large.' ), 413 );
		}

		if ( ! class_exists( 'WooCommerce' ) ) {
			$message = 'WooCommerce is not active.';
			wp_send_json_error(
				array(
					'error' => $message,
					'message' => $message,
				)
			);
			return;
		}

		$opts = $this->get_opts();

		$email      = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
		$is_cb_mini = isset( $_POST['is_cb_mini'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['is_cb_mini'] ) );
		$session    = $this->require_verified_frontend_session( false );
		$offer_token = isset( $_POST['offer_token'] ) ? sanitize_text_field( wp_unslash( $_POST['offer_token'] ) ) : '';
		$offer       = $this->verify_coupon_offer_token( $offer_token, $session, $is_cb_mini ? 'mini' : 'cart' );
		if ( empty( $offer ) ) {
			wp_send_json_error(
				array(
					'error'   => 'The discount offer is invalid or expired.',
					'message' => 'The discount offer is invalid or expired.',
				),
				403
			);
		}
		$idempotency_key = 'niwa_ai_cb_offer_result_' . md5( (string) $offer['jti'] );
		$prior_result    = get_transient( $idempotency_key );
		if ( is_array( $prior_result ) ) {
			wp_send_json_success( $prior_result );
		}

		// OS3 (CB Mini) offers a coupon using its own Attention Capture settings,
		// independent from the Conversion Busters cart-abandonment module.
		$coupon_opts = $is_cb_mini ? $this->get_cb_mini_coupon_opts( $opts ) : $opts;

		if ( empty( $coupon_opts['enabled'] ) ) {
			$message = $is_cb_mini
				? __( 'The discount offer is currently disabled.', 'niwa-ai' )
				: __( 'Conversion Busters is disabled.', 'niwa-ai' );
			wp_send_json_error(
				array(
					'error'   => $message,
					'message' => $message,
				),
				403
			);
			return;
		}

		$email_hunt_enabled = $this->is_code_email_hunt_enabled();
		$user_ip = Niwa_AI_Session_Utils::get_client_ip();

		if ( $email_hunt_enabled ) {
			if ( ! is_email( $email ) ) {
				$message = __( 'Please enter a valid email address before I send the code.', 'niwa-ai' );
				wp_send_json_error(
					array(
						'error'   => $message,
						'message' => $message,
					),
					400
				);
				return;
			}

			if ( class_exists( 'Niwa_AI_Email_Validator' ) ) {
				$email_check = ( new Niwa_AI_Email_Validator() )->validate( $email );
				if ( empty( $email_check['valid'] ) ) {
					$message = isset( $email_check['message'] ) ? (string) $email_check['message'] : __( 'Please enter a valid email address before I send the code.', 'niwa-ai' );
					wp_send_json_error(
						array(
							'error'   => $message,
							'message' => $message,
						),
						400
					);
					return;
				}
			}

			if ( class_exists( 'Niwa_AI_Contact_Service' ) ) {
				( new Niwa_AI_Contact_Service() )->save_contact( $email, '', '', 'cb_code_email_hunt' );
			}
		}

		if ( ! $is_cb_mini ) {
			if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
				$message = __( 'Cart is not available.', 'niwa-ai' );
				wp_send_json_error(
					array(
						'reason'  => 'cart_unavailable',
						'error'   => $message,
						'message' => $message,
					),
					400
				);
				return;
			}

			$cart_count = WC()->cart->get_cart_contents_count();
			$totals     = WC()->cart->get_totals();
			$cart_total = isset( $totals['subtotal'] ) ? (float) $totals['subtotal'] : 0.0;
			$threshold  = (int) ( $opts['cart_threshold'] ?? 2 );
			$minimum_amount = (float) ( $opts['minimum_amount'] ?? 0 );

			if ( $cart_count < $threshold ) {
				$message = __( 'Cart threshold not met.', 'niwa-ai' );
				wp_send_json_error(
					array(
						'reason'   => 'cart_threshold_not_met',
						'error'    => $message,
						'message'  => $message,
						'required' => $threshold,
						'current'  => (int) $cart_count,
					),
					400
				);
				return;
			}

			if ( $minimum_amount > 0 && $cart_total < $minimum_amount ) {
				$message = __( 'Minimum amount not met.', 'niwa-ai' );
				wp_send_json_error(
					array(
						'reason'   => 'cart_minimum_not_met',
						'error'    => $message,
						'message'  => $message,
						'required' => $minimum_amount,
						'current'  => $cart_total,
					),
					400
				);
				return;
			}
		}

		$retry_after = $this->coupon_retry_after_seconds( $coupon_opts, $session, $user_ip );
		if ( $retry_after > 0 ) {
			$message = sprintf(
				/* translators: %d: number of seconds */
				__( 'Please wait %d seconds before requesting another coupon.', 'niwa-ai' ),
				$retry_after
			);
			wp_send_json_error(
				array(
					'error'       => $message,
					'message'     => $message,
					'retry_after' => $retry_after,
				),
				429
			);
			return;
		}

		if ( ! $this->consume_coupon_abuse_limits( $session, $user_ip, $email ) ) {
			wp_send_json_error(
				array(
					'error'       => 'Coupon request limit reached. Please try again later.',
					'message'     => 'Coupon request limit reached. Please try again later.',
					'retry_after' => HOUR_IN_SECONDS,
				),
				429
			);
		}
		if ( ! $this->claim_coupon_offer( (string) $offer['jti'] ) ) {
			$prior_result = get_transient( $idempotency_key );
			if ( is_array( $prior_result ) ) {
				wp_send_json_success( $prior_result );
			}
			wp_send_json_error( array( 'message' => 'This discount offer is already being processed.' ), 409 );
		}

		$prefix = sanitize_text_field( $coupon_opts['coupon_prefix'] ?? 'NIWA' );
		$code   = strtoupper( $prefix . wp_generate_password( 6, false ) );

		$coupon = new \WC_Coupon();
		$coupon->set_code( $code );
		$coupon->set_discount_type( $coupon_opts['discount_type'] ?? 'percent' );
		$coupon->set_amount( floatval( $coupon_opts['discount_amount'] ?? 10 ) );
		$coupon->set_usage_limit( (int) ( $coupon_opts['max_uses'] ?? 1 ) );
		$coupon->set_minimum_amount( floatval( $coupon_opts['minimum_amount'] ?? 0 ) );
		$coupon->set_individual_use( true );

		// Set expiration.
		$duration = max( 1, (int) ( $coupon_opts['coupon_duration'] ?? 10 ) );
		$expiry   = new \DateTime( 'now', new \DateTimeZone( wp_timezone_string() ) );
		$expiry->modify( '+' . $duration . ' minutes' );
		$coupon->set_date_expires( $expiry->getTimestamp() );

		if ( ! empty( $email ) ) {
			$coupon->set_email_restrictions( array( $email ) );
		}

		$coupon->save();
		$this->mark_coupon_throttle( $coupon_opts, $session, $user_ip );

		// Record analytics.
		$this->record_analytics( $code, $session, $user_ip );

		$discount_label = $coupon_opts['discount_type'] === 'percent'
			? rtrim( rtrim( number_format( (float) $coupon_opts['discount_amount'], 2, '.', '' ), '0' ), '.' ) . '%'
			: wp_strip_all_tags( html_entity_decode( wc_price( (float) $coupon_opts['discount_amount'] ), ENT_QUOTES, get_bloginfo( 'charset' ) ) );

		$message = $this->build_confirmation_message( $coupon_opts, $code, $duration, $discount_label );
		$email_sent = false;
		if ( $email_hunt_enabled && is_email( $email ) ) {
			$cart_context = $this->build_coupon_email_cart_context( $session, $email );
			$email_sent   = $this->send_coupon_email( $email, $code, $duration, $discount_label, $cart_context );
			$this->schedule_coupon_reminder( $code, $email, $duration, $cart_context );
		}

		$response_payload = array(
			'coupon_code'      => $code,
			'message'          => $message,
			'email_sent'       => $email_sent,
			'discount_amount'  => $discount_label,
			'discount_amount_rsd' => $discount_label,
			'duration'         => $duration,
			'expires_in'       => $duration,
			'duration_minutes' => $duration,
		);
		set_transient( $idempotency_key, $response_payload, DAY_IN_SECONDS );
		wp_send_json_success( $response_payload );
	}

	/**
	 * Get initial CB message.
	 */
	public function ajax_get_initial_message() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

		$opts = $this->get_opts();
		$message = isset( $opts['initial_message'] ) ? (string) $opts['initial_message'] : '';

		wp_send_json_success( array( 'message' => $message ) );
	}

	/**
	 * Record analytics entry.
	 *
	 * @param string $coupon_code Coupon code.
	 */
	private function record_analytics( $coupon_code, $session = '', $user_ip = '' ) {
		global $wpdb;

		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;

		// Check table exists.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) !== $table_name ) {
			$this->create_analytics_table();
		}

		$wpdb->insert(
			$table_name,
			array(
				'coupon_code'  => $coupon_code,
				'user_session' => (string) $session,
				'user_ip'      => '' !== $user_ip ? (string) $user_ip : Niwa_AI_Session_Utils::get_client_ip(),
				'status'       => 'created',
			),
			array( '%s', '%s', '%s', '%s' )
		);
	}

	/**
	 * Track coupon usage against Woo orders.
	 *
	 * @param int $order_id WooCommerce order id.
	 */
	public function track_coupon_usage( $order_id ) {
		$order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;
		if ( ! $order ) {
			return;
		}

		$coupons = $order->get_coupon_codes();
		if ( empty( $coupons ) ) {
			return;
		}

		global $wpdb;
		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;

		foreach ( $coupons as $coupon_code ) {
			$wpdb->update(
				$table_name,
				array(
					'used_at'         => current_time( 'mysql' ),
					'order_id'        => $order_id,
					'order_total'     => $order->get_total(),
					'discount_amount' => $order->get_discount_total(),
					'status'          => 'used',
				),
				array(
					'coupon_code' => $coupon_code,
					'status'      => 'created',
				),
				array( '%s', '%d', '%f', '%f', '%s' ),
				array( '%s', '%s' )
			);
		}
	}

	/**
	 * Mark expired created coupons in analytics.
	 */
	public function cleanup_expired_coupons() {
		if ( ! class_exists( 'WooCommerce' ) || ! class_exists( 'WC_Coupon' ) ) {
			return;
		}

		global $wpdb;
		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;

		$expired_coupons = $wpdb->get_results(
			"SELECT coupon_code
			FROM {$table_name}
			WHERE status = 'created'
			  AND created_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)"
		);

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

		foreach ( $expired_coupons as $coupon_row ) {
			if ( empty( $coupon_row->coupon_code ) ) {
				continue;
			}

			$coupon = new \WC_Coupon( $coupon_row->coupon_code );
			$expiry = $coupon->get_date_expires();
			if ( ! $coupon->get_id() || ! $expiry ) {
				continue;
			}

			if ( time() > $expiry->getTimestamp() ) {
				$wpdb->update(
					$table_name,
					array( 'status' => 'expired' ),
					array(
						'coupon_code' => $coupon_row->coupon_code,
						'status'      => 'created',
					),
					array( '%s' ),
					array( '%s', '%s' )
				);
			}
		}
	}

	/**
	 * Return remaining cooldown seconds for coupon creation.
	 *
	 * @param array  $opts    Plugin settings.
	 * @param string $session Session id.
	 * @param string $user_ip Client ip.
	 * @return int
	 */
	private function coupon_retry_after_seconds( $opts, $session, $user_ip ) {
		$key = $this->build_coupon_throttle_key( $session, $user_ip );
		$until = intval( get_transient( $key ) );
		if ( $until <= time() ) {
			return 0;
		}
		return max( 0, $until - time() );
	}

	/**
	 * Mark coupon cooldown for the current user/session.
	 *
	 * @param array  $opts    Plugin settings.
	 * @param string $session Session id.
	 * @param string $user_ip Client ip.
	 */
	private function mark_coupon_throttle( $opts, $session, $user_ip ) {
		$cooldown_seconds = $this->get_coupon_cooldown_seconds( $opts );
		$until = time() + $cooldown_seconds;
		set_transient( $this->build_coupon_throttle_key( $session, $user_ip ), $until, $cooldown_seconds + MINUTE_IN_SECONDS );
	}

	/**
	 * Build transient key for coupon throttle.
	 *
	 * @param string $session Session id.
	 * @param string $user_ip Client ip.
	 * @return string
	 */
	private function build_coupon_throttle_key( $session, $user_ip ) {
		$fingerprint = trim( (string) $session ) . '|' . trim( (string) $user_ip );
		$fingerprint = trim( $fingerprint, '|' );
		if ( '' === $fingerprint ) {
			$fingerprint = 'anon';
		}
		return self::THROTTLE_PREFIX . md5( $fingerprint );
	}

	/**
	 * Resolve cooldown in seconds from plugin settings.
	 *
	 * @param array $opts Settings.
	 * @return int
	 */
	private function get_coupon_cooldown_seconds( $opts ) {
		$minutes = isset( $opts['session_cooldown'] ) ? absint( $opts['session_cooldown'] ) : 30;
		$minutes = max( 1, $minutes );
		return $minutes * MINUTE_IN_SECONDS;
	}

	/**
	 * Fast local keyword/pattern classifier for yes/no intent.
	 * Runs before the LLM call to handle common affirmations/negations without
	 * any API round-trip. Returns confidence >= 0.95 when certain.
	 *
	 * @param string $message Normalised (lowercased, trimmed) shopper reply.
	 * @return array{intent:string,confidence:float}
	 */
	private function classify_local_intent( $message ) {
		$text = mb_strtolower( trim( (string) $message ), 'UTF-8' );

		// Affirmations: en / sr / hr / bs / de / fr / es / it / ru (transliterated)
		$yes_exact = array(
			'yes', 'yeah', 'yep', 'yup', 'yea', 'sure', 'ok', 'okay', 'k',
			'of course', 'absolutely', 'definitely', 'please', 'please do',
			'go ahead', 'generate it', 'give me', 'send it', 'create it',
			// sr / hr / bs
			'da', 'jeste', 'naravno', 'svakako', 'hocu', 'hoću', 'jasno',
			'sigurno', 'daj', 'daj mi', 'generisi', 'generiši', 'kreiraj',
			// de
			'ja', 'sicher', 'klar', 'bitte', 'gerne',
			// fr
			'oui', 'bien sûr', 'bien sur', 'd\'accord',
			// es
			'sí', 'si', 'claro', 'por favor',
			// it
			'sì', 'certo', 'va bene',
		);

		// Negations
		$no_exact = array(
			'no', 'nope', 'nah', 'nay', 'no thanks', 'no thank you',
			'cancel', 'nevermind', 'never mind', 'skip', 'pass', 'stop',
			'not interested', 'not now', 'maybe later', 'later',
			// sr / hr / bs
			'ne', 'ne hvala', 'otkaži', 'otkazi', 'nije potrebno', 'nije',
			// de
			'nein', 'danke nein', 'nein danke', 'kein interesse',
			// fr
			'non', 'non merci', 'pas maintenant',
			// es
			'no gracias',
			// it
			'no grazie',
		);

		if ( in_array( $text, $yes_exact, true ) ) {
			return array( 'intent' => 'yes', 'confidence' => 0.97 );
		}
		if ( in_array( $text, $no_exact, true ) ) {
			return array( 'intent' => 'no', 'confidence' => 0.97 );
		}

		// Word-boundary scan for multi-word messages
		foreach ( $yes_exact as $phrase ) {
			if ( mb_strlen( $phrase, 'UTF-8' ) >= 3 ) {
				$pattern = '/\b' . preg_quote( $phrase, '/' ) . '\b/iu';
				if ( preg_match( $pattern, $text ) ) {
					return array( 'intent' => 'yes', 'confidence' => 0.92 );
				}
			}
		}
		foreach ( $no_exact as $phrase ) {
			if ( mb_strlen( $phrase, 'UTF-8' ) >= 3 ) {
				$pattern = '/\b' . preg_quote( $phrase, '/' ) . '\b/iu';
				if ( preg_match( $pattern, $text ) ) {
					return array( 'intent' => 'no', 'confidence' => 0.92 );
				}
			}
		}

		return array( 'intent' => 'unclear', 'confidence' => 0 );
	}

	/**
	 * Semantic classifier for discount-offer replies.
	 * Tries the fast local classifier first; falls back to the LLM only for
	 * ambiguous messages that the pattern list cannot resolve.
	 *
	 * @param string $message Shopper reply.
	 * @return array{intent:string,confidence:float}
	 */
	private function classify_offer_response_intent( $message ) {
		$message = trim( (string) $message );
		if ( '' === $message ) {
			return array(
				'intent'     => 'unclear',
				'confidence' => 0,
			);
		}

		// Fast path: local classifier handles common affirmations/negations
		// without any API call. Threshold 0.9 keeps edge cases for the LLM.
		$local = $this->classify_local_intent( $message );
		if ( $local['confidence'] >= 0.9 ) {
			return $local;
		}

		if ( ! class_exists( 'Niwa_AI_OpenAI_Client' ) || ! class_exists( 'Niwa_AI_Auth_Manager' ) || ! class_exists( 'Niwa_AI_Model_Router' ) ) {
			return array(
				'intent'     => 'unclear',
				'confidence' => 0,
			);
		}

		$auth_manager = new Niwa_AI_Auth_Manager( 'frontend' );
		$auth_mode    = $auth_manager->get_mode();
		$creds        = $auth_manager->get_credentials();
		$model_router = new Niwa_AI_Model_Router( class_exists( 'Niwa_AI_Model_Catalog' ) ? new Niwa_AI_Model_Catalog() : null );
		$model        = $model_router->route_for_frontend_chat();
		$client       = new Niwa_AI_OpenAI_Client( 'frontend_fast' );

		$instructions = implode(
			"\n",
			array(
				'Classify the shopper reply to a discount-code offer.',
				'The assistant previously asked whether the shopper wants a discount coupon or discount code.',
				'Interpret the shopper reply in any language.',
				'Use yes when the shopper is accepting, confirming, or asking the assistant to proceed with the coupon.',
				'Use no when the shopper is declining, postponing, cancelling, or clearly saying they do not want the coupon.',
				'Use unclear for questions, unrelated replies, ambiguous replies, gratitude, or anything that is not a clear yes/no decision.',
				'Do not rely on exact keywords. Infer the meaning semantically.',
				'Return minified JSON only: {"intent":"yes|no|unclear","confidence":0.0}.',
			)
		);

		$input = "[ASSISTANT CONTEXT]\nThe shopper is replying to a live discount-code offer in chat.\n[/ASSISTANT CONTEXT]\n\n[SHOPPER MESSAGE]\n" . $message . "\n[/SHOPPER MESSAGE]";

		$result = $client->generate( $model, $input, $instructions, $auth_mode, $creds, 20 );
		if ( empty( $result['success'] ) || empty( $result['answer'] ) ) {
			return array(
				'intent'     => 'unclear',
				'confidence' => 0,
			);
		}

		return $this->parse_offer_response_classifier_payload( $result['answer'] );
	}

	/**
	 * Sanitize frontend-provided history prune targets into lookup keys.
	 *
	 * @param mixed $entries Raw JSON-decoded entries.
	 * @return array<string,bool>
	 */
	private function sanitize_history_prune_entries( $entries ) {
		if ( ! is_array( $entries ) ) {
			return array();
		}

		$targets = array();
		foreach ( $entries as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}

			$role = isset( $entry['role'] ) ? sanitize_key( (string) $entry['role'] ) : '';
			$text = isset( $entry['text'] ) ? sanitize_textarea_field( (string) $entry['text'] ) : '';
			$text = $this->normalize_history_prune_text( $text );

			if ( '' === $text || ! in_array( $role, array( 'user', 'bot', 'assistant' ), true ) ) {
				continue;
			}

			$targets[ $role . self::PRUNE_KEY_SEPARATOR . $text ] = true;
		}

		return $targets;
	}

	/**
	 * Normalize chat text for exact-history pruning.
	 *
	 * @param string $text Message text.
	 * @return string
	 */
	private function normalize_history_prune_text( $text ) {
		$text = preg_replace( '/\s+/u', ' ', (string) $text );
		return trim( null === $text ? '' : $text );
	}

	private function require_verified_frontend_session( $create_if_missing ) {
		$legacy  = isset( $_POST['session'] ) ? sanitize_text_field( wp_unslash( $_POST['session'] ) ) : '';
		$session = Niwa_AI_Session_Utils::resolve_verified_session( (bool) $create_if_missing, $legacy );
		if ( '' === $session ) {
			wp_send_json_error(
				array(
					'error'   => 'Invalid frontend session credential.',
					'message' => 'Invalid frontend session credential.',
				),
				403
			);
		}
		return $session;
	}

	private function enforce_public_endpoint_limits( $scope, $session, $subject_limit, $global_limit ) {
		$ip = Niwa_AI_Session_Utils::get_client_ip();
		foreach (
			array(
				array( $scope . '_global', 'global', $global_limit ),
				array( $scope . '_ip', $ip, $subject_limit ),
				array( $scope . '_session', $session, $subject_limit ),
			) as $limit
		) {
			if ( ! Niwa_AI_Session_Utils::consume_fixed_window_limit( $limit[0], $limit[1], max( 1, (int) $limit[2] ), MINUTE_IN_SECONDS ) ) {
				wp_send_json_error( array( 'message' => 'Request limit reached.' ), 429 );
			}
		}
	}

	private function create_coupon_offer_token( $session, $mode ) {
		$payload = array(
			'v'       => 1,
			'session' => (string) $session,
			'mode'    => 'mini' === $mode ? 'mini' : 'cart',
			'iat'     => time(),
			'jti'     => str_replace( '-', '', wp_generate_uuid4() ),
		);
		$body = rtrim( strtr( base64_encode( (string) wp_json_encode( $payload ) ), '+/', '-_' ), '=' );
		return 'v1.' . $body . '.' . hash_hmac( 'sha256', $body, wp_salt( 'nonce' ) . '|niwa-ai-coupon-offer-v1' );
	}

	private function verify_coupon_offer_token( $token, $session, $mode ) {
		$parts = explode( '.', trim( (string) $token ) );
		if ( 3 !== count( $parts ) || 'v1' !== $parts[0] || ! preg_match( '/^[a-f0-9]{64}$/', $parts[2] ) ) {
			return array();
		}
		$expected = hash_hmac( 'sha256', $parts[1], wp_salt( 'nonce' ) . '|niwa-ai-coupon-offer-v1' );
		if ( ! hash_equals( $expected, $parts[2] ) ) {
			return array();
		}
		$body    = strtr( $parts[1], '-_', '+/' );
		$decoded = json_decode( (string) base64_decode( $body . str_repeat( '=', ( 4 - strlen( $body ) % 4 ) % 4 ), true ), true );
		if (
			! is_array( $decoded )
			|| 1 !== (int) ( $decoded['v'] ?? 0 )
			|| ! hash_equals( (string) $session, (string) ( $decoded['session'] ?? '' ) )
			|| ( 'mini' === $mode ? 'mini' : 'cart' ) !== (string) ( $decoded['mode'] ?? '' )
			|| ! preg_match( '/^[a-zA-Z0-9_-]{16,96}$/', (string) ( $decoded['jti'] ?? '' ) )
			|| (int) ( $decoded['iat'] ?? 0 ) < time() - ( 10 * MINUTE_IN_SECONDS )
			|| (int) ( $decoded['iat'] ?? 0 ) > time() + 60
		) {
			return array();
		}
		return $decoded;
	}

	private function claim_coupon_offer( $jti ) {
		$key = 'niwa_ai_cb_offer_claim_' . md5( (string) $jti );
		if ( ! add_option( '_transient_' . $key, time(), '', false ) ) {
			return false;
		}
		add_option( '_transient_timeout_' . $key, time() + ( 15 * MINUTE_IN_SECONDS ), '', false );
		return true;
	}

	private function consume_coupon_abuse_limits( $session, $user_ip, $email ) {
		$woo_session = '';
		if ( function_exists( 'WC' ) && WC() && isset( WC()->session ) && is_object( WC()->session ) && method_exists( WC()->session, 'get_customer_id' ) ) {
			$woo_session = (string) WC()->session->get_customer_id();
		}

		$limits = array(
			array( 'coupon_global_hour', 'global', (int) apply_filters( 'niwa_ai_coupon_global_per_hour', 100 ), HOUR_IN_SECONDS ),
			array( 'coupon_global_day', 'global', (int) apply_filters( 'niwa_ai_coupon_global_per_day', 500 ), DAY_IN_SECONDS ),
			array( 'coupon_ip_day', $user_ip, (int) apply_filters( 'niwa_ai_coupon_ip_per_day', 10 ), DAY_IN_SECONDS ),
			array( 'coupon_session_day', $session, (int) apply_filters( 'niwa_ai_coupon_session_per_day', 3 ), DAY_IN_SECONDS ),
		);
		if ( '' !== $woo_session ) {
			$limits[] = array( 'coupon_woo_day', $woo_session, (int) apply_filters( 'niwa_ai_coupon_woo_session_per_day', 3 ), DAY_IN_SECONDS );
		}
		if ( '' !== $email ) {
			$limits[] = array( 'coupon_email_day', strtolower( $email ), (int) apply_filters( 'niwa_ai_coupon_email_per_day', 2 ), DAY_IN_SECONDS );
		}

		foreach ( $limits as $limit ) {
			if ( ! Niwa_AI_Session_Utils::consume_fixed_window_limit( $limit[0], $limit[1], max( 1, $limit[2] ), $limit[3] ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Parse semantic classifier payload.
	 *
	 * @param string $text Raw model output.
	 * @return array{intent:string,confidence:float}
	 */
	private function parse_offer_response_classifier_payload( $text ) {
		$text = trim( (string) $text );
		if ( '' === $text ) {
			return array(
				'intent'     => 'unclear',
				'confidence' => 0,
			);
		}

		if ( preg_match( '/\{.*\}/s', $text, $matches ) ) {
			$text = (string) $matches[0];
		}

		$decoded = json_decode( $text, true );
		if ( ! is_array( $decoded ) ) {
			return array(
				'intent'     => 'unclear',
				'confidence' => 0,
			);
		}

		$intent = isset( $decoded['intent'] ) ? strtolower( trim( (string) $decoded['intent'] ) ) : 'unclear';
		if ( ! in_array( $intent, array( 'yes', 'no', 'unclear' ), true ) ) {
			$intent = 'unclear';
		}

		$confidence = isset( $decoded['confidence'] ) ? floatval( $decoded['confidence'] ) : 0;
		$confidence = max( 0, min( 1, $confidence ) );

		return array(
			'intent'     => $intent,
			'confidence' => $confidence,
		);
	}

	/**
	 * Render admin page.
	 */
	public function render_page() {
		$opts   = $this->get_opts();
		$labels = $this->get_message_field_labels();
		$ok     = self::OPTION_KEY;

		echo '<div class="wrap niwa-wrap">';

		/* ── Page Header ── */
		echo '<div class="niwa-page-header">';
		echo '<div class="niwa-page-header__icon">';
		echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>';
		echo '</div>';
		echo '<div class="niwa-page-header__text">';
		echo '<h1 class="niwa-page-header__title">' . esc_html__( 'Conversion Busters', 'niwa-ai' ) . '</h1>';
		echo '<p class="niwa-page-header__subtitle">' . esc_html__( 'Dynamic coupon system — engage visitors and boost conversions with time-limited offers.', 'niwa-ai' ) . '</p>';
		echo '</div>';
		echo '</div>';

		/* ── WooCommerce Warning ── */
		if ( ! class_exists( 'WooCommerce' ) ) {
			echo '<div class="niwa-alert niwa-alert--warning">';
			echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
			echo esc_html__( 'WooCommerce is not active. Conversion Busters requires WooCommerce.', 'niwa-ai' );
			echo '</div>';
		}

		echo '<form method="post" action="options.php">';
		settings_fields( 'niwa_ai_cb_group' );

		/* ================================================================
		 * Card 1 — Coupon Configuration
		 * ============================================================= */
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon">';
		echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
		echo '</div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Coupon Configuration', 'niwa-ai' ) . '</h2>';
		echo '</div>';

		echo '<div class="niwa-card__body">';

		// Enable toggle.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $ok ) . '[enabled]" value="1" ' . checked( ! empty( $opts['enabled'] ), true, false ) . ' />';
		echo esc_html__( 'Enable Conversion Busters', 'niwa-ai' );
		echo '</label>';
		echo '</div>';

		// Row 1: Cart Threshold + Discount Amount + Discount Type.
		echo '<div class="niwa-field">';
		echo '<span class="niwa-field__label">' . esc_html__( 'Discount Rules', 'niwa-ai' ) . '</span>';
		echo '<div class="niwa-field__row">';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Cart Threshold', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $ok ) . '[cart_threshold]" value="' . esc_attr( $opts['cart_threshold'] ) . '" min="1" max="20" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Discount Amount', 'niwa-ai' ) . '</label>';
		echo '<input type="number" step="0.01" name="' . esc_attr( $ok ) . '[discount_amount]" value="' . esc_attr( $opts['discount_amount'] ) . '" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Discount Type', 'niwa-ai' ) . '</label>';
		echo '<select name="' . esc_attr( $ok ) . '[discount_type]">';
		echo '<option value="percent" ' . selected( $opts['discount_type'], 'percent', false ) . '>' . esc_html__( 'Percentage', 'niwa-ai' ) . '</option>';
		echo '<option value="fixed_cart" ' . selected( $opts['discount_type'], 'fixed_cart', false ) . '>' . esc_html__( 'Fixed Cart', 'niwa-ai' ) . '</option>';
		echo '<option value="fixed_product" ' . selected( $opts['discount_type'], 'fixed_product', false ) . '>' . esc_html__( 'Fixed Product', 'niwa-ai' ) . '</option>';
		echo '</select>';
		echo '</div>';

		echo '</div>'; // .niwa-field__row
		echo '</div>'; // .niwa-field

		// Row 2: Coupon Duration + Max Uses + Session Cooldown.
		echo '<div class="niwa-field">';
		echo '<span class="niwa-field__label">' . esc_html__( 'Coupon Limits', 'niwa-ai' ) . '</span>';
		echo '<div class="niwa-field__row">';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Duration (min)', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $ok ) . '[coupon_duration]" value="' . esc_attr( $opts['coupon_duration'] ) . '" min="1" max="1440" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Max Uses', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $ok ) . '[max_uses]" value="' . esc_attr( $opts['max_uses'] ) . '" min="1" max="100" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Cooldown (min)', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $ok ) . '[session_cooldown]" value="' . esc_attr( $opts['session_cooldown'] ) . '" min="1" max="120" />';
		echo '</div>';

		echo '</div>'; // .niwa-field__row
		echo '</div>'; // .niwa-field

		// Row 3: Coupon Prefix + Minimum Amount.
		echo '<div class="niwa-field">';
		echo '<span class="niwa-field__label">' . esc_html__( 'Additional', 'niwa-ai' ) . '</span>';
		echo '<div class="niwa-field__row">';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Coupon Prefix', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $ok ) . '[coupon_prefix]" value="' . esc_attr( $opts['coupon_prefix'] ) . '" />';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Minimum Amount', 'niwa-ai' ) . '</label>';
		echo '<input type="number" step="0.01" name="' . esc_attr( $ok ) . '[minimum_amount]" value="' . esc_attr( $opts['minimum_amount'] ) . '" min="0" />';
		echo '</div>';

		echo '</div>'; // .niwa-field__row
		echo '</div>'; // .niwa-field

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card

		/* ================================================================
		 * Card 2 — Messages & Templates
		 * ============================================================= */
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon">';
		echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
		echo '</div>';
		echo '<h2 class="niwa-card__title">' . esc_html( $labels['messages_heading'] ) . '</h2>';
		echo '</div>';

		echo '<div class="niwa-card__body">';

		// Placeholder info box.
		echo '<div class="niwa-alert niwa-alert--info">';
		echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
		echo esc_html__( 'Available placeholders:', 'niwa-ai' ) . ' ';
		echo '<span class="niwa-badge niwa-badge--info">{coupon_code}</span> ';
		echo '<span class="niwa-badge niwa-badge--info">{duration}</span> ';
		echo '<span class="niwa-badge niwa-badge--info">{discount_amount}</span>';
		echo '</div>';

		// Initial Message.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $labels['initial_label'] ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html( $labels['initial_desc'] ) . '</span>';
		echo '<textarea name="' . esc_attr( $ok ) . '[initial_message]" rows="3" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['initial_message'] ?? '' ) . '</textarea>';
		echo '</div>';

		// Confirmation Message.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $labels['confirmation_label'] ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html( $labels['confirmation_desc'] ) . '</span>';
		echo '<textarea name="' . esc_attr( $ok ) . '[confirmation_message]" rows="3" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['confirmation_message'] ?? '' ) . '</textarea>';
		echo '</div>';

		// Clarify Message.
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $labels['clarify_label'] ) . '</label>';
		echo '<span class="niwa-field__hint">' . esc_html( $labels['clarify_desc'] ) . '</span>';
		echo '<textarea name="' . esc_attr( $ok ) . '[clarify_message]" rows="3" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $opts['clarify_message'] ?? '' ) . '</textarea>';
		echo '</div>';

		// Submit button inside card footer.
		echo '<div class="niwa-card__footer">';
		submit_button( __( 'Save Settings', 'niwa-ai' ), 'primary niwa-btn niwa-btn--primary', 'submit', false );
		echo '</div>';

		echo '</div>'; // .niwa-card__body
		echo '</div>'; // .niwa-card

		echo '</form>';
		echo '</div>'; // .wrap.niwa-wrap
	}
}
