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

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

class Niwa_AI_Attention_Capture {

	const OPTION_KEY = Niwa_AI_Constants::OPT_ATTENTION_CAPTURE;
	const ANALYTICS_TABLE = 'niwa_ai_ac_analytics';

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

		// AJAX for frontend config.
		add_action( 'wp_ajax_niwa_ai_ac_get_config', array( $this, 'ajax_get_config' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_ac_get_config', array( $this, 'ajax_get_config' ) );
		add_action( 'wp_ajax_niwa_ai_ac_track_event', array( $this, 'ajax_track_event' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_ac_track_event', array( $this, 'ajax_track_event' ) );
		add_action( 'wp_ajax_niwa_ai_ac_generate_product_objection', array( $this, 'ajax_generate_product_objection' ) );
		add_action( 'wp_ajax_nopriv_niwa_ai_ac_generate_product_objection', array( $this, 'ajax_generate_product_objection' ) );
	}

	/**
	 * Add submenu page.
	 */
	public function add_submenu() {
		add_submenu_page(
			'niwa-ai',
			__( 'Attention Capture', 'niwa-ai' ),
			__( 'Attention Capture', 'niwa-ai' ),
			'manage_options',
			'niwa-ai-attention-capture',
			array( $this, 'render_page' )
		);
	}

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

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

		$bool_fields = array(
			'enabled', 'test_mode', 'welcome_enabled', 'wakeup_enabled',
			'objection_slayer_enabled', 'nudge_os1_enabled', 'nudge_os1_ai_enabled',
			'nudge_os2_enabled', 'nudge_os3_enabled',
			'checkout_incentive_enabled', 'checkout_finish_enabled',
		);

		foreach ( $bool_fields as $field ) {
			$sanitized[ $field ] = ! empty( $input[ $field ] ) ? 1 : 0;
		}

		$int_fields = array(
			'wakeup_timeout', 'nudge_timeout',
			'nudge_os2_timeout', 'nudge_os3_timeout',
			'nudge_os3_button_timeout',
			'checkout_incentive_timeout',
		);

		foreach ( $int_fields as $field ) {
			$sanitized[ $field ] = isset( $input[ $field ] ) ? absint( $input[ $field ] ) : 0;
		}

		$text_fields = array(
			'welcome_custom_message', 'wakeup_custom_message',
			'nudge_sitewide_message',
			'nudge_os2_message', 'nudge_os3_message',
			'nudge_os3_clarify_message',
			'nudge_os3_button_text', 'nudge_os3_button_reply_text',
			'nudge_os3_confirmation_message',
			'checkout_incentive_custom_message', 'checkout_finish_custom_message',
		);

		foreach ( $text_fields as $field ) {
			$sanitized[ $field ] = isset( $input[ $field ] ) ? sanitize_textarea_field( $input[ $field ] ) : '';
		}

		// OS3 (CB Mini) coupon settings — independent from the Conversion Busters module.
		$os3_discount_type           = isset( $input['nudge_os3_discount_type'] ) ? sanitize_text_field( $input['nudge_os3_discount_type'] ) : 'percent';
		$sanitized['nudge_os3_discount_type']  = in_array( $os3_discount_type, array( 'percent', 'fixed_cart' ), true ) ? $os3_discount_type : 'percent';
		$sanitized['nudge_os3_discount_amount'] = isset( $input['nudge_os3_discount_amount'] ) ? max( 0.0, floatval( $input['nudge_os3_discount_amount'] ) ) : 10.0;
		$sanitized['nudge_os3_coupon_duration'] = isset( $input['nudge_os3_coupon_duration'] ) ? max( 1, min( 1440, absint( $input['nudge_os3_coupon_duration'] ) ) ) : 10;
		$sanitized['nudge_os3_coupon_prefix']   = isset( $input['nudge_os3_coupon_prefix'] ) ? sanitize_text_field( $input['nudge_os3_coupon_prefix'] ) : 'NIWA';
		$sanitized['nudge_os3_max_uses']        = isset( $input['nudge_os3_max_uses'] ) ? max( 1, absint( $input['nudge_os3_max_uses'] ) ) : 1;
		$sanitized['nudge_os3_minimum_amount']  = isset( $input['nudge_os3_minimum_amount'] ) ? max( 0.0, floatval( $input['nudge_os3_minimum_amount'] ) ) : 0.0;

		// Conditional rules — unlimited repeater (replaces the old fixed OS4/OS5 fields).
		$sanitized['conditional_rules'] = $this->sanitize_conditional_rules( $input );
		$sanitized['ignore_pages']       = $this->sanitize_ignore_pages( $input );

		return $sanitized;
	}

	/**
	 * Sanitize the dynamic conditional-rule repeater into a normalized list.
	 *
	 * @param array $input Raw settings input.
	 * @return array<int, array<string, mixed>> Normalized rules with stable ids.
	 */
	private function sanitize_conditional_rules( $input ) {
		$raw = isset( $input['conditional_rules'] ) && is_array( $input['conditional_rules'] ) ? $input['conditional_rules'] : array();

		$rules = array();
		$id    = 0;
		foreach ( $raw as $rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}

			$id++;
			$rules[] = array(
				'id'                => $id,
				'enabled'           => ! empty( $rule['enabled'] ) ? 1 : 0,
				'all_products'      => ! empty( $rule['all_products'] ) ? 1 : 0,
				'url'               => isset( $rule['url'] ) ? sanitize_text_field( $rule['url'] ) : '',
				'match_type'        => $this->sanitize_match_type( isset( $rule['match_type'] ) ? $rule['match_type'] : 'contains' ),
				'priority'          => isset( $rule['priority'] ) ? absint( $rule['priority'] ) : ( $id * 10 ),
				'timeout'           => isset( $rule['timeout'] ) ? absint( $rule['timeout'] ) : 30,
				'message'           => isset( $rule['message'] ) ? sanitize_textarea_field( $rule['message'] ) : '',
				'button_text'       => isset( $rule['button_text'] ) ? sanitize_text_field( $rule['button_text'] ) : '',
				'button_reply_text' => isset( $rule['button_reply_text'] ) ? sanitize_text_field( $rule['button_reply_text'] ) : '',
				'button_timeout'    => isset( $rule['button_timeout'] ) ? absint( $rule['button_timeout'] ) : 5,
			);
		}

		return $rules;
	}

	/**
	 * Sanitize the dynamic ignore-pages repeater into a normalized list.
	 *
	 * @param array $input Raw settings input.
	 * @return array<int, array<string, mixed>> Normalized ignore-page rules.
	 */
	private function sanitize_ignore_pages( $input ) {
		$raw = isset( $input['ignore_pages'] ) && is_array( $input['ignore_pages'] ) ? $input['ignore_pages'] : array();

		$pages = array();
		$id    = 0;
		foreach ( $raw as $page ) {
			if ( ! is_array( $page ) ) {
				continue;
			}

			$url = isset( $page['url'] ) ? trim( sanitize_text_field( $page['url'] ) ) : '';
			if ( '' === $url ) {
				continue;
			}

			$id++;
			$pages[] = array(
				'id'         => $id,
				'enabled'    => ! empty( $page['enabled'] ) ? 1 : 0,
				'url'        => $url,
				'match_type' => $this->sanitize_match_type( isset( $page['match_type'] ) ? $page['match_type'] : 'contains' ),
			);
		}

		return $pages;
	}

	/**
	 * Whitelist a URL match type.
	 *
	 * @param string $value Raw value.
	 * @return string One of contains|equals|regex.
	 */
	private function sanitize_match_type( $value ) {
		$value = sanitize_key( (string) $value );
		return in_array( $value, array( 'contains', 'equals', 'regex' ), true ) ? $value : 'contains';
	}

	/**
	 * Get saved options with defaults.
	 *
	 * @return array
	 */
	public function get_opts() {
		$defaults = self::get_default_opts();

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

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

		// One-time migration: fold the legacy fixed OS4/OS5 fields into the new
		// `conditional_rules` repeater, persist, and drop the legacy keys.
		if ( ! array_key_exists( 'conditional_rules', $stored ) ) {
			$migrated_rules = $this->migrate_legacy_conditional_rules( $stored );
			if ( ! empty( $migrated_rules ) ) {
				$opts['conditional_rules'] = $migrated_rules;

				$clean = $opts;
				$clean['conditional_rules'] = $migrated_rules;
				foreach ( array( 4, 5 ) as $level ) {
					foreach ( array( 'enabled', 'all_products', 'url', 'match_type', 'priority', 'timeout', 'message', 'button_text', 'button_reply_text', 'button_timeout' ) as $suffix ) {
						unset( $clean[ 'nudge_os' . $level . '_' . $suffix ] );
					}
				}
				update_option( self::OPTION_KEY, $clean );
			}
		}

		if ( ! is_array( $opts['conditional_rules'] ?? null ) ) {
			$opts['conditional_rules'] = array();
		}
		if ( ! is_array( $opts['ignore_pages'] ?? null ) ) {
			$opts['ignore_pages'] = array();
		}

		return $opts;
	}

	/**
	 * Default settings shape (single source of truth for get_opts + tests).
	 *
	 * @return array
	 */
	public static function get_default_opts() {
		return array(
			'enabled'                           => 0,
			'test_mode'                         => 0,
			'welcome_enabled'                   => 1,
			'welcome_custom_message'            => 'Hi there! Can I help you find something?',
			'wakeup_enabled'                    => 1,
			'wakeup_timeout'                    => 60,
			'wakeup_custom_message'             => 'Still browsing? Let me know if you need help!',
			'objection_slayer_enabled'          => 1,
			'nudge_os1_enabled'                 => 1,
			'nudge_os1_ai_enabled'              => 0,
			'nudge_timeout'                     => 10,
			'nudge_sitewide_message'            => 'Have any questions about this product?',
			'nudge_os2_enabled'                 => 1,
			'nudge_os2_timeout'                 => 22,
			'nudge_os2_message'                 => 'This item is popular! Want to know more?',
			'nudge_os3_enabled'                 => 0,
			'nudge_os3_timeout'                 => 35,
			'nudge_os3_message'                 => '',
			'nudge_os3_clarify_message'         => '',
			'nudge_os3_button_text'             => '',
			'nudge_os3_button_reply_text'       => '',
			'nudge_os3_button_timeout'          => 5,
			// OS3 coupon settings (independent from the Conversion Busters module).
			'nudge_os3_discount_type'           => 'percent',
			'nudge_os3_discount_amount'         => 10,
			'nudge_os3_coupon_duration'         => 10,
			'nudge_os3_coupon_prefix'           => 'NIWA',
			'nudge_os3_max_uses'                => 1,
			'nudge_os3_minimum_amount'          => 0,
			'nudge_os3_confirmation_message'    => '',
			// Conditional rules repeater.
			'conditional_rules'                 => array(),
			// Pages where Attention Capture must stay silent.
			'ignore_pages'                      => array(),
			'checkout_incentive_enabled'        => 1,
			'checkout_incentive_timeout'        => 15,
			'checkout_incentive_custom_message' => 'Great choice! Ready to checkout?',
			'checkout_finish_enabled'           => 1,
			'checkout_finish_custom_message'    => 'Thank you for your order!',
		);
	}

	/**
	 * Build conditional rules from legacy OS4/OS5 flat keys.
	 *
	 * @param array $stored Stored (pre-migration) options.
	 * @return array<int, array<string, mixed>>
	 */
	private function migrate_legacy_conditional_rules( $stored ) {
		$rules = array();
		$id    = 0;

		foreach ( array( 4, 5 ) as $level ) {
			$prefix = 'nudge_os' . $level . '_';
			$has_data = isset( $stored[ $prefix . 'enabled' ] )
				|| ( isset( $stored[ $prefix . 'url' ] ) && '' !== trim( (string) $stored[ $prefix . 'url' ] ) )
				|| ( isset( $stored[ $prefix . 'message' ] ) && '' !== trim( (string) $stored[ $prefix . 'message' ] ) );

			if ( ! $has_data ) {
				continue;
			}

			$id++;
			$rules[] = array(
				'id'                => $id,
				'enabled'           => ! empty( $stored[ $prefix . 'enabled' ] ) ? 1 : 0,
				'all_products'      => ! empty( $stored[ $prefix . 'all_products' ] ) ? 1 : 0,
				'url'               => isset( $stored[ $prefix . 'url' ] ) ? sanitize_text_field( (string) $stored[ $prefix . 'url' ] ) : '',
				'match_type'        => $this->sanitize_match_type( $stored[ $prefix . 'match_type' ] ?? 'contains' ),
				'priority'          => isset( $stored[ $prefix . 'priority' ] ) ? absint( $stored[ $prefix . 'priority' ] ) : ( $id * 10 ),
				'timeout'           => isset( $stored[ $prefix . 'timeout' ] ) ? absint( $stored[ $prefix . 'timeout' ] ) : 30,
				'message'           => isset( $stored[ $prefix . 'message' ] ) ? sanitize_textarea_field( (string) $stored[ $prefix . 'message' ] ) : '',
				'button_text'       => isset( $stored[ $prefix . 'button_text' ] ) ? sanitize_text_field( (string) $stored[ $prefix . 'button_text' ] ) : '',
				'button_reply_text' => isset( $stored[ $prefix . 'button_reply_text' ] ) ? sanitize_text_field( (string) $stored[ $prefix . 'button_reply_text' ] ) : '',
				'button_timeout'    => isset( $stored[ $prefix . 'button_timeout' ] ) ? absint( $stored[ $prefix . 'button_timeout' ] ) : 5,
			);
		}

		return $rules;
	}

	/**
	 * Resolve OS3 (CB Mini) coupon settings, normalized to the keys the
	 * Conversion Busters coupon creator expects. Shared with the CB module so
	 * OS3 can offer a coupon independently of the CB cart-abandonment flow.
	 *
	 * @return array<string, mixed>
	 */
	public static function get_os3_coupon_settings() {
		$stored = get_option( self::OPTION_KEY, array() );
		if ( ! is_array( $stored ) ) {
			$stored = array();
		}
		$defaults = self::get_default_opts();
		$opts     = array_merge( $defaults, $stored );

		$discount_type = isset( $opts['nudge_os3_discount_type'] ) ? (string) $opts['nudge_os3_discount_type'] : 'percent';
		if ( ! in_array( $discount_type, array( 'percent', 'fixed_cart' ), true ) ) {
			$discount_type = 'percent';
		}

		return array(
			'enabled'              => ! empty( $opts['enabled'] ) && ! empty( $opts['objection_slayer_enabled'] ) && ! empty( $opts['nudge_os3_enabled'] ),
			'discount_type'        => $discount_type,
			'discount_amount'      => max( 0.0, floatval( $opts['nudge_os3_discount_amount'] ?? 10 ) ),
			'coupon_duration'      => max( 1, min( 1440, absint( $opts['nudge_os3_coupon_duration'] ?? 10 ) ) ),
			'coupon_prefix'        => sanitize_text_field( (string) ( $opts['nudge_os3_coupon_prefix'] ?? 'NIWA' ) ),
			'max_uses'             => max( 1, absint( $opts['nudge_os3_max_uses'] ?? 1 ) ),
			'minimum_amount'       => max( 0.0, floatval( $opts['nudge_os3_minimum_amount'] ?? 0 ) ),
			'confirmation_message' => isset( $opts['nudge_os3_confirmation_message'] ) ? sanitize_textarea_field( (string) $opts['nudge_os3_confirmation_message'] ) : '',
		);
	}

	/**
	 * Enqueue the conditional-rule repeater script on the AC settings page.
	 *
	 * @param string $hook_suffix Current admin page hook.
	 */
	public function enqueue_admin_assets( $hook_suffix ) {
		if ( false === strpos( (string) $hook_suffix, 'niwa-ai-attention-capture' ) ) {
			return;
		}

		$rel  = 'assets/js/niwa-attention-admin.js';
		$path = NIWA_AI_DIR . $rel;
		$ver  = file_exists( $path ) ? (string) filemtime( $path ) : NIWA_AI_VERSION;

		wp_enqueue_script(
			'niwa-attention-admin',
			NIWA_AI_URL . $rel,
			array(),
			$ver,
			array( 'in_footer' => true )
		);
	}

	/**
	 * AJAX endpoint: return AC config for frontend.
	 */
	public function ajax_get_config() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

		$opts = $this->get_opts();

		wp_send_json_success( $opts );
	}

	/**
	 * AJAX endpoint: track AC lifecycle events.
	 */
	public function ajax_track_event() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );
		if ( isset( $_SERVER['CONTENT_LENGTH'] ) && (int) $_SERVER['CONTENT_LENGTH'] > 32768 ) {
			wp_send_json_error( array( 'message' => 'Analytics payload is too large.' ), 413 );
		}

		$allowed_events = array(
			'bubble_shown',
			'bubble_clicked',
			'action_button_clicked',
			'bubble_dismissed',
			'cb_mini_started',
			'launcher_click',
		);

		$event_type = isset( $_POST['event_type'] ) ? sanitize_text_field( wp_unslash( $_POST['event_type'] ) ) : '';
		if ( ! in_array( $event_type, $allowed_events, true ) ) {
			wp_send_json_error( array( 'message' => 'Invalid event type.' ), 400 );
		}

		$message_type = isset( $_POST['message_type'] ) ? sanitize_text_field( wp_unslash( $_POST['message_type'] ) ) : '';
		$page_url     = isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : home_url( '/' );
		$meta         = isset( $_POST['meta'] ) ? wp_unslash( $_POST['meta'] ) : '';
		$meta_json    = '';

		if ( is_array( $meta ) ) {
			$meta_json = wp_json_encode( $meta );
		} elseif ( is_string( $meta ) ) {
			$meta_json = sanitize_text_field( $meta );
		}

		$session = Niwa_AI_Session_Utils::resolve_user_session();
		$user_ip = Niwa_AI_Session_Utils::get_client_ip();
		foreach (
			array(
				array( 'attention_global_minute', 'global', 2000 ),
				array( 'attention_ip_minute', $user_ip, 120 ),
				array( 'attention_session_minute', $session, 120 ),
			) 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' => 'Analytics request limit reached.' ), 429 );
			}
		}

		$ok = $this->record_event( $event_type, $message_type, $page_url, $session, $user_ip, $meta_json );

		if ( ! $ok ) {
			wp_send_json_error( array( 'message' => 'Failed to store analytics event.' ), 500 );
		}

		wp_send_json_success( array( 'tracked' => true ) );
	}

	/**
	 * Generate the AI-led OS1 message for the current WooCommerce product.
	 */
	public function ajax_generate_product_objection() {
		check_ajax_referer( 'niwa_ai_chat_nonce', 'nonce' );

		$opts = $this->get_opts();
		if ( empty( $opts['enabled'] ) || empty( $opts['objection_slayer_enabled'] ) || empty( $opts['nudge_os1_enabled'] ) || empty( $opts['nudge_os1_ai_enabled'] ) ) {
			wp_send_json_error( array( 'message' => 'AI objection slayer is disabled.' ), 403 );
		}

		$product_id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
		if ( $product_id <= 0 ) {
			wp_send_json_error( array( 'message' => 'Invalid product.' ), 400 );
		}

		if ( ! class_exists( 'Niwa_AI_Attention_Objection_Generator' ) ) {
			wp_send_json_error( array( 'message' => 'AI objection generator is unavailable.' ), 503 );
		}

		$result = ( new Niwa_AI_Attention_Objection_Generator() )->generate_for_product( $product_id );
		if ( empty( $result['success'] ) || empty( $result['message'] ) ) {
			wp_send_json_error( array( 'message' => 'Could not prepare a product-specific message.' ), 502 );
		}

		wp_send_json_success(
			array(
				'message' => $result['message'],
				'cached'  => ! empty( $result['cached'] ),
			)
		);
	}

	/**
	 * Render admin page.
	 */
	public function render_page() {
		$opts = $this->get_opts();
		// Page wrapper
		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"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>';
		echo '</div>';
		echo '<div class="niwa-page-header__text">';
		echo '<h1 class="niwa-page-header__title">' . esc_html__( 'Attention Capture', 'niwa-ai' ) . '</h1>';
		echo '<p class="niwa-page-header__subtitle">' . esc_html__( 'Automated engagement triggers — welcome messages, nudges, objection slayers, and checkout incentives.', 'niwa-ai' ) . '</p>';
		echo '</div>';
		echo '</div>';

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

		// --- Card: General ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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 0 1 0 2.83 2 2 0 0 1-2.83 0l-.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-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-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-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'General', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'enabled', __( 'Enable Attention Capture', 'niwa-ai' ), $opts );
		$this->render_checkbox( 'test_mode', __( 'Test Mode (show multiple times per session)', 'niwa-ai' ), $opts );
		echo '</div>';
		echo '</div>';

		// --- Card: Ignore Pages ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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="10"/><path d="m4.93 4.93 14.14 14.14"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Ignore Pages', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		echo '<p class="niwa-field__hint" style="margin-top:0;">' . esc_html__( 'Add pages where Attention Capture bubbles should never appear. This blocks welcome, wakeup, objection-slayer, custom rule, and checkout bubbles on matching pages.', 'niwa-ai' ) . '</p>';

		$ignore_pages = ( isset( $opts['ignore_pages'] ) && is_array( $opts['ignore_pages'] ) ) ? $opts['ignore_pages'] : array();

		echo '<div class="niwa-repeater" id="niwa-ignore-pages" data-niwa-repeater="ignore-pages">';
		$ignore_index = 0;
		foreach ( $ignore_pages as $page ) {
			$this->render_ignore_page_card( $ignore_index, $page, false );
			$ignore_index++;
		}
		if ( 0 === $ignore_index ) {
			echo '<p class="niwa-repeater__empty">' . esc_html__( 'No ignored pages yet. Click "Add ignored page" to create one.', 'niwa-ai' ) . '</p>';
		}
		echo '</div>';

		echo '<button type="button" class="button niwa-btn niwa-btn--secondary niwa-repeater__add" data-niwa-repeater-add="ignore-pages"><span aria-hidden="true">+</span> ' . esc_html__( 'Add ignored page', 'niwa-ai' ) . '</button>';

		echo '<template id="niwa-ignore-page-template">';
		$this->render_ignore_page_card( '__INDEX__', array(), true );
		echo '</template>';

		echo '</div>';
		echo '</div>';

		// --- Card: Welcome Message ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Welcome Message', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'welcome_enabled', __( 'Enable Welcome Bubble', 'niwa-ai' ), $opts );
		$this->render_textarea( 'welcome_custom_message', __( 'Welcome Message', 'niwa-ai' ), $opts );
		echo '</div>';
		echo '</div>';

		// --- Card: Wakeup Nudge ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Wakeup Nudge', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'wakeup_enabled', __( 'Enable Wakeup Nudge', 'niwa-ai' ), $opts );
		$this->render_number( 'wakeup_timeout', __( 'Inactivity Timeout (seconds)', 'niwa-ai' ), $opts );
		$this->render_textarea( 'wakeup_custom_message', __( 'Wakeup Message', 'niwa-ai' ), $opts );
		echo '</div>';
		echo '</div>';

		// --- Card: Objection Slayers ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Objection Slayers', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'objection_slayer_enabled', __( 'Enable Objection Slayers', 'niwa-ai' ), $opts );
		echo '</div>';

		// --- Collapsible: OS1 — Product Page Nudge ---
		echo '<details class="niwa-collapsible">';
		echo '<summary>' . esc_html__( 'OS1 — Product Page Nudge', 'niwa-ai' ) . '</summary>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'nudge_os1_enabled', __( 'Enable OS1', 'niwa-ai' ), $opts );
		$this->render_checkbox( 'nudge_os1_ai_enabled', __( 'AI-led message for each product', 'niwa-ai' ), $opts, __( 'Niwa reads the current WooCommerce product, its attributes and available options, then addresses the most likely purchase concern in the frontend language selected in UI Settings.', 'niwa-ai' ) );
		$this->render_number( 'nudge_timeout', __( 'Delay (seconds)', 'niwa-ai' ), $opts );
		$this->render_textarea( 'nudge_sitewide_message', __( 'Manual / fallback message', 'niwa-ai' ), $opts, __( 'Shown as written when AI mode is off, or if a product-specific AI message cannot be generated.', 'niwa-ai' ) );
		echo '</div>';
		echo '</details>';

		// --- Collapsible: OS2 — Scarcity Appeal ---
		echo '<details class="niwa-collapsible">';
		echo '<summary>' . esc_html__( 'OS2 — Scarcity Appeal', 'niwa-ai' ) . '</summary>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'nudge_os2_enabled', __( 'Enable OS2', 'niwa-ai' ), $opts );
		$this->render_number( 'nudge_os2_timeout', __( 'Delay (seconds)', 'niwa-ai' ), $opts );
		$this->render_textarea( 'nudge_os2_message', __( 'Message', 'niwa-ai' ), $opts );
		echo '</div>';
		echo '</details>';

		// --- Collapsible: OS3 — CB Mini (discount offer) ---
		echo '<details class="niwa-collapsible">';
		echo '<summary>' . esc_html__( 'OS3 — CB Mini (discount offer)', 'niwa-ai' ) . '</summary>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'nudge_os3_enabled', __( 'Enable OS3', 'niwa-ai' ), $opts, __( 'A bubble that offers a discount code. When the visitor confirms in chat, Niwa generates a WooCommerce coupon using the settings below. Requires WooCommerce.', 'niwa-ai' ) );
		$this->render_number( 'nudge_os3_timeout', __( 'Delay (seconds)', 'niwa-ai' ), $opts, __( 'How long after the page loads before the offer bubble appears.', 'niwa-ai' ) );
		$this->render_textarea( 'nudge_os3_message', __( 'Offer message', 'niwa-ai' ), $opts, __( 'The bubble text that offers the discount, e.g. "Want a discount code for this order?". You can use {product}.', 'niwa-ai' ) );
		$this->render_textarea( 'nudge_os3_clarify_message', __( 'Clarify message (when the reply is unclear)', 'niwa-ai' ), $opts, __( 'If the visitor answers something that is not a clear yes/no, Niwa sends this nudge to ask them to confirm. Leave empty to use the built-in default.', 'niwa-ai' ) );

		// Discount & coupon sub-group.
		echo '<h3 class="niwa-subheading">' . esc_html__( 'Discount & coupon', 'niwa-ai' ) . '</h3>';
		echo '<p class="niwa-field__hint" style="margin-top:0;">' . esc_html__( 'These settings apply to OS3 only and are independent from the Conversion Busters module.', 'niwa-ai' ) . '</p>';
		echo '<div class="niwa-coupon-grid">';
		$this->render_select(
			'nudge_os3_discount_type',
			__( 'Discount type', 'niwa-ai' ),
			$opts,
			array(
				'percent'    => __( 'Percentage (%)', 'niwa-ai' ),
				'fixed_cart' => __( 'Fixed amount', 'niwa-ai' ),
			)
		);
		$this->render_decimal( 'nudge_os3_discount_amount', __( 'Discount amount', 'niwa-ai' ), $opts, __( 'e.g. 10 = 10% or a fixed amount, depending on the type.', 'niwa-ai' ) );
		$this->render_number( 'nudge_os3_coupon_duration', __( 'Valid for (minutes)', 'niwa-ai' ), $opts, __( 'How long the generated coupon stays valid.', 'niwa-ai' ) );
		$this->render_text( 'nudge_os3_coupon_prefix', __( 'Coupon code prefix', 'niwa-ai' ), $opts, __( 'A short prefix added in front of the random code, e.g. NIWA.', 'niwa-ai' ) );
		$this->render_number( 'nudge_os3_max_uses', __( 'Max uses per coupon', 'niwa-ai' ), $opts );
		$this->render_decimal( 'nudge_os3_minimum_amount', __( 'Minimum cart amount', 'niwa-ai' ), $opts, __( '0 = no minimum.', 'niwa-ai' ) );
		echo '</div>';
		$this->render_textarea( 'nudge_os3_confirmation_message', __( 'Confirmation message', 'niwa-ai' ), $opts, __( 'Shown after the coupon is created. Placeholders: {coupon_code}, {duration}, {discount_amount}. Leave empty for the built-in default.', 'niwa-ai' ) );

		// Optional quick-reply button.
		echo '<h3 class="niwa-subheading">' . esc_html__( 'Quick-reply button (optional)', 'niwa-ai' ) . '</h3>';
		$this->render_text( 'nudge_os3_button_text', __( 'Button text', 'niwa-ai' ), $opts, __( 'Leave empty for no button.', 'niwa-ai' ) );
		$this->render_text( 'nudge_os3_button_reply_text', __( 'Button reply sent to chat', 'niwa-ai' ), $opts, __( 'The message sent on the visitor\'s behalf when they click the button (e.g. "Yes, I want the code").', 'niwa-ai' ) );
		$this->render_number( 'nudge_os3_button_timeout', __( 'Button delay (seconds)', 'niwa-ai' ), $opts );
		echo '</div>';
		echo '</details>';

		// --- Collapsible: Conditional Rules (unlimited repeater) ---
		echo '<details class="niwa-collapsible"' . ( ! empty( $opts['conditional_rules'] ) ? ' open' : '' ) . '>';
		echo '<summary>' . esc_html__( 'Conditional Rules — target specific products / pages', 'niwa-ai' ) . '</summary>';
		echo '<div class="niwa-card__body">';
		echo '<p class="niwa-field__hint" style="margin-top:0;">' . esc_html__( 'Each rule shows a chat bubble on the product or page you target, after the delay you set, with the message you write. Add as many rules as you like.', 'niwa-ai' ) . '</p>';

		$rules = ( isset( $opts['conditional_rules'] ) && is_array( $opts['conditional_rules'] ) ) ? $opts['conditional_rules'] : array();

		echo '<div class="niwa-repeater" id="niwa-conditional-rules" data-niwa-repeater="conditional-rules">';
		$rule_index = 0;
		foreach ( $rules as $rule ) {
			$this->render_conditional_rule_card( $rule_index, $rule, false );
			$rule_index++;
		}
		if ( 0 === $rule_index ) {
			echo '<p class="niwa-repeater__empty">' . esc_html__( 'No rules yet. Click "Add rule" to create one.', 'niwa-ai' ) . '</p>';
		}
		echo '</div>';

		echo '<button type="button" class="button niwa-btn niwa-btn--secondary niwa-repeater__add" data-niwa-repeater-add="conditional-rules"><span aria-hidden="true">+</span> ' . esc_html__( 'Add rule', 'niwa-ai' ) . '</button>';

		// Hidden clone source for new rules (template content is not submitted).
		echo '<template id="niwa-conditional-rule-template">';
		$this->render_conditional_rule_card( '__INDEX__', array(), true );
		echo '</template>';

		echo '</div>';
		echo '</details>';

		echo '</div>'; // end Objection Slayers card

		// --- Card: Checkout ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Checkout', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';
		$this->render_checkbox( 'checkout_incentive_enabled', __( 'Enable Checkout Incentive', 'niwa-ai' ), $opts );
		$this->render_number( 'checkout_incentive_timeout', __( 'Delay (seconds)', 'niwa-ai' ), $opts );
		$this->render_textarea( 'checkout_incentive_custom_message', __( 'Incentive Message', 'niwa-ai' ), $opts );
		$this->render_checkbox( 'checkout_finish_enabled', __( 'Enable Order Complete Message', 'niwa-ai' ), $opts );
		$this->render_textarea( 'checkout_finish_custom_message', __( 'Order Complete Message', 'niwa-ai' ), $opts );
		echo '</div>';
		echo '<div class="niwa-card__footer">';
		submit_button( __( 'Save Settings', 'niwa-ai' ), 'primary niwa-btn niwa-btn--primary', 'submit', false );
		echo '</div>';
		echo '</div>';

		echo '</form>';
		echo '</div>'; // end wrap
	}

	/**
	 * Export filtered analytics to CSV.
	 */
	public function handle_export_csv() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Unauthorized', 'niwa-ai' ) );
		}

		check_admin_referer( 'niwa_ai_ac_export_csv', 'niwa_ai_ac_export_nonce' );
		$this->create_analytics_table();

		$filters = $this->get_dashboard_filters( 'post' );
		$rows    = $this->get_export_rows( $filters, 10000 );
		$stamp   = gmdate( 'Ymd_His' );

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename=niwa-ac-analytics-' . $stamp . '.csv' );

		$output = fopen( 'php://output', 'w' );
		if ( false === $output ) {
			wp_die( esc_html__( 'Unable to generate CSV.', 'niwa-ai' ) );
		}

		fputcsv( $output, array( 'created_at', 'event_type', 'message_type', 'page_url', 'page_path', 'user_session', 'user_ip', 'meta' ) );
		foreach ( $rows as $row ) {
			fputcsv(
				$output,
				array(
					$row['created_at'] ?? '',
					$row['event_type'] ?? '',
					$row['message_type'] ?? '',
					$row['page_url'] ?? '',
					$row['page_path'] ?? '',
					$row['user_session'] ?? '',
					$row['user_ip'] ?? '',
					$row['meta'] ?? '',
				)
			);
		}

		fclose( $output );
		exit;
	}

	/**
	 * Read dashboard filters from request.
	 *
	 * @param string $source Request source (`get` or `post`).
	 * @return array
	 */
	private function get_dashboard_filters( $source = 'get' ) {
		$request = ( 'post' === $source ) ? $_POST : $_GET;
		$allowed_days = array( 1, 7, 14, 30, 90 );
		$allowed_events = array_keys( $this->get_event_type_options() );

		$days = isset( $request['ac_days'] ) ? absint( wp_unslash( $request['ac_days'] ) ) : 7;
		if ( ! in_array( $days, $allowed_days, true ) ) {
			$days = 7;
		}

		$event_type = isset( $request['ac_event'] ) ? sanitize_text_field( wp_unslash( $request['ac_event'] ) ) : '';
		if ( ! in_array( $event_type, $allowed_events, true ) ) {
			$event_type = '';
		}

		$page_contains = isset( $request['ac_page'] ) ? sanitize_text_field( wp_unslash( $request['ac_page'] ) ) : '';
		$page_contains = substr( $page_contains, 0, 120 );

		return array(
			'days'          => $days,
			'event_type'    => $event_type,
			'page_contains' => $page_contains,
		);
	}

	/**
	 * Event options for dashboard filter.
	 *
	 * @return array
	 */
	private function get_event_type_options() {
		return array(
			''                      => 'All events',
			'bubble_shown'          => 'Bubble shown',
			'bubble_clicked'        => 'Bubble clicked',
			'action_button_clicked' => 'Action button clicked',
			'bubble_dismissed'      => 'Bubble dismissed',
			'cb_mini_started'       => 'CB Mini started',
			'launcher_click'        => 'Launcher click',
		);
	}

		/**
		 * Create AC 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,
			event_type varchar(40) NOT NULL DEFAULT '',
			message_type varchar(80) NOT NULL DEFAULT '',
			page_url text NULL,
			page_path varchar(255) NOT NULL DEFAULT '',
			user_session varchar(100) NOT NULL DEFAULT '',
			user_ip varchar(45) NOT NULL DEFAULT '',
			meta longtext NULL,
			created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
			PRIMARY KEY (id),
			KEY event_type (event_type),
			KEY message_type (message_type),
			KEY created_at (created_at),
			KEY page_path (page_path(191))
		) {$charset_collate};";

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

	/**
	 * Insert AC analytics row.
	 *
	 * @param string $event_type Event type.
	 * @param string $message_type AC message type.
	 * @param string $page_url URL where event occurred.
	 * @param string $session Session fingerprint.
	 * @param string $user_ip User IP.
	 * @param string $meta_json Optional metadata.
	 * @return bool
	 */
	private function record_event( $event_type, $message_type, $page_url, $session, $user_ip, $meta_json = '' ) {
		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 ) {
			$this->create_analytics_table();
		}

		$page_path = $this->extract_path( $page_url );
		$result    = $wpdb->insert(
			$table_name,
			array(
				'event_type'   => $event_type,
				'message_type' => $message_type,
				'page_url'     => $page_url,
				'page_path'    => $page_path,
				'user_session' => $session,
				'user_ip'      => $user_ip,
				'meta'         => $meta_json,
			),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
		);

		return false !== $result;
	}

	/**
	 * Build SQL WHERE clause for dashboard filters.
	 *
	 * @param array $filters Dashboard filters.
	 * @param array $params  Prepared statement params (returned by reference).
	 * @return string
	 */
	private function build_filters_where_clause( $filters, &$params ) {
		global $wpdb;

		$params  = array();
		$clauses = array( '1=1' );
		$days    = max( 1, absint( $filters['days'] ?? 7 ) );

		$clauses[] = 'created_at >= %s';
		$params[]  = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );

		if ( ! empty( $filters['event_type'] ) ) {
			$clauses[] = 'event_type = %s';
			$params[]  = $filters['event_type'];
		}

		if ( ! empty( $filters['page_contains'] ) ) {
			$clauses[] = 'page_path LIKE %s';
			$params[]  = '%' . $wpdb->esc_like( $filters['page_contains'] ) . '%';
		}

		return implode( ' AND ', $clauses );
	}

	/**
	 * Get analytics summary for dashboard.
	 *
	 * @param array $filters Dashboard filters.
	 * @return array
	 */
	private function get_analytics_summary( $filters ) {
		global $wpdb;

		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;
		$params     = array();
		$where_sql  = $this->build_filters_where_clause( $filters, $params );

		$query = $wpdb->prepare(
			"SELECT
				COUNT(*) AS total_events,
				SUM(CASE WHEN event_type = 'bubble_shown' THEN 1 ELSE 0 END) AS bubble_shown,
				SUM(CASE WHEN event_type = 'bubble_clicked' THEN 1 ELSE 0 END) AS bubble_clicked,
				SUM(CASE WHEN event_type = 'action_button_clicked' THEN 1 ELSE 0 END) AS action_clicked,
				SUM(CASE WHEN event_type = 'cb_mini_started' THEN 1 ELSE 0 END) AS cb_mini_started,
				COUNT(DISTINCT page_path) AS unique_pages
			FROM {$table_name}
			WHERE {$where_sql}",
			$params
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$row = $wpdb->get_row( $query, ARRAY_A );

		$bubble_shown   = isset( $row['bubble_shown'] ) ? (int) $row['bubble_shown'] : 0;
		$bubble_clicked = isset( $row['bubble_clicked'] ) ? (int) $row['bubble_clicked'] : 0;
		$click_rate     = $bubble_shown > 0 ? round( ( $bubble_clicked / $bubble_shown ) * 100, 1 ) : 0.0;

		return array(
			'total_events'    => isset( $row['total_events'] ) ? (int) $row['total_events'] : 0,
			'bubble_shown'    => $bubble_shown,
			'bubble_clicked'  => $bubble_clicked,
			'action_clicked'  => isset( $row['action_clicked'] ) ? (int) $row['action_clicked'] : 0,
			'cb_mini_started' => isset( $row['cb_mini_started'] ) ? (int) $row['cb_mini_started'] : 0,
			'unique_pages'    => isset( $row['unique_pages'] ) ? (int) $row['unique_pages'] : 0,
			'click_rate'      => $click_rate,
		);
	}

	/**
	 * Get top trigger points by page path.
	 *
	 * @param int   $limit Max rows.
	 * @param array $filters Dashboard filters.
	 * @return array
	 */
	private function get_top_trigger_pages( $limit, $filters ) {
		global $wpdb;

		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;
		$limit      = max( 1, absint( $limit ) );
		$params     = array();
		$where_sql  = $this->build_filters_where_clause( $filters, $params );
		$params[]   = $limit;

		$query = $wpdb->prepare(
			"SELECT
				page_path,
				COUNT(*) AS total,
				SUM(CASE WHEN event_type = 'bubble_shown' THEN 1 ELSE 0 END) AS shown,
				SUM(CASE WHEN event_type = 'bubble_clicked' THEN 1 ELSE 0 END) AS clicked
			FROM {$table_name}
			WHERE {$where_sql}
			GROUP BY page_path
			ORDER BY total DESC
			LIMIT %d",
			$params
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_results( $query, ARRAY_A );
	}

	/**
	 * Get most recent AC events.
	 *
	 * @param int   $limit Max rows.
	 * @param array $filters Dashboard filters.
	 * @return array
	 */
	private function get_recent_events( $limit, $filters ) {
		global $wpdb;

		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;
		$limit      = max( 1, absint( $limit ) );
		$params     = array();
		$where_sql  = $this->build_filters_where_clause( $filters, $params );
		$params[]   = $limit;

		$query = $wpdb->prepare(
			"SELECT event_type, message_type, page_path, created_at
			FROM {$table_name}
			WHERE {$where_sql}
			ORDER BY created_at DESC
			LIMIT %d",
			$params
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_results( $query, ARRAY_A );
	}

	/**
	 * Get analytics rows for CSV export.
	 *
	 * @param array $filters Dashboard filters.
	 * @param int   $limit Max rows.
	 * @return array
	 */
	private function get_export_rows( $filters, $limit = 10000 ) {
		global $wpdb;

		$table_name = $wpdb->prefix . self::ANALYTICS_TABLE;
		$limit      = max( 1, min( 50000, absint( $limit ) ) );
		$params     = array();
		$where_sql  = $this->build_filters_where_clause( $filters, $params );
		$params[]   = $limit;

		$query = $wpdb->prepare(
			"SELECT created_at, event_type, message_type, page_url, page_path, user_session, user_ip, meta
			FROM {$table_name}
			WHERE {$where_sql}
			ORDER BY created_at DESC
			LIMIT %d",
			$params
		);

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		return $wpdb->get_results( $query, ARRAY_A );
	}

	/**
	 * Render analytics dashboard.
	 *
	 * @param array $analytics Summary data.
	 * @param array $top_pages Top pages.
	 * @param array $recent Recent events.
	 * @param array $filters Dashboard filters.
	 */
	private function render_dashboard( $analytics, $top_pages, $recent, $filters ) {
		$event_options = $this->get_event_type_options();
		$reset_url     = admin_url( 'admin.php?page=niwa-ai-attention-capture' );
		$export_url    = admin_url( 'admin-post.php' );

		// --- Analytics Card ---
		echo '<div class="niwa-card">';
		echo '<div class="niwa-card__header">';
		echo '<div class="niwa-card__icon"><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"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></div>';
		echo '<h2 class="niwa-card__title">' . esc_html__( 'Analytics Dashboard', 'niwa-ai' ) . '</h2>';
		echo '</div>';
		echo '<div class="niwa-card__body">';

		// Filter toolbar
		echo '<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;margin-bottom:20px;">';

		echo '<form method="get" style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end;">';
		echo '<input type="hidden" name="page" value="niwa-ai-attention-capture" />';

		echo '<div class="niwa-field" style="margin-bottom:0;">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Period', 'niwa-ai' ) . '</label>';
		echo '<select name="ac_days">';
		foreach ( array( 1, 7, 14, 30, 90 ) as $days ) {
			$label = sprintf( 'Last %d day%s', $days, 1 === $days ? '' : 's' );
			echo '<option value="' . esc_attr( (string) $days ) . '" ' . selected( (int) $filters['days'], $days, false ) . '>' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
		echo '</div>';

		echo '<div class="niwa-field" style="margin-bottom:0;">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Event', 'niwa-ai' ) . '</label>';
		echo '<select name="ac_event">';
		foreach ( $event_options as $key => $label ) {
			echo '<option value="' . esc_attr( $key ) . '" ' . selected( $filters['event_type'], $key, false ) . '>' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
		echo '</div>';

		echo '<div class="niwa-field" style="margin-bottom:0;">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Page contains', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="ac_page" value="' . esc_attr( $filters['page_contains'] ) . '" placeholder="/product/..." class="niwa-input--wide" />';
		echo '</div>';

		echo '<div style="display:flex;gap:8px;align-items:flex-end;padding-bottom:2px;">';
		submit_button( __( 'Apply Filters', 'niwa-ai' ), 'primary niwa-btn niwa-btn--primary', '', false );
		echo ' <a class="button niwa-btn niwa-btn--secondary" href="' . esc_url( $reset_url ) . '">' . esc_html__( 'Reset', 'niwa-ai' ) . '</a>';
		echo '</div>';
		echo '</form>';

		echo '<form method="post" action="' . esc_url( $export_url ) . '" style="margin-left:auto;display:flex;align-items:flex-end;padding-bottom:2px;">';
		wp_nonce_field( 'niwa_ai_ac_export_csv', 'niwa_ai_ac_export_nonce' );
		echo '<input type="hidden" name="action" value="niwa_ai_ac_export_csv" />';
		echo '<input type="hidden" name="ac_days" value="' . esc_attr( (string) $filters['days'] ) . '" />';
		echo '<input type="hidden" name="ac_event" value="' . esc_attr( $filters['event_type'] ) . '" />';
		echo '<input type="hidden" name="ac_page" value="' . esc_attr( $filters['page_contains'] ) . '" />';
		submit_button( __( 'Export CSV', 'niwa-ai' ), 'secondary niwa-btn niwa-btn--secondary', '', false );
		echo '</form>';

		echo '</div>'; // end filter toolbar flex

		// Stat cards
		echo '<div class="niwa-stats">';
		$this->render_stat_card( __( 'Total Events', 'niwa-ai' ), (string) ( $analytics['total_events'] ?? 0 ) );
		$this->render_stat_card( __( 'Bubbles Shown', 'niwa-ai' ), (string) ( $analytics['bubble_shown'] ?? 0 ) );
		$this->render_stat_card( __( 'Bubbles Clicked', 'niwa-ai' ), (string) ( $analytics['bubble_clicked'] ?? 0 ) );
		$this->render_stat_card( __( 'Action Button Clicks', 'niwa-ai' ), (string) ( $analytics['action_clicked'] ?? 0 ) );
		$this->render_stat_card( __( 'CB Mini Starts', 'niwa-ai' ), (string) ( $analytics['cb_mini_started'] ?? 0 ) );
		$this->render_stat_card( __( 'Click Rate', 'niwa-ai' ), (string) ( $analytics['click_rate'] ?? 0 ) . '%' );
		echo '</div>';

		// Top Trigger Points table
		echo '<h3 style="margin:20px 0 8px;">' . esc_html__( 'Top Trigger Points (Page Paths)', 'niwa-ai' ) . '</h3>';
		if ( empty( $top_pages ) ) {
			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" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><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__( 'No trigger data for selected filters.', 'niwa-ai' );
			echo '</div>';
		} else {
			echo '<table class="niwa-table">';
			echo '<thead><tr>';
			echo '<th>' . esc_html__( 'Page Path', 'niwa-ai' ) . '</th>';
			echo '<th>' . esc_html__( 'Total', 'niwa-ai' ) . '</th>';
			echo '<th>' . esc_html__( 'Shown', 'niwa-ai' ) . '</th>';
			echo '<th>' . esc_html__( 'Clicked', 'niwa-ai' ) . '</th>';
			echo '</tr></thead><tbody>';
			foreach ( $top_pages as $row ) {
				echo '<tr>';
				echo '<td>' . esc_html( ! empty( $row['page_path'] ) ? $row['page_path'] : '/' ) . '</td>';
				echo '<td>' . esc_html( (string) ( $row['total'] ?? 0 ) ) . '</td>';
				echo '<td>' . esc_html( (string) ( $row['shown'] ?? 0 ) ) . '</td>';
				echo '<td>' . esc_html( (string) ( $row['clicked'] ?? 0 ) ) . '</td>';
				echo '</tr>';
			}
			echo '</tbody></table>';
		}

		// Recent Events table
		echo '<h3 style="margin:20px 0 8px;">' . esc_html__( 'Recent Events', 'niwa-ai' ) . '</h3>';
		if ( empty( $recent ) ) {
			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" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><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__( 'No recent events for selected filters.', 'niwa-ai' );
			echo '</div>';
		} else {
			echo '<table class="niwa-table">';
			echo '<thead><tr>';
			echo '<th>' . esc_html__( 'Time', 'niwa-ai' ) . '</th>';
			echo '<th>' . esc_html__( 'Event', 'niwa-ai' ) . '</th>';
			echo '<th>' . esc_html__( 'Message Type', 'niwa-ai' ) . '</th>';
			echo '<th>' . esc_html__( 'Page Path', 'niwa-ai' ) . '</th>';
			echo '</tr></thead><tbody>';
			foreach ( $recent as $row ) {
				echo '<tr>';
				echo '<td>' . esc_html( isset( $row['created_at'] ) ? (string) $row['created_at'] : '' ) . '</td>';
				echo '<td>' . esc_html( isset( $row['event_type'] ) ? (string) $row['event_type'] : '' ) . '</td>';
				echo '<td>' . esc_html( isset( $row['message_type'] ) ? (string) $row['message_type'] : '' ) . '</td>';
				echo '<td>' . esc_html( ! empty( $row['page_path'] ) ? (string) $row['page_path'] : '/' ) . '</td>';
				echo '</tr>';
			}
			echo '</tbody></table>';
		}

		echo '</div>'; // end niwa-card__body
		echo '</div>'; // end niwa-card
	}

	/**
	 * Render one analytics stat card.
	 *
	 * @param string $label Label.
	 * @param string $value Value.
	 */
	private function render_stat_card( $label, $value ) {
		echo '<div class="niwa-stat">';
		echo '<div class="niwa-stat__label">' . esc_html( $label ) . '</div>';
		echo '<div class="niwa-stat__value">' . esc_html( $value ) . '</div>';
		echo '</div>';
	}

	// --- Helper render methods ---

	private function render_hint( $hint ) {
		if ( '' === (string) $hint ) {
			return;
		}
		echo '<span class="niwa-field__hint">' . esc_html( $hint ) . '</span>';
	}

	private function render_checkbox( $key, $label, $opts, $hint = '' ) {
		$name    = self::OPTION_KEY . '[' . $key . ']';
		$checked = ! empty( $opts[ $key ] ) ? 'checked' : '';
		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $name ) . '" value="1" ' . $checked . ' />';
		echo esc_html( $label );
		echo '</label>';
		$this->render_hint( $hint );
		echo '</div>';
	}

	private function render_text( $key, $label, $opts, $hint = '' ) {
		$name  = self::OPTION_KEY . '[' . $key . ']';
		$value = isset( $opts[ $key ] ) ? $opts[ $key ] : '';
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $label ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" class="niwa-input--wide" />';
		$this->render_hint( $hint );
		echo '</div>';
	}

	private function render_select( $key, $label, $opts, $choices, $hint = '' ) {
		$name  = self::OPTION_KEY . '[' . $key . ']';
		$value = isset( $opts[ $key ] ) ? (string) $opts[ $key ] : '';
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $label ) . '</label>';
		echo '<select name="' . esc_attr( $name ) . '">';
		foreach ( $choices as $choice_value => $choice_label ) {
			echo '<option value="' . esc_attr( $choice_value ) . '" ' . selected( $value, (string) $choice_value, false ) . '>' . esc_html( $choice_label ) . '</option>';
		}
		echo '</select>';
		$this->render_hint( $hint );
		echo '</div>';
	}

	private function render_textarea( $key, $label, $opts, $hint = '' ) {
		$name  = self::OPTION_KEY . '[' . $key . ']';
		$value = isset( $opts[ $key ] ) ? $opts[ $key ] : '';
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $label ) . '</label>';
		echo '<textarea name="' . esc_attr( $name ) . '" rows="3" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $value ) . '</textarea>';
		$this->render_hint( $hint );
		echo '</div>';
	}

	private function render_number( $key, $label, $opts, $hint = '' ) {
		$name  = self::OPTION_KEY . '[' . $key . ']';
		$value = isset( $opts[ $key ] ) ? (int) $opts[ $key ] : 0;
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $label ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" min="0" />';
		$this->render_hint( $hint );
		echo '</div>';
	}

	private function render_decimal( $key, $label, $opts, $hint = '' ) {
		$name  = self::OPTION_KEY . '[' . $key . ']';
		$value = isset( $opts[ $key ] ) ? (float) $opts[ $key ] : 0;
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $label ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $name ) . '" value="' . esc_attr( (string) $value ) . '" min="0" step="0.01" />';
		$this->render_hint( $hint );
		echo '</div>';
	}

	private function render_url( $key, $label, $opts, $hint = '' ) {
		$name  = self::OPTION_KEY . '[' . $key . ']';
		$value = isset( $opts[ $key ] ) ? $opts[ $key ] : '';
		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html( $label ) . '</label>';
		echo '<input type="url" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" class="niwa-input--wide" placeholder="https://..." />';
		$this->render_hint( $hint );
		echo '</div>';
	}

	/**
	 * Render a single conditional-rule card for the repeater.
	 *
	 * Uses array-notation field names so an unlimited number of rules round-trip
	 * through `sanitize_conditional_rules()`. Pass `$is_template = true` to emit
	 * the JS clone source with an `__INDEX__` placeholder (and no live values).
	 *
	 * @param int|string $index Numeric index, or `__INDEX__` for the template.
	 * @param array      $rule  Rule data.
	 * @param bool       $is_template Whether this is the hidden clone source.
	 */
	private function render_conditional_rule_card( $index, $rule, $is_template = false ) {
		$base = self::OPTION_KEY . '[conditional_rules][' . $index . ']';

		$rule = wp_parse_args(
			is_array( $rule ) ? $rule : array(),
			array(
				'enabled'           => 1,
				'all_products'      => 0,
				'url'               => '',
				'match_type'        => 'contains',
				'priority'          => 10,
				'timeout'           => 30,
				'message'           => '',
				'button_text'       => '',
				'button_reply_text' => '',
				'button_timeout'    => 5,
			)
		);

		$en_checked = ! empty( $rule['enabled'] ) ? 'checked' : '';
		$ap_checked = ! empty( $rule['all_products'] ) ? 'checked' : '';
		$mt         = in_array( $rule['match_type'], array( 'contains', 'equals', 'regex' ), true ) ? $rule['match_type'] : 'contains';
		$match_opts = array(
			'contains' => __( 'Contains — page address contains this text', 'niwa-ai' ),
			'equals'   => __( 'Equals — address must match exactly', 'niwa-ai' ),
			'regex'    => __( 'Regex — advanced pattern', 'niwa-ai' ),
		);

		echo '<div class="niwa-rule"' . ( $is_template ? ' data-niwa-rule-template="1"' : '' ) . '>';

		echo '<div class="niwa-rule__header">';
		echo '<span class="niwa-rule__title">' . esc_html__( 'Rule', 'niwa-ai' ) . '</span>';
		echo '<button type="button" class="niwa-rule__remove" aria-label="' . esc_attr__( 'Remove rule', 'niwa-ai' ) . '">';
		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" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
		echo '<span>' . esc_html__( 'Remove', 'niwa-ai' ) . '</span>';
		echo '</button>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $base . '[enabled]' ) . '" value="1" ' . $en_checked . ' />';
		echo esc_html__( 'Enable this rule', 'niwa-ai' );
		echo '</label>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" class="niwa-rule__all-products" name="' . esc_attr( $base . '[all_products]' ) . '" value="1" ' . $ap_checked . ' />';
		echo esc_html__( 'Show on all product pages (ignore the link below)', 'niwa-ai' );
		echo '</label>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Product link / page address', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $base . '[url]' ) . '" value="' . esc_attr( $rule['url'] ) . '" class="niwa-input--wide" placeholder="/product/my-product or https://..." />';
		$this->render_hint( __( 'Paste the product link (or part of it). The bubble shows only on pages whose address matches.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Delay (seconds)', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $base . '[timeout]' ) . '" value="' . esc_attr( (string) (int) $rule['timeout'] ) . '" min="0" />';
		$this->render_hint( __( 'How long after the visitor lands on the page before the bubble appears.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Message', 'niwa-ai' ) . '</label>';
		echo '<textarea name="' . esc_attr( $base . '[message]' ) . '" rows="3" class="niwa-input--wide" style="max-width:100%;">' . esc_textarea( $rule['message'] ) . '</textarea>';
		$this->render_hint( __( 'What the bubble says. You can use {product} to insert the product name.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Quick-reply button text (optional)', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $base . '[button_text]' ) . '" value="' . esc_attr( $rule['button_text'] ) . '" class="niwa-input--wide" />';
		$this->render_hint( __( 'Leave empty for no button. The button appears under the message after the delay below.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Button reply sent to chat (optional)', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $base . '[button_reply_text]' ) . '" value="' . esc_attr( $rule['button_reply_text'] ) . '" class="niwa-input--wide" />';
		$this->render_hint( __( 'When the visitor clicks the button, this message is sent into the chat on their behalf.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Button delay (seconds)', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $base . '[button_timeout]' ) . '" value="' . esc_attr( (string) (int) $rule['button_timeout'] ) . '" min="0" />';
		echo '</div>';

		echo '<details class="niwa-rule__advanced">';
		echo '<summary>' . esc_html__( 'Advanced', 'niwa-ai' ) . '</summary>';
		echo '<div class="niwa-rule__advanced-body">';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Match type', 'niwa-ai' ) . '</label>';
		echo '<select name="' . esc_attr( $base . '[match_type]' ) . '">';
		foreach ( $match_opts as $mv => $ml ) {
			echo '<option value="' . esc_attr( $mv ) . '" ' . selected( $mt, $mv, false ) . '>' . esc_html( $ml ) . '</option>';
		}
		echo '</select>';
		$this->render_hint( __( 'How the link above is compared to the visitor\'s current page address.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Priority (lower wins)', 'niwa-ai' ) . '</label>';
		echo '<input type="number" name="' . esc_attr( $base . '[priority]' ) . '" value="' . esc_attr( (string) (int) $rule['priority'] ) . '" min="0" />';
		$this->render_hint( __( 'If several link rules match the same page, only the one with the lowest number runs.', 'niwa-ai' ) );
		echo '</div>';

		echo '</div>'; // advanced-body
		echo '</details>';

		echo '</div>'; // niwa-rule
	}

	/**
	 * Render a single ignore-page card for the repeater.
	 *
	 * @param int|string $index Numeric index, or `__INDEX__` for the template.
	 * @param array      $page  Ignore-page data.
	 * @param bool       $is_template Whether this is the hidden clone source.
	 */
	private function render_ignore_page_card( $index, $page, $is_template = false ) {
		$base = self::OPTION_KEY . '[ignore_pages][' . $index . ']';

		$page = wp_parse_args(
			is_array( $page ) ? $page : array(),
			array(
				'enabled'    => 1,
				'url'        => '',
				'match_type' => 'contains',
			)
		);

		$enabled_checked = ! empty( $page['enabled'] ) ? 'checked' : '';
		$match_type      = in_array( $page['match_type'], array( 'contains', 'equals', 'regex' ), true ) ? $page['match_type'] : 'contains';
		$match_opts      = array(
			'contains' => __( 'Contains - page address contains this text', 'niwa-ai' ),
			'equals'   => __( 'Equals - address or path must match exactly', 'niwa-ai' ),
			'regex'    => __( 'Regex - advanced pattern', 'niwa-ai' ),
		);

		echo '<div class="niwa-rule niwa-ignore-page"' . ( $is_template ? ' data-niwa-rule-template="1"' : '' ) . '>';

		echo '<div class="niwa-rule__header">';
		echo '<span class="niwa-rule__title">' . esc_html__( 'Ignored page', 'niwa-ai' ) . '</span>';
		echo '<button type="button" class="niwa-rule__remove" aria-label="' . esc_attr__( 'Remove ignored page', 'niwa-ai' ) . '">';
		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" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
		echo '<span>' . esc_html__( 'Remove', 'niwa-ai' ) . '</span>';
		echo '</button>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-toggle">';
		echo '<input type="checkbox" name="' . esc_attr( $base . '[enabled]' ) . '" value="1" ' . $enabled_checked . ' />';
		echo esc_html__( 'Enable this ignored page', 'niwa-ai' );
		echo '</label>';
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Page address', 'niwa-ai' ) . '</label>';
		echo '<input type="text" name="' . esc_attr( $base . '[url]' ) . '" value="' . esc_attr( $page['url'] ) . '" class="niwa-input--wide" placeholder="/cart or https://..." />';
		$this->render_hint( __( 'Paste the page link or part of it. When the visitor is on a matching page, no Attention Capture bubble will be shown.', 'niwa-ai' ) );
		echo '</div>';

		echo '<div class="niwa-field">';
		echo '<label class="niwa-field__label">' . esc_html__( 'Match type', 'niwa-ai' ) . '</label>';
		echo '<select name="' . esc_attr( $base . '[match_type]' ) . '">';
		foreach ( $match_opts as $value => $label ) {
			echo '<option value="' . esc_attr( $value ) . '" ' . selected( $match_type, $value, false ) . '>' . esc_html( $label ) . '</option>';
		}
		echo '</select>';
		$this->render_hint( __( 'How the page address above is compared to the visitor\'s current page address.', 'niwa-ai' ) );
		echo '</div>';

		echo '</div>';
	}

	/**
	 * Extract normalized path from URL.
	 *
	 * @param string $page_url Full URL.
	 * @return string
	 */
	private function extract_path( $page_url ) {
		$parts = wp_parse_url( $page_url );
		$path  = isset( $parts['path'] ) ? $parts['path'] : '/';
		if ( isset( $parts['query'] ) && '' !== $parts['query'] ) {
			$path .= '?' . $parts['query'];
		}
		return substr( (string) $path, 0, 255 );
	}
}
