<?php
/**
 * Asset Loader
 *
 * Handles loading of frontend assets (CSS, JS) for the chat widget.
 *
 * @package Niwa_AI
 * @since   0.2.0
 */

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

class Niwa_AI_Asset_Loader {

	const MOBILE_FLYOUT_WIDTH_DEFAULT = 95;
	const MOBILE_FLYOUT_HEIGHT_DEFAULT = 95;

	/**
	 * Plugin version.
	 *
	 * @var string
	 */
	private $version;

	/**
	 * Cached result of bundles_available() (null = not yet resolved).
	 *
	 * @var bool|null
	 */
	private $use_bundles_cache = null;

	/**
	 * Constructor.
	 *
	 * @param string $version Plugin version.
	 */
	public function __construct( $version ) {
		$this->version = $version;
	}

	/**
	 * Enqueue frontend assets.
	 */
	public function enqueue() {
		if ( ! $this->should_load_assets() ) {
			return;
		}

		Niwa_AI_Session_Utils::resolve_verified_session( true );

		$opts = $this->get_merged_opts();

		$this->enqueue_styles();
		$this->enqueue_scripts( $opts );
		$this->localize_scripts( $opts );
	}

	/**
	 * Whether minified+concatenated frontend bundles are available.
	 *
	 * Bundles are generated by `npm run build:assets` (build/build-frontend.mjs)
	 * into assets/dist/. When the full set is present, the frontend serves a
	 * single core JS + single core CSS file (fewer requests, smaller parse) and
	 * minified conditional modules. When any bundle is missing — or the
	 * `NIWA_AI_DISABLE_BUNDLES` constant is set — the loader falls back to the
	 * individual source modules. The bundles are behaviorally identical to the
	 * sources (ordered concat + minify only; no logic or animation changes).
	 *
	 * @return bool
	 */
	private function bundles_available() {
		if ( null !== $this->use_bundles_cache ) {
			return $this->use_bundles_cache;
		}

		if ( defined( 'NIWA_AI_DISABLE_BUNDLES' ) && NIWA_AI_DISABLE_BUNDLES ) {
			$this->use_bundles_cache = false;
			return false;
		}

		$dist     = NIWA_AI_DIR . 'assets/dist/';
		$required = array(
			'niwa-frontend.min.js',
			'niwa-frontend.min.css',
			'niwa-frontend-shield.min.css',
			'niwa-lead-form.min.js',
			'niwa-scheduler.min.js',
			'niwa-lead-form.min.css',
			'conversion-busters.min.js',
			'attention-capture.min.js',
			'leave-stoper.min.js',
		);

		foreach ( $required as $file ) {
			if ( ! file_exists( $dist . $file ) ) {
				$this->use_bundles_cache = false;
				return false;
			}
		}

		$this->use_bundles_cache = true;
		return true;
	}

	/**
	 * Absolute URL for a built bundle in assets/dist/.
	 *
	 * @param string $file Bundle file name.
	 * @return string
	 */
	private function dist_url( $file ) {
		return NIWA_AI_URL . 'assets/dist/' . $file;
	}

	/**
	 * Cache-busting version for a built bundle in assets/dist/.
	 *
	 * @param string $file Bundle file name.
	 * @return string
	 */
	private function dist_ver( $file ) {
		return $this->get_file_version( NIWA_AI_DIR . 'assets/dist/' . $file );
	}

	/**
	 * Handle that carries the localized NiwaAI config (and is the dependency
	 * root for conditional frontend modules). Switches to the bundle handle
	 * when bundles are active so wp_localize_script targets a real handle.
	 *
	 * @return string
	 */
	private function core_script_handle() {
		return $this->bundles_available() ? 'niwa-frontend' : 'niwa-utils';
	}

	/**
	 * Enqueue the minified core JS bundle + conditional minified modules.
	 *
	 * Mirrors the conditional gating of the legacy per-module path in
	 * {@see enqueue_scripts()} — same options, same config builders, same
	 * WooCommerce/OS3 rules — but each module is the minified build artifact
	 * and conditional modules depend on the single `niwa-frontend` core bundle
	 * (which already contains cb-shim + activity in the original order).
	 *
	 * @param array $opts   Plugin options.
	 * @param array $footer Script registration args.
	 */
	private function enqueue_script_bundles( $opts, $footer ) {
		wp_enqueue_script(
			'niwa-frontend',
			$this->dist_url( 'niwa-frontend.min.js' ),
			array(),
			$this->dist_ver( 'niwa-frontend.min.js' ),
			$footer
		);

		// Lead Form Generator (conditional) — engine + chat-mode + card-mode.
		$lf_opts_js        = class_exists( 'Niwa_AI_Lead_Form_Schema' ) ? Niwa_AI_Lead_Form_Schema::get() : array();
		$scheduler_config  = $this->get_scheduler_frontend_config();
		$scheduler_enabled = ! empty( $scheduler_config['enabled'] ) && ! empty( $scheduler_config['services'] );
		if ( ! empty( $lf_opts_js['enabled'] ) ) {
			wp_enqueue_script(
				'niwa-lead-form',
				$this->dist_url( 'niwa-lead-form.min.js' ),
				array( 'niwa-frontend' ),
				$this->dist_ver( 'niwa-lead-form.min.js' ),
				$footer
			);
			wp_localize_script(
				'niwa-lead-form',
				'NiwaLF',
				$this->build_lead_form_frontend_config( $lf_opts_js )
			);
		} elseif ( $scheduler_enabled ) {
			wp_enqueue_script(
				'niwa-scheduler',
				$this->dist_url( 'niwa-scheduler.min.js' ),
				array( 'niwa-frontend' ),
				$this->dist_ver( 'niwa-scheduler.min.js' ),
				$footer
			);
			wp_localize_script( 'niwa-scheduler', 'NiwaScheduler', $scheduler_config );
		}

		// Leave Stoper (conditional).
		if ( ! empty( $opts['leave_stoper_enabled'] ) ) {
			wp_enqueue_script(
				'niwa-leave-stoper',
				$this->dist_url( 'leave-stoper.min.js' ),
				array( 'niwa-frontend' ),
				$this->dist_ver( 'leave-stoper.min.js' ),
				$footer
			);
		}

		// Attention Capture (conditional).
		$ac_opts = get_option( Niwa_AI_Constants::OPT_ATTENTION_CAPTURE, array() );
		if ( ! empty( $ac_opts['enabled'] ) ) {
			wp_enqueue_script(
				'niwa-attention-capture',
				$this->dist_url( 'attention-capture.min.js' ),
				array( 'niwa-frontend' ),
				$this->dist_ver( 'attention-capture.min.js' ),
				$footer
			);
			wp_localize_script( 'niwa-attention-capture', 'NiwaAC', $this->build_ac_config( $ac_opts, $opts ) );
		}

		// Conversion Busters (conditional) — identical gating to legacy path.
		$cb_opts      = get_option( Niwa_AI_Constants::OPT_CONVERSION_BUSTERS, array() );
		$os3_needs_cb = ! empty( $ac_opts['enabled'] )
			&& ! empty( $ac_opts['objection_slayer_enabled'] )
			&& ! empty( $ac_opts['nudge_os3_enabled'] );
		if ( ( ! empty( $cb_opts['enabled'] ) || $os3_needs_cb ) && class_exists( 'WooCommerce' ) ) {
			wp_enqueue_script(
				'niwa-conversion-busters',
				$this->dist_url( 'conversion-busters.min.js' ),
				array( 'niwa-frontend' ),
				$this->dist_ver( 'conversion-busters.min.js' ),
				$footer
			);
			wp_localize_script( 'niwa-conversion-busters', 'NiwaCB', $this->build_cb_config( $cb_opts ) );
		}
	}

	/**
	 * Enqueue the minified core CSS bundle + conditional minified stylesheets.
	 *
	 * Preserves cascade order: core bundle (tokens … attention) first, optional
	 * lead-form next, frontend-shield last. Inline responsive-launcher + Woo
	 * thumbnail-ratio CSS attach to the core bundle handle.
	 */
	private function enqueue_style_bundles() {
		wp_enqueue_style(
			'niwa-frontend',
			$this->dist_url( 'niwa-frontend.min.css' ),
			array(),
			$this->dist_ver( 'niwa-frontend.min.css' )
		);

		$responsive_css = $this->generate_responsive_launcher_css();
		if ( $responsive_css ) {
			wp_add_inline_style( 'niwa-frontend', $responsive_css );
		}

		$ratio_css = $this->get_woo_thumbnail_ratio_css();
		if ( $ratio_css ) {
			wp_add_inline_style( 'niwa-frontend', $ratio_css );
		}

		$lf_opts_css = class_exists( 'Niwa_AI_Lead_Form_Schema' ) ? Niwa_AI_Lead_Form_Schema::get() : array();
		$lf_enabled  = ! empty( $lf_opts_css['enabled'] );
		$scheduler_config  = $this->get_scheduler_frontend_config();
		$scheduler_enabled = ! empty( $scheduler_config['enabled'] ) && ! empty( $scheduler_config['services'] );
		$shield_deps = array( 'niwa-frontend' );

		if ( $lf_enabled || $scheduler_enabled ) {
			wp_enqueue_style(
				'niwa-lead-form',
				$this->dist_url( 'niwa-lead-form.min.css' ),
				array( 'niwa-frontend' ),
				$this->dist_ver( 'niwa-lead-form.min.css' )
			);
			$shield_deps[] = 'niwa-lead-form';
		}

		wp_enqueue_style(
			'niwa-frontend-shield',
			$this->dist_url( 'niwa-frontend-shield.min.css' ),
			$shield_deps,
			$this->dist_ver( 'niwa-frontend-shield.min.css' )
		);
	}

	/**
	 * Enqueue the morph chat UI inside wp-admin for the admin autonomy launcher.
	 *
	 * Loads the same visual system as the frontend widget, but only the core chat
	 * modules needed for launcher interaction and AJAX messaging.
	 *
	 * @param array $data Localized config exposed as window.NiwaAI.
	 */
	public function enqueue_admin_launcher_assets( $data = array() ) {
		$this->enqueue_admin_launcher_styles();

		$js_dir = NIWA_AI_DIR . 'assets/js/';
		$js_url = NIWA_AI_URL . 'assets/js/';
		$footer = array( 'in_footer' => true, 'strategy' => 'defer' );

		wp_enqueue_script(
			'niwa-utils',
			$js_url . 'modules/utils.js',
			array(),
			$this->get_file_version( $js_dir . 'modules/utils.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-theme-switcher',
			$js_url . 'modules/theme-switcher.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/theme-switcher.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-morph-controller',
			$js_url . 'modules/morph-controller.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/morph-controller.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-persistence',
			$js_url . 'modules/admin-chat-persistence.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-persistence.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-transport',
			$js_url . 'modules/admin-chat-transport.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-transport.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-history',
			$js_url . 'modules/admin-chat-history.js',
			array( 'niwa-admin-chat-transport' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-history.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-status',
			$js_url . 'modules/admin-chat-status.js',
			array( 'niwa-admin-chat-transport' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-status.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-store',
			$js_url . 'modules/admin-chat-store.js',
			array( 'niwa-utils', 'niwa-admin-chat-persistence' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-store.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-view',
			$js_url . 'modules/admin-chat-view.js',
			array( 'niwa-utils', 'niwa-admin-chat-store' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-view.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-queue',
			$js_url . 'modules/admin-chat-queue.js',
			array( 'niwa-admin-chat-transport', 'niwa-admin-chat-status', 'niwa-admin-chat-store' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-queue.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-poller',
			$js_url . 'modules/admin-chat-poller.js',
			array( 'niwa-admin-chat-history', 'niwa-admin-chat-status', 'niwa-admin-chat-store' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-poller.js' ),
			$footer
		);

		wp_enqueue_script(
			'niwa-admin-chat-client',
			$js_url . 'modules/admin-chat-client.js',
			array( 'niwa-utils', 'niwa-theme-switcher', 'niwa-morph-controller', 'niwa-admin-chat-store', 'niwa-admin-chat-view', 'niwa-admin-chat-status', 'niwa-admin-chat-queue', 'niwa-admin-chat-poller' ),
			$this->get_file_version( $js_dir . 'modules/admin-chat-client.js' ),
			$footer
		);

		wp_add_inline_script(
			'niwa-utils',
			'window.NiwaAI = ' . wp_json_encode( $data ) . ';',
			'before'
		);
	}

	private function enqueue_admin_launcher_styles() {
		$css_dir = NIWA_AI_DIR . 'assets/css/';
		$css_url = NIWA_AI_URL . 'assets/css/';

		wp_enqueue_style(
			'niwa-tokens',
			$css_url . 'niwa-tokens.css',
			array(),
			$this->get_file_version( $css_dir . 'niwa-tokens.css' )
		);

		$tokens = array( 'niwa-tokens' );
		foreach ( array( 'reset', 'theme-light', 'theme-dark', 'theme-neutral', 'layout', 'animations' ) as $comp ) {
			wp_enqueue_style(
				"niwa-{$comp}",
				$css_url . "niwa-{$comp}.css",
				$tokens,
				$this->get_file_version( $css_dir . "niwa-{$comp}.css" )
			);
		}

		wp_enqueue_style(
			'niwa-morph',
			$css_url . 'niwa-morph.css',
			array( 'niwa-tokens', 'niwa-layout', 'niwa-animations' ),
			$this->get_file_version( $css_dir . 'niwa-morph.css' )
		);

		wp_enqueue_style(
			'niwa-typing',
			$css_url . 'niwa-typing.css',
			array( 'niwa-tokens', 'niwa-animations' ),
			$this->get_file_version( $css_dir . 'niwa-typing.css' )
		);

		$responsive_css = $this->generate_responsive_launcher_css();
		if ( $responsive_css ) {
			wp_add_inline_style( 'niwa-morph', $responsive_css );
		}

		wp_enqueue_style(
			'niwa-admin-chat',
			$css_url . 'niwa-admin-chat.css',
			array( 'niwa-tokens', 'niwa-reset', 'niwa-theme-light', 'niwa-theme-dark', 'niwa-theme-neutral', 'niwa-layout', 'niwa-animations', 'niwa-morph', 'niwa-typing' ),
			$this->get_file_version( $css_dir . 'niwa-admin-chat.css' )
		);
	}

	/**
	 * Check if chat assets should be loaded on current page.
	 *
	 * @return bool
	 */
	private function should_load_assets() {
		global $post;

		if ( $this->is_runtime_conflict_context() ) {
			return false;
		}

		if ( $post && ( has_shortcode( $post->post_content, 'niwa_ai_chat' ) || has_shortcode( $post->post_content, 'niwa_booking' ) ) ) {
			return true;
		}

		$scheduler = $this->get_scheduler_frontend_config();
		if ( ! empty( $scheduler['enabled'] ) && ! empty( $scheduler['services'] ) ) {
			return true;
		}

		// Read merged general+UI options so this gate stays in sync with
		// `Niwa_AI_Chat_Renderer::render_flyout()` — the renderer also reads the
		// merged option, and a disagreement would render the launcher HTML
		// without enqueuing its CSS/JS (or vice versa).
		$opts    = $this->get_merged_opts();
		$enabled = ! isset( $opts['enable_flyout'] ) || intval( $opts['enable_flyout'] ) === 1;

		if ( $enabled ) {
			return true;
		}

		return apply_filters( 'niwa_ai_should_load_assets', false );
	}

	/**
	 * Detect contexts where frontend widget assets should not load.
	 *
	 * Prevents collisions inside editors, previews, AJAX/JSON requests, and
	 * other non-standard runtimes where the chat UI is not meant to run.
	 *
	 * @return bool
	 */
	private function is_runtime_conflict_context() {
		if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
			return true;
		}

		if ( function_exists( 'wp_is_json_request' ) && wp_is_json_request() ) {
			return true;
		}

		if ( is_feed() || is_embed() ) {
			return true;
		}

		if ( is_customize_preview() ) {
			return true;
		}

		$elementor_preview = isset( $_GET['elementor-preview'] ) ? wp_unslash( $_GET['elementor-preview'] ) : '';
		$elementor_action  = isset( $_GET['action'] ) ? wp_unslash( $_GET['action'] ) : '';

		if ( '' !== $elementor_preview || 'elementor' === $elementor_action ) {
			return true;
		}

		return false;
	}

	/**
	 * Enqueue stylesheets.
	 *
	 * Module load order (dependency chain):
	 *   1. niwa-tokens       → (none) — must load first
	 *   2. niwa-reset         → niwa-tokens (scoped to .niwa-widget)
	 *   3. niwa-theme-light   → niwa-tokens
	 *   4. niwa-theme-dark    → niwa-tokens
	 *   5. niwa-theme-neutral → niwa-tokens
	 *   6. niwa-layout        → niwa-tokens
	 *   6. niwa-animations    → niwa-tokens (keyframes used by morph and typing)
	 *   7. niwa-morph         → niwa-tokens, niwa-layout, niwa-animations
	 *   8. niwa-centerstage   → niwa-tokens, niwa-layout, niwa-animations
	 *   9-14. Component CSS   → niwa-tokens (messages, typing, actions, input-bar, badges, scrollbar)
	 *  15. niwa-products      → niwa-tokens
	 *  16. niwa-attention     → niwa-tokens
	 *  17. niwa-frontend-shield → all frontend styles; final isolation layer against theme CSS
	 */
	private function enqueue_styles() {
		if ( $this->bundles_available() ) {
			$this->enqueue_style_bundles();
			return;
		}

		$css_dir = NIWA_AI_DIR . 'assets/css/';
		$css_url = NIWA_AI_URL . 'assets/css/';

		// 1. Tokens (no deps — must load first).
		wp_enqueue_style(
			'niwa-tokens',
			$css_url . 'niwa-tokens.css',
			array(),
			$this->get_file_version( $css_dir . 'niwa-tokens.css' )
		);

		$tokens = array( 'niwa-tokens' );

		// 2. Reset (scoped to .niwa-widget).
		wp_enqueue_style(
			'niwa-reset',
			$css_url . 'niwa-reset.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-reset.css' )
		);

		// 3. Theme Light.
		wp_enqueue_style(
			'niwa-theme-light',
			$css_url . 'niwa-theme-light.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-theme-light.css' )
		);

		// 4. Theme Dark.
		wp_enqueue_style(
			'niwa-theme-dark',
			$css_url . 'niwa-theme-dark.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-theme-dark.css' )
		);

		// 5. Theme Neutral.
		wp_enqueue_style(
			'niwa-theme-neutral',
			$css_url . 'niwa-theme-neutral.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-theme-neutral.css' )
		);

		// 5. Layout.
		wp_enqueue_style(
			'niwa-layout',
			$css_url . 'niwa-layout.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-layout.css' )
		);

		// 6. Animations (keyframes used by morph and typing).
		wp_enqueue_style(
			'niwa-animations',
			$css_url . 'niwa-animations.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-animations.css' )
		);

		$base = array( 'niwa-tokens', 'niwa-layout', 'niwa-animations' );

		// 7. Morph (main component — pill-to-chat).
		wp_enqueue_style(
			'niwa-morph',
			$css_url . 'niwa-morph.css',
			$base,
			$this->get_file_version( $css_dir . 'niwa-morph.css' )
		);

		// 8. Centerstage variant.
		wp_enqueue_style(
			'niwa-centerstage',
			$css_url . 'niwa-centerstage.css',
			$base,
			$this->get_file_version( $css_dir . 'niwa-centerstage.css' )
		);

		// 8b. Responsive launcher positioning (per-device settings from admin).
		$responsive_css = $this->generate_responsive_launcher_css();
		if ( $responsive_css ) {
			wp_add_inline_style( 'niwa-morph', $responsive_css );
		}

		// 9-14. Component stylesheets (all depend on tokens only).
		$components = array( 'messages', 'typing', 'actions', 'input-bar', 'badges', 'scrollbar' );
		foreach ( $components as $comp ) {
			wp_enqueue_style(
				"niwa-{$comp}",
				$css_url . "niwa-{$comp}.css",
				$tokens,
				$this->get_file_version( $css_dir . "niwa-{$comp}.css" )
			);
		}

		// 15. Products (card rail + pricing).
		wp_enqueue_style(
			'niwa-products',
			$css_url . 'niwa-products.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-products.css' )
		);

		// Inject WooCommerce thumbnail aspect ratio as CSS variable.
		$ratio_css = $this->get_woo_thumbnail_ratio_css();
		if ( $ratio_css ) {
			wp_add_inline_style( 'niwa-products', $ratio_css );
		}

		// 16. Attention capture.
		wp_enqueue_style(
			'niwa-attention',
			$css_url . 'niwa-attention.css',
			$tokens,
			$this->get_file_version( $css_dir . 'niwa-attention.css' )
		);

		// 16.5 Lead Form Generator (conditional, after attention, before shield).
		$lf_opts_css = class_exists( 'Niwa_AI_Lead_Form_Schema' ) ? Niwa_AI_Lead_Form_Schema::get() : array();
		$lf_enabled  = ! empty( $lf_opts_css['enabled'] );
		$scheduler_config  = $this->get_scheduler_frontend_config();
		$scheduler_enabled = ! empty( $scheduler_config['enabled'] ) && ! empty( $scheduler_config['services'] );
		if ( $lf_enabled || $scheduler_enabled ) {
			wp_enqueue_style(
				'niwa-lead-form',
				$css_url . 'niwa-lead-form.css',
				$tokens,
				$this->get_file_version( $css_dir . 'niwa-lead-form.css' )
			);
		}

		// 17. Final frontend shield against theme/global button and form styles.
		$shield_deps = array(
			'niwa-reset',
			'niwa-theme-light',
			'niwa-theme-dark',
			'niwa-theme-neutral',
			'niwa-layout',
			'niwa-animations',
			'niwa-morph',
			'niwa-centerstage',
			'niwa-messages',
			'niwa-typing',
			'niwa-actions',
			'niwa-input-bar',
			'niwa-badges',
			'niwa-scrollbar',
			'niwa-products',
			'niwa-attention',
		);
		if ( $lf_enabled || $scheduler_enabled ) {
			$shield_deps[] = 'niwa-lead-form';
		}
		wp_enqueue_style(
			'niwa-frontend-shield',
			$css_url . 'niwa-frontend-shield.css',
			$shield_deps,
			$this->get_file_version( $css_dir . 'niwa-frontend-shield.css' )
		);
	}

	/**
	 * Enqueue scripts.
	 *
	 * Module load order (dependency chain) — vanilla JS, no GSAP, no jQuery:
	 *   1. niwa-utils            → (none)
	 *   2. niwa-theme-switcher   → niwa-utils
	 *   3. niwa-scroll-behavior  → niwa-utils
	 *   4. niwa-morph-controller → niwa-utils
	 *   5. niwa-message-renderer → niwa-utils, niwa-scroll-behavior
	 *   6. niwa-form-handler     → niwa-utils, niwa-message-renderer
	 *   7. niwa-product-manager  → niwa-utils, niwa-message-renderer
	 *   8. niwa-product-experience → niwa-utils, niwa-message-renderer, niwa-product-manager
	 *   9. niwa-chat-client      → niwa-utils, niwa-message-renderer, niwa-product-manager, niwa-form-handler
	 *  10. niwa-app              → all modules (orchestrator — last)
	 *  10. niwa-cb-shim          → niwa-app
	 *  11. niwa-activity         → niwa-app
	 *  12. niwa-leave-stoper     → niwa-cb-shim (conditional)
	 *  13. niwa-attention-capture → niwa-cb-shim (conditional)
	 *  14. niwa-conversion-busters → niwa-cb-shim (conditional)
	 *
	 * @param array $opts Plugin options.
	 */
	private function enqueue_scripts( $opts ) {
		$js_dir = NIWA_AI_DIR . 'assets/js/';
		$js_url = NIWA_AI_URL . 'assets/js/';
		$footer = array( 'in_footer' => true, 'strategy' => 'defer' );

		if ( $this->bundles_available() ) {
			$this->enqueue_script_bundles( $opts, $footer );
			return;
		}

		// NO GSAP — removed.

		// 1. Utils (vanilla JS, no deps).
		wp_enqueue_script(
			'niwa-utils',
			$js_url . 'modules/utils.js',
			array(),
			$this->get_file_version( $js_dir . 'modules/utils.js' ),
			$footer
		);

		// 2. Theme Switcher.
		wp_enqueue_script(
			'niwa-theme-switcher',
			$js_url . 'modules/theme-switcher.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/theme-switcher.js' ),
			$footer
		);

		// 3. Scroll Behavior.
		wp_enqueue_script(
			'niwa-scroll-behavior',
			$js_url . 'modules/scroll-behavior.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/scroll-behavior.js' ),
			$footer
		);

		// 4. Morph Controller.
		wp_enqueue_script(
			'niwa-morph-controller',
			$js_url . 'modules/morph-controller.js',
			array( 'niwa-utils' ),
			$this->get_file_version( $js_dir . 'modules/morph-controller.js' ),
			$footer
		);

		// 5. Message Renderer.
		wp_enqueue_script(
			'niwa-message-renderer',
			$js_url . 'modules/message-renderer.js',
			array( 'niwa-utils', 'niwa-scroll-behavior' ),
			$this->get_file_version( $js_dir . 'modules/message-renderer.js' ),
			$footer
		);

		// 6. Form Handler.
		wp_enqueue_script(
			'niwa-form-handler',
			$js_url . 'modules/form-handler.js',
			array( 'niwa-utils', 'niwa-message-renderer' ),
			$this->get_file_version( $js_dir . 'modules/form-handler.js' ),
			$footer
		);

		// 7. Product Manager.
		wp_enqueue_script(
			'niwa-product-manager',
			$js_url . 'modules/product-manager.js',
			array( 'niwa-utils', 'niwa-message-renderer' ),
			$this->get_file_version( $js_dir . 'modules/product-manager.js' ),
			$footer
		);

		// 8. In-chat product grid and detail experience.
		wp_enqueue_script(
			'niwa-product-experience',
			$js_url . 'modules/product-experience.js',
			array( 'niwa-utils', 'niwa-message-renderer', 'niwa-product-manager' ),
			$this->get_file_version( $js_dir . 'modules/product-experience.js' ),
			$footer
		);

		// 9. Chat Client (AJAX handler).
		wp_enqueue_script(
			'niwa-chat-client',
			$js_url . 'modules/chat-client.js',
			array( 'niwa-utils', 'niwa-message-renderer', 'niwa-product-manager', 'niwa-product-experience', 'niwa-form-handler' ),
			$this->get_file_version( $js_dir . 'modules/chat-client.js' ),
			$footer
		);

		// 10. Main App (orchestrator — last).
		wp_enqueue_script(
			'niwa-app',
			$js_url . 'niwa-app.js',
			array( 'niwa-utils', 'niwa-theme-switcher', 'niwa-morph-controller', 'niwa-scroll-behavior', 'niwa-message-renderer', 'niwa-chat-client', 'niwa-form-handler', 'niwa-product-manager', 'niwa-product-experience' ),
			$this->get_file_version( $js_dir . 'niwa-app.js' ),
			$footer
		);

		// 9.5 Lead Form Generator (conditional — after niwa-app).
		$lf_opts_js        = class_exists( 'Niwa_AI_Lead_Form_Schema' ) ? Niwa_AI_Lead_Form_Schema::get() : array();
		$scheduler_config  = $this->get_scheduler_frontend_config();
		$scheduler_enabled = ! empty( $scheduler_config['enabled'] ) && ! empty( $scheduler_config['services'] );
		if ( ! empty( $lf_opts_js['enabled'] ) || $scheduler_enabled ) {
			wp_enqueue_script(
				'niwa-scheduler',
				$js_url . 'modules/scheduler.js',
				array( 'niwa-app' ),
				$this->get_file_version( $js_dir . 'modules/scheduler.js' ),
				$footer
			);
			if ( empty( $lf_opts_js['enabled'] ) ) {
				wp_localize_script( 'niwa-scheduler', 'NiwaScheduler', $scheduler_config );
			}
		}
		if ( ! empty( $lf_opts_js['enabled'] ) ) {
			wp_enqueue_script(
				'niwa-lead-form-engine',
				$js_url . 'modules/lead-form-engine.js',
				array( 'niwa-app', 'niwa-scheduler' ),
				$this->get_file_version( $js_dir . 'modules/lead-form-engine.js' ),
				$footer
			);
			wp_enqueue_script(
				'niwa-lead-form-chat-mode',
				$js_url . 'modules/lead-form-chat-mode.js',
				array( 'niwa-lead-form-engine', 'niwa-message-renderer', 'niwa-chat-client' ),
				$this->get_file_version( $js_dir . 'modules/lead-form-chat-mode.js' ),
				$footer
			);
			wp_enqueue_script(
				'niwa-lead-form-card-mode',
				$js_url . 'modules/lead-form-card-mode.js',
				array( 'niwa-lead-form-engine', 'niwa-morph-controller', 'niwa-form-handler' ),
				$this->get_file_version( $js_dir . 'modules/lead-form-card-mode.js' ),
				$footer
			);

			wp_localize_script(
				'niwa-lead-form-engine',
				'NiwaLF',
				$this->build_lead_form_frontend_config( $lf_opts_js )
			);
		}

		// 10. CB Shim (after app).
		wp_enqueue_script(
			'niwa-cb-shim',
			$js_url . 'niwa-cb-shim.js',
			array( 'niwa-app' ),
			$this->get_file_version( $js_dir . 'niwa-cb-shim.js' ),
			$footer
		);

		// 11. Activity tracker (after app).
		wp_enqueue_script(
			'niwa-activity',
			$js_url . 'niwa-activity.js',
			array( 'niwa-app' ),
			$this->get_file_version( $js_dir . 'niwa-activity.js' ),
			$footer
		);

		// 12. Leave Stoper (conditional).
		if ( ! empty( $opts['leave_stoper_enabled'] ) ) {
			wp_enqueue_script(
				'niwa-leave-stoper',
				$js_url . 'leave-stoper.js',
				array( 'niwa-cb-shim' ),
				$this->get_file_version( $js_dir . 'leave-stoper.js' ),
				$footer
			);
		}

		// 13. Attention Capture (conditional).
		$ac_opts = get_option( Niwa_AI_Constants::OPT_ATTENTION_CAPTURE, array() );
		if ( ! empty( $ac_opts['enabled'] ) ) {
			wp_enqueue_script(
				'niwa-attention-capture',
				$js_url . 'attention-capture.js',
				array( 'niwa-cb-shim' ),
				$this->get_file_version( $js_dir . 'attention-capture.js' ),
				$footer
			);

			wp_localize_script( 'niwa-attention-capture', 'NiwaAC', $this->build_ac_config( $ac_opts, $opts ) );
		}

		// 14. Conversion Busters (conditional). Loaded when the CB module is on,
		// OR when Attention Capture OS3 (CB Mini) needs the coupon flow even with
		// the CB module disabled. WooCommerce is required either way. When the CB
		// module is off, NiwaCB.enabled is false so the cart auto-monitor stays
		// dormant — only OS3's manual startMiniFlow() runs.
		$cb_opts      = get_option( Niwa_AI_Constants::OPT_CONVERSION_BUSTERS, array() );
		$os3_needs_cb = ! empty( $ac_opts['enabled'] )
			&& ! empty( $ac_opts['objection_slayer_enabled'] )
			&& ! empty( $ac_opts['nudge_os3_enabled'] );
		if ( ( ! empty( $cb_opts['enabled'] ) || $os3_needs_cb ) && class_exists( 'WooCommerce' ) ) {
			wp_enqueue_script(
				'niwa-conversion-busters',
				$js_url . 'conversion-busters.js',
				array( 'niwa-cb-shim' ),
				$this->get_file_version( $js_dir . 'conversion-busters.js' ),
				$footer
			);

			wp_localize_script( 'niwa-conversion-busters', 'NiwaCB', $this->build_cb_config( $cb_opts ) );
		}
	}

	/**
	 * Localize scripts with configuration data.
	 *
	 * @param array $opts Plugin options.
	 */
	private function localize_scripts( $opts ) {
		$checkout_url = function_exists( 'wc_get_checkout_url' ) ? wc_get_checkout_url() : site_url( '/checkout/' );
		$lead_opts    = class_exists( 'Niwa_AI_Lead_Capture_Settings' ) ? Niwa_AI_Lead_Capture_Settings::get_settings() : array();

		$labels = $this->get_labels( $opts['ui_language'] ?? 'en' );

		$custom_greeting = isset( $opts['greeting_text'] ) ? trim( $opts['greeting_text'] ) : '';
		$greeting_text   = '' !== $custom_greeting ? wp_kses_post( $opts['greeting_text'] ) : $labels['greeting'];

		$banner_message = isset( $opts['banner_message'] ) && '' !== trim( $opts['banner_message'] )
			? wp_kses_post( $opts['banner_message'] )
			: $labels['banner_message'];
		$product_card_footer_mode = isset( $opts['product_card_footer_mode'] ) ? sanitize_text_field( $opts['product_card_footer_mode'] ) : 'delivery';
		if ( ! in_array( $product_card_footer_mode, array( 'delivery', 'score', 'off' ), true ) ) {
			$product_card_footer_mode = 'delivery';
		}

		$product_card_action_mode = isset( $opts['product_card_action_mode'] ) ? sanitize_text_field( $opts['product_card_action_mode'] ) : 'store';
		if ( ! in_array( $product_card_action_mode, array( 'store', 'catalog' ), true ) ) {
			$product_card_action_mode = 'store';
		}

		$product_card_banner_type = isset( $opts['product_card_banner_type'] ) ? sanitize_text_field( $opts['product_card_banner_type'] ) : 'percent';
		if ( 'amount' === $product_card_banner_type ) {
			$product_card_banner_type = 'amount_off';
		}
		if ( ! in_array( $product_card_banner_type, array( 'text', 'percent', 'amount_off', 'off' ), true ) ) {
			$product_card_banner_type = 'percent';
		}

		$product_card_banner_bg_color = isset( $opts['product_card_banner_bg_color'] ) && sanitize_hex_color( $opts['product_card_banner_bg_color'] )
			? sanitize_hex_color( $opts['product_card_banner_bg_color'] )
			: '#000000';
		$product_card_banner_bg_opacity = isset( $opts['product_card_banner_bg_opacity'] ) ? max( 0, min( 100, absint( $opts['product_card_banner_bg_opacity'] ) ) ) : 45;
		$product_card_delivery_days    = isset( $opts['delivery_days'] ) ? max( 1, min( 30, absint( $opts['delivery_days'] ) ) ) : 3;

		// Version stamp so the frontend can detect greeting/language changes
		// and refresh the first chat bubble from localStorage history.
		$greeting_version = md5( ( $opts['ui_language'] ?? 'en' ) . '|' . $greeting_text . '|' . $labels['name_greeting'] );

		$data = array(
			'ajax'      => admin_url( 'admin-ajax.php' ),
			'nonce'     => wp_create_nonce( 'niwa_ai_chat_nonce' ),
			'action'    => 'niwa_ai_chat',
			'nonce_refresh_action' => 'niwa_ai_refresh_chat_nonce',
			'delay'     => 500,
			'greeting'  => $greeting_text,
			'greeting_version' => $greeting_version,
			'checkout'  => $checkout_url,
			'brand'     => array( 'dark' => '#020c0a', 'accent' => '#d2ef9a', 'white' => '#FFFFFF' ),
			'is_debug'  => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
			'pluginURL' => NIWA_AI_URL,
			'analytics' => array(
				'ajax_url'     => admin_url( 'admin-ajax.php' ),
				'nonce'        => wp_create_nonce( 'niwa_ai_chat_nonce' ),
				'track_action' => 'niwa_ai_track_frontend_event',
			),

			// Banner & branding assets.
			'banner_message'     => $banner_message,
			'avatar_url'         => $this->get_frontend_avatar_url( $opts ),
			'default_avatar_url' => $this->get_default_frontend_avatar_url(),
			'wordmark_url'       => NIWA_AI_URL . 'assets/img/niwa-wordmark.svg',

			// Centerstage mode.
			'centerstage_mode'             => ! empty( $opts['centerstage_mode'] ),
			'centerstage_pill_width'       => isset( $opts['centerstage_pill_width'] ) ? (int) $opts['centerstage_pill_width'] : 224,
			'centerstage_pill_height'      => isset( $opts['centerstage_pill_height'] ) ? (int) $opts['centerstage_pill_height'] : 48,
			'centerstage_pill_text'        => isset( $opts['centerstage_pill_text'] ) ? $opts['centerstage_pill_text'] : 'Ask Niwa',
			'centerstage_pill_text_color'  => isset( $opts['centerstage_pill_text_color'] ) ? $opts['centerstage_pill_text_color'] : '#374151',
			'centerstage_pill_arrow_color' => isset( $opts['centerstage_pill_arrow_color'] ) ? $opts['centerstage_pill_arrow_color'] : '#374151',
			'centerstage_pill_desktop'     => array(
				'w' => (int) ( $opts['desktop_centerstage_pill_width'] ?? $opts['centerstage_pill_width'] ?? 224 ),
				'h' => (int) ( $opts['desktop_centerstage_pill_height'] ?? $opts['centerstage_pill_height'] ?? 48 ),
			),
			'centerstage_pill_tablet'      => array(
				'w' => (int) ( $opts['tablet_centerstage_pill_width'] ?? $opts['centerstage_pill_width'] ?? 212 ),
				'h' => (int) ( $opts['tablet_centerstage_pill_height'] ?? $opts['centerstage_pill_height'] ?? 48 ),
			),
			'centerstage_pill_mobile'      => array(
				'w' => (int) ( $opts['mobile_centerstage_pill_width'] ?? $opts['centerstage_pill_width'] ?? 196 ),
				'h' => (int) ( $opts['mobile_centerstage_pill_height'] ?? $opts['centerstage_pill_height'] ?? 48 ),
			),

			// Device-specific launcher settings.
			'launcher_desktop' => array(
				'position' => $opts['desktop_launcher_position'] ?? 'br',
				'x'        => (int) ( $opts['desktop_launcher_x'] ?? 24 ),
				'y'        => (int) ( $opts['desktop_launcher_y'] ?? 24 ),
				'w'        => (int) ( $opts['desktop_launcher_width'] ?? 148 ),
				'h'        => (int) ( $opts['desktop_launcher_height'] ?? 48 ),
			),
			'launcher_tablet' => array(
				'position' => $opts['tablet_launcher_position'] ?? 'br',
				'x'        => (int) ( $opts['tablet_launcher_x'] ?? 20 ),
				'y'        => (int) ( $opts['tablet_launcher_y'] ?? 20 ),
				'w'        => (int) ( $opts['tablet_launcher_width'] ?? 148 ),
				'h'        => (int) ( $opts['tablet_launcher_height'] ?? 48 ),
			),
			'launcher_mobile' => array(
				'position' => $opts['mobile_launcher_position'] ?? 'br',
				'x'        => (int) ( $opts['mobile_launcher_x'] ?? 16 ),
				'y'        => (int) ( $opts['mobile_launcher_y'] ?? 16 ),
				'w'        => (int) ( $opts['mobile_launcher_width'] ?? 140 ),
				'h'        => (int) ( $opts['mobile_launcher_height'] ?? 48 ),
			),

			// Device-specific flyout size settings.
			'desktop' => array(
				'w'          => (int) ( $opts['desktop_width'] ?? 380 ),
				'h'          => (int) ( $opts['desktop_height'] ?? 680 ),
			),
			'tablet' => array(
				'w'          => (int) ( $opts['tablet_width'] ?? 340 ),
				'h'          => (int) ( $opts['tablet_height'] ?? 560 ),
			),
			'mobile' => array(
				'w'          => $this->get_mobile_flyout_width( $opts ),
				'h'          => $this->get_mobile_flyout_height( $opts ),
				'fullscreen' => $this->get_mobile_fullscreen_setting( $opts ),
			),

			// Language and labels.
			'language'    => $opts['ui_language'] ?? 'en',
			'labels'      => $labels,
			'storage_key' => 'niwa_ai_chat_history_v1',
			'cleanup_local_storage_on_load' => ! empty( get_option( Niwa_AI_Constants::OPT_CLEANUP_LOCALSTORAGE, 0 ) ),
			'cleanup_flag_action'           => 'niwa_ai_clear_cleanup_flag',

			// Initial suggestions.
			'initial_suggestions' => class_exists( 'WooCommerce' ) ? array(
				'list'  => array( $labels['show_bestsellers'], $labels['show_new'] ),
				'codes' => array( 'bestsellers', 'latest' ),
			) : null,

			// Email capture settings.
			'email_optin_enabled'  => ! empty( $lead_opts['email_optin_enabled'] ),
			'email_ask_name_again' => ! empty( $lead_opts['email_ask_name_again'] ),
			'email_start_chat_cacher_enabled' => ! empty( $lead_opts['email_start_chat_cacher_enabled'] ),
			'email_start_chat_cacher_prompt'  => isset( $lead_opts['email_start_chat_cacher_prompt'] ) ? (string) $lead_opts['email_start_chat_cacher_prompt'] : '',
			'cb_code_email_hunt_enabled' => ! empty( $lead_opts['cb_code_email_hunt_enabled'] ),
			'cb_code_email_hunt_prompt'  => isset( $lead_opts['cb_code_email_hunt_prompt'] ) ? (string) $lead_opts['cb_code_email_hunt_prompt'] : '',

			// Phone capture settings.
			'phone_capture_enabled' => ! empty( $lead_opts['phone_capture_enabled'] ),
			'phone_capture_mode'    => $lead_opts['phone_capture_mode'] ?? 'email_phone',

			// Leave Stoper settings.
			'leave_stoper' => $this->build_leave_stoper_settings( $opts ),

			// Theme mode (neutral/auto/light/dark).
			'chat_theme_mode' => $opts['chat_theme_mode'] ?? 'neutral',

			// Delivery days (shown on product cards).
			'delivery_days' => $product_card_delivery_days,
			'product_card' => array(
				'action_mode'       => $product_card_action_mode,
				'footer_mode'       => $product_card_footer_mode,
				'banner_type'       => $product_card_banner_type,
				'banner_text'       => $this->get_product_card_banner_text( $opts ),
				'banner_bg_color'   => $product_card_banner_bg_color,
				'banner_bg_opacity' => $product_card_banner_bg_opacity,
				'delivery_days'     => $product_card_delivery_days,
			),

			// Site favicon for product card logos.
			'site_icon' => get_site_icon_url( 64, '' ),

			// Performance-tunable poll intervals — read by niwa-app.js admin
			// history poll and niwa-admin-embeddings.js status poll. Defaults
			// match historical hardcoded values; site owners can lower the
			// rate via Niwa AI → Performance to reduce wp-admin contention
			// on smaller hosts and on WooCommerce-heavy installs.
			'poll' => $this->build_poll_intervals(),
		);

		// Localize to the core handle so NiwaAI config is available to all
		// modules. The handle is the bundle when bundles are active, otherwise
		// the first individual module (niwa-utils).
		wp_localize_script( $this->core_script_handle(), 'NiwaAI', $data );
	}

	/**
	 * Get UI labels with i18n support.
	 *
	 * @param string $lang Language code (en, sr, hr, de, es, fr).
	 * @return array Labels.
	 */
	private function get_labels( $lang = 'en' ) {
		$labels = array(
			// Messages.
			'greeting'            => Niwa_AI_Constants::FRONTEND_DEFAULT_GREETING,
			'banner_message'      => 'How can I help?',
			'error_generic'       => 'Something went wrong. Please try again.',
			'error_message'       => 'Error',
			'typing'              => 'Typing...',

			// Form elements.
			'message_placeholder' => 'Ask Niwa AI...',
			'message_label'       => 'Type your message',
			'send_button'         => 'Send',

			// Product card buttons.
			'add_to_cart'         => 'Add to Cart',
			'adding_to_cart'      => 'Adding...',
			'loading_options'     => 'Loading options...',
			'buy_now'             => 'Buy Now',
			'shop_now'            => 'Shop Now',
			'view_details'        => 'View Details',
			'catalog_cta'         => 'View more',
			'choose_options'      => 'Options',
			'added_to_cart'       => 'Added',
			'product_description' => 'Description',
			'goes_well_with'      => 'Goes well with',
			'see_all_colors'      => 'See all colors',
			'select_options'      => 'Select options',
			'select_attribute'    => 'Select: %s',
			'select_variation'    => 'Select variation',
			'previous_variation'  => 'Previous variation',
			'next_variation'      => 'Next variation',
			'back_to_product'     => 'Back to product',
			'wishlist_product'    => 'Save product',
			'close_product'       => 'Close product',
			'close_panel'         => 'Close panel',
			'delivery'            => 'Delivery',
			'day'                 => 'day',
			'days'                => 'days',
			'score'               => 'Score',
			'review'              => 'review',
			'reviews'             => 'reviews',
			'off'                 => 'Off',
			'discount_off'        => 'OFF',
			'card_banner_text'    => 'Text',
			'card_banner_percent' => 'Percent Off',
			'card_banner_amount_off' => 'Amount Off',
			'card_banner_off'     => 'Off',
			'banner_text_default' => 'Banner text',
			'cart_error'          => 'Could not add to cart. Please try again.',
			'cart_refresh_error'  => 'Could not refresh cart. Please reload the page.',

			// Suggestion buttons.
			'show_bestsellers'    => 'Show bestsellers',
			'show_new'            => 'Show new arrivals',

			// Name capture.
			'name_greeting'       => 'Hello, I am Niwa Assistant, at your service. Can you please tell me your name so I know how to address you?',
			'name_acknowledgment' => 'Nice to meet you, {name}. Please enter your email so we can chat :)',

			// Email capture.
			'email_prompt'        => 'Nice to meet you, {name}. Please enter your email so we can chat :)',
			'email_prompt_no_name' => 'Please enter your email so we can chat :)',
			'email_placeholder'   => 'your.email@example.com',
			'email_label'         => 'Email',
			'email_continue'      => 'Continue',
			'email_invalid'       => 'Please enter a valid email address',
			'email_thanks'        => 'Thank you! Your email has been saved.',
			'email_connected'     => 'Thank you, now we are connected! What can I help you with?',
			'email_unlock_chat'   => 'Please enter your email to unlock the AI assistant.',

			// Phone capture.
			'phone_prompt'        => 'Thank you, {name}. Can you please give me your contact phone number?',
			'phone_prompt_no_name' => 'Can you please give me your contact phone number?',
			'phone_placeholder'   => '+1 234 567 8900',
			'phone_label'         => 'Phone',
			'phone_invalid'       => 'Please enter a valid phone number',
			'phone_thanks'        => 'Thank you! Your phone number has been saved.',
			'phone_connected'     => 'Thank you! How can I help you?',

			// Coupon.
			'coupon_copy'         => 'Copy Code',
			'coupon_copied'       => 'Copied!',
		);

		$i18n = array(
			'sr' => array(
				// Messages.
				'banner_message'      => 'Kako mogu da pomognem?',
				'error_generic'       => 'Došlo je do greške. Pokušajte ponovo.',
				'error_message'       => 'Greška',
				'typing'              => 'Kuca...',

				// Form elements.
				'message_placeholder' => 'Pitaj Niwa AI...',
				'message_label'       => 'Poruka',
				'send_button'         => 'Pošalji',

				// Product card buttons.
				'add_to_cart'         => 'Dodaj u korpu',
				'adding_to_cart'      => 'Dodajem...',
				'loading_options'     => 'Učitavam opcije...',
				'buy_now'             => 'Kupi',
				'shop_now'            => 'Pogledaj',
				'delivery'            => 'Dostava',
				'day'                 => 'dan',
				'days'                => 'dana',
				'score'               => 'Ocena',
				'review'              => 'recenzija',
				'reviews'             => 'recenzija',
				'off'                 => 'Isključeno',
				'discount_off'        => 'POPUST',
				'card_banner_text'    => 'Tekst',
				'card_banner_percent' => 'Procenat',
				'card_banner_amount_off' => 'Iznos popusta',
				'card_banner_off'     => 'Isključeno',
				'banner_text_default' => 'Baner tekst',
				'view_details'        => 'Detaljnije',
				'catalog_cta'         => 'Pogledajte više',
				'choose_options'      => 'Opcije',
				'added_to_cart'       => 'Dodato',
				'product_description' => 'Opis',
				'goes_well_with'      => 'Dobro se slaže sa',
				'see_all_colors'      => 'Sve boje',
				'select_options'      => 'Izaberi opcije',
				'select_attribute'    => 'Izaberi: %s',
				'select_variation'    => 'Izaberi varijaciju',
				'previous_variation'  => 'Prethodna varijacija',
				'next_variation'      => 'Sledeća varijacija',
				'back_to_product'     => 'Nazad na proizvod',
				'wishlist_product'    => 'Sačuvaj proizvod',
				'close_product'       => 'Zatvori proizvod',
				'close_panel'         => 'Zatvori panel',
				'cart_error'          => 'Nije moguće dodati u korpu. Pokušajte ponovo.',
				'cart_refresh_error'  => 'Nije moguće osvežiti korpu. Osvežite stranicu.',

				// Suggestion buttons.
				'show_bestsellers'    => 'Prikaži najprodavanije',
				'show_new'            => 'Prikaži najnovije',

				// Name capture.
				'name_greeting'       => 'Pozdrav, ja sam Niwa Asistent, vama na usluzi. Možete li mi reći vaše ime da znam kako da vas oslovljavam?',
				'name_acknowledgment' => 'Drago mi je {name}. Molim vas upišite vaš email kako bismo mogli da pričamo :)',

				// Email capture.
				'email_prompt'        => 'Drago mi je {name}. Molim vas upišite vaš email kako bismo mogli da pričamo :)',
				'email_prompt_no_name' => 'Molim vas upišite vaš email kako bismo mogli da pričamo :)',
				'email_placeholder'   => 'vas.email@primer.com',
				'email_label'         => 'Email',
				'email_continue'      => 'Nastavi',
				'email_invalid'       => 'Molimo unesite ispravnu email adresu',
				'email_thanks'        => 'Hvala! Vaš email je sačuvan.',
				'email_connected'     => 'Hvala, sada smo povezani! Kako vam mogu pomoći?',
				'email_unlock_chat'   => 'Molim vas upišite email da otključate Ai asistenta.',

				// Phone capture.
				'phone_prompt'        => 'Hvala, {name}. Možete li mi dati kontakt broj telefona?',
				'phone_prompt_no_name' => 'Možete li mi dati kontakt broj telefona?',
				'phone_placeholder'   => '+381 60 123 4567',
				'phone_label'         => 'Telefon',
				'phone_invalid'       => 'Molimo unesite ispravan broj telefona',
				'phone_thanks'        => 'Hvala! Vaš broj telefona je sačuvan.',
				'phone_connected'     => 'Hvala! Kako mogu da pomognem?',

				// Coupon.
				'coupon_copy'         => 'Kopiraj kod',
				'coupon_copied'       => 'Kopirano!',
			),
			'hr' => array(
				// Messages.
				'banner_message'      => 'Kako mogu pomoci?',
				'error_generic'       => 'Došlo je do pogreške. Pokušajte ponovo.',
				'error_message'       => 'Greška',
				'typing'              => 'Tipka...',

				// Form elements.
				'message_placeholder' => 'Pitaj Niwa AI...',
				'message_label'       => 'Poruka',
				'send_button'         => 'Pošalji',

				// Product card buttons.
				'add_to_cart'         => 'Dodaj u košaricu',
				'adding_to_cart'      => 'Dodajem...',
				'loading_options'     => 'Učitavam opcije...',
				'buy_now'             => 'Kupi',
				'shop_now'            => 'Pogledaj',
				'delivery'            => 'Dostava',
				'day'                 => 'dan',
				'days'                => 'dana',
				'score'               => 'Ocjena',
				'review'              => 'recenzija',
				'reviews'             => 'recenzija',
				'off'                 => 'Isključeno',
				'card_banner_text'    => 'Tekst',
				'discount_off'        => 'POPUST',
				'card_banner_percent' => 'Postotak',
				'card_banner_amount_off' => 'Iznos popusta',
				'card_banner_off'     => 'Isključeno',
				'banner_text_default' => 'Banner tekst',
				'view_details'        => 'Detaljnije',
				'catalog_cta'         => 'Pogledajte više',
				'choose_options'      => 'Opcije',
				'added_to_cart'       => 'Dodano',
				'product_description' => 'Opis',
				'goes_well_with'      => 'Dobro ide uz',
				'see_all_colors'      => 'Sve boje',
				'select_options'      => 'Odaberi opcije',
				'select_attribute'    => 'Odaberi: %s',
				'select_variation'    => 'Odaberi varijaciju',
				'previous_variation'  => 'Prethodna varijacija',
				'next_variation'      => 'Sljedeća varijacija',
				'back_to_product'     => 'Natrag na proizvod',
				'wishlist_product'    => 'Spremi proizvod',
				'close_product'       => 'Zatvori proizvod',
				'close_panel'         => 'Zatvori panel',
				'cart_error'          => 'Nije moguće dodati u košaricu. Pokušajte ponovo.',
				'cart_refresh_error'  => 'Nije moguće osvježiti košaricu. Osvježite stranicu.',

				// Suggestion buttons.
				'show_bestsellers'    => 'Prikaži najprodavanije',
				'show_new'            => 'Prikaži najnovije',

				// Name capture.
				'name_greeting'       => 'Pozdrav, ja sam Niwa Asistent. Možete li mi reći vaše ime kako bismo mogli razgovarati?',
				'name_acknowledgment' => 'Drago mi je {name}. Molim vas upišite vaš email kako bismo mogli da pričamo :)',

				// Email capture.
				'email_prompt'        => 'Drago mi je {name}. Molim vas upišite vaš email kako bismo mogli da pričamo :)',
				'email_prompt_no_name' => 'Molim vas upišite vaš email kako bismo mogli da pričamo :)',
				'email_placeholder'   => 'vas.email@primer.com',
				'email_label'         => 'Email',
				'email_continue'      => 'Nastavi',
				'email_invalid'       => 'Molimo unesite ispravnu email adresu',
				'email_thanks'        => 'Hvala! Vaš email je spremljen.',
				'email_connected'     => 'Hvala, sada smo povezani! Kako vam mogu pomoći?',
				'email_unlock_chat'   => 'Molim vas upišite email da otključate Ai asistenta.',

				// Phone capture.
				'phone_prompt'        => 'Hvala, {name}. Možete li mi dati kontakt broj telefona?',
				'phone_prompt_no_name' => 'Možete li mi dati kontakt broj telefona?',
				'phone_placeholder'   => '+385 91 123 4567',
				'phone_label'         => 'Telefon',
				'phone_invalid'       => 'Molimo unesite ispravan broj telefona',
				'phone_thanks'        => 'Hvala! Vaš broj telefona je spremljen.',
				'phone_connected'     => 'Hvala! Kako vam mogu pomoći?',

				// Coupon.
				'coupon_copy'         => 'Kopiraj kod',
				'coupon_copied'       => 'Kopirano!',
			),
			'de' => array(
				// Messages.
				'banner_message'      => 'Wie kann ich helfen?',
				'error_generic'       => 'Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.',
				'error_message'       => 'Fehler',
				'typing'              => 'Schreibt...',

				// Form elements.
				'message_placeholder' => 'Frage Niwa AI...',
				'message_label'       => 'Nachricht',
				'send_button'         => 'Senden',

				// Product card buttons.
				'add_to_cart'         => 'Hinzufügen',
				'adding_to_cart'      => 'Wird hinzugefügt...',
				'loading_options'     => 'Optionen werden geladen...',
				'buy_now'             => 'Kaufen',
				'shop_now'            => 'Ansehen',
				'delivery'            => 'Lieferung',
				'day'                 => 'Tag',
				'days'                => 'Tage',
				'score'               => 'Bewertung',
				'review'              => 'Bewertung',
				'reviews'             => 'Bewertungen',
				'off'                 => 'Aus',
				'discount_off'        => 'RABATT',
				'card_banner_text'    => 'Text',
				'card_banner_percent' => 'Prozent',
				'card_banner_amount_off' => 'Betrag',
				'card_banner_off'     => 'Aus',
				'banner_text_default' => 'Bannertext',
				'view_details'        => 'Details',
				'catalog_cta'         => 'Mehr ansehen',
				'choose_options'      => 'Optionen',
				'added_to_cart'       => 'Hinzugefügt',
				'product_description' => 'Beschreibung',
				'goes_well_with'      => 'Passt gut zu',
				'see_all_colors'      => 'Alle Farben',
				'select_options'      => 'Optionen wählen',
				'select_attribute'    => 'Auswählen: %s',
				'select_variation'    => 'Variante wählen',
				'previous_variation'  => 'Vorherige Variante',
				'next_variation'      => 'Nächste Variante',
				'back_to_product'     => 'Zurück zum Produkt',
				'wishlist_product'    => 'Produkt speichern',
				'close_product'       => 'Produkt schließen',
				'close_panel'         => 'Panel schließen',
				'cart_error'          => 'Konnte nicht zum Warenkorb hinzugefügt werden. Bitte versuchen Sie es erneut.',
				'cart_refresh_error'  => 'Warenkorb konnte nicht aktualisiert werden. Bitte laden Sie die Seite neu.',

				// Suggestion buttons.
				'show_bestsellers'    => 'Bestseller anzeigen',
				'show_new'            => 'Neuheiten anzeigen',

				// Name capture.
				'name_greeting'       => 'Hallo, ich bin Niwa Assistent. Können Sie mir bitte Ihren Namen sagen, damit ich weiß, wie ich Sie ansprechen soll?',
				'name_acknowledgment' => 'Freut mich, {name}. Bitte geben Sie Ihre E-Mail ein, damit wir chatten können :)',

				// Email capture.
				'email_prompt'        => 'Freut mich, {name}. Bitte geben Sie Ihre E-Mail ein, damit wir chatten können :)',
				'email_prompt_no_name' => 'Bitte geben Sie Ihre E-Mail ein, damit wir chatten können :)',
				'email_placeholder'   => 'ihre.email@beispiel.de',
				'email_label'         => 'E-Mail',
				'email_continue'      => 'Weiter',
				'email_invalid'       => 'Bitte geben Sie eine gültige E-Mail-Adresse ein',
				'email_thanks'        => 'Danke! Ihre E-Mail wurde gespeichert.',
				'email_connected'     => 'Vielen Dank, jetzt sind wir verbunden! Wie kann ich Ihnen helfen?',
				'email_unlock_chat'   => 'Bitte geben Sie Ihre E-Mail ein, um den KI-Assistenten freizuschalten.',

				// Phone capture.
				'phone_prompt'        => 'Danke, {name}. Können Sie mir bitte Ihre Telefonnummer geben?',
				'phone_prompt_no_name' => 'Können Sie mir bitte Ihre Telefonnummer geben?',
				'phone_placeholder'   => '+49 30 12345678',
				'phone_label'         => 'Telefon',
				'phone_invalid'       => 'Bitte geben Sie eine gültige Telefonnummer ein',
				'phone_thanks'        => 'Danke! Ihre Telefonnummer wurde gespeichert.',
				'phone_connected'     => 'Vielen Dank! Wie kann ich Ihnen helfen?',

				// Coupon.
				'coupon_copy'         => 'Code kopieren',
				'coupon_copied'       => 'Kopiert!',
			),
			'es' => array(
				// Messages.
				'banner_message'      => 'Como puedo ayudarte?',
				'error_generic'       => 'Algo salió mal. Por favor, inténtalo de nuevo.',
				'error_message'       => 'Error',
				'typing'              => 'Escribiendo...',

				// Form elements.
				'message_placeholder' => 'Pregunta a Niwa AI...',
				'message_label'       => 'Mensaje',
				'send_button'         => 'Enviar',

				// Product card buttons.
				'add_to_cart'         => 'Añadir',
				'adding_to_cart'      => 'Añadiendo...',
				'loading_options'     => 'Cargando opciones...',
				'buy_now'             => 'Comprar',
				'shop_now'            => 'Ver producto',
				'delivery'            => 'Entrega',
				'day'                 => 'dÃ­a',
				'days'                => 'dÃ­as',
				'review'              => 'reseÃ±a',
				'reviews'             => 'reseÃ±as',
				'score'               => 'PuntuaciÃƒÂ³n',
				'review'              => 'reseÃƒÂ±a',
				'reviews'             => 'reseÃƒÂ±as',
				'off'                 => 'Desactivado',
				'discount_off'        => 'DTO.',
				'card_banner_text'    => 'Texto',
				'card_banner_percent' => 'Porcentaje',
				'card_banner_amount_off' => 'Importe',
				'card_banner_off'     => 'Desactivado',
				'banner_text_default' => 'Texto del banner',
				'view_details'        => 'Detalles',
				'catalog_cta'         => 'Ver más',
				'choose_options'      => 'Opciones',
				'added_to_cart'       => 'Añadido',
				'product_description' => 'Descripción',
				'goes_well_with'      => 'Combina bien con',
				'see_all_colors'      => 'Todos los colores',
				'select_options'      => 'Elegir opciones',
				'select_attribute'    => 'Elegir: %s',
				'select_variation'    => 'Elegir variación',
				'previous_variation'  => 'Variación anterior',
				'next_variation'      => 'Siguiente variación',
				'back_to_product'     => 'Volver al producto',
				'wishlist_product'    => 'Guardar producto',
				'close_product'       => 'Cerrar producto',
				'close_panel'         => 'Cerrar panel',
				'cart_error'          => 'No se pudo añadir al carrito. Por favor, inténtalo de nuevo.',
				'cart_refresh_error'  => 'No se pudo actualizar el carrito. Por favor, recarga la página.',

				// Suggestion buttons.
				'show_bestsellers'    => 'Mostrar más vendidos',
				'show_new'            => 'Mostrar novedades',

				// Name capture.
				'name_greeting'       => 'Hola, soy Niwa Asistente. ¿Puedes decirme tu nombre para saber cómo dirigirme a ti?',
				'name_acknowledgment' => 'Encantado, {name}. Por favor, ingresa tu email para que podamos chatear :)',

				// Email capture.
				'email_prompt'        => 'Encantado, {name}. Por favor, ingresa tu email para que podamos chatear :)',
				'email_prompt_no_name' => 'Por favor, ingresa tu email para que podamos chatear :)',
				'email_placeholder'   => 'tu.email@ejemplo.com',
				'email_label'         => 'Email',
				'email_continue'      => 'Continuar',
				'email_invalid'       => 'Por favor, ingresa una dirección de email válida',
				'email_thanks'        => '¡Gracias! Tu email ha sido guardado.',
				'email_connected'     => '¡Gracias, ahora estamos conectados! ¿En qué puedo ayudarte?',
				'email_unlock_chat'   => 'Por favor, ingresa tu email para desbloquear el asistente de IA.',

				// Phone capture.
				'phone_prompt'        => 'Gracias, {name}. ¿Puedes darme tu número de teléfono de contacto?',
				'phone_prompt_no_name' => '¿Puedes darme tu número de teléfono de contacto?',
				'phone_placeholder'   => '+34 612 345 678',
				'phone_label'         => 'Teléfono',
				'phone_invalid'       => 'Por favor, ingresa un número de teléfono válido',
				'phone_thanks'        => '¡Gracias! Tu número de teléfono ha sido guardado.',
				'phone_connected'     => '¡Gracias! ¿En qué puedo ayudarte?',

				// Coupon.
				'coupon_copy'         => 'Copiar código',
				'coupon_copied'       => '¡Copiado!',
			),
			'fr' => array(
				// Messages.
				'banner_message'      => 'Comment puis-je aider ?',
				'error_generic'       => 'Une erreur est survenue. Veuillez réessayer.',
				'error_message'       => 'Erreur',
				'typing'              => 'Écrit...',

				// Form elements.
				'message_placeholder' => 'Demander a Niwa AI...',
				'message_label'       => 'Message',
				'send_button'         => 'Envoyer',

				// Product card buttons.
				'add_to_cart'         => 'Ajouter',
				'adding_to_cart'      => 'Ajout en cours...',
				'loading_options'     => 'Chargement des options...',
				'buy_now'             => 'Acheter',
				'shop_now'            => 'Voir le produit',
				'view_details'        => 'Détails',
				'catalog_cta'         => 'Voir plus',
				'choose_options'      => 'Options',
				'added_to_cart'       => 'Ajouté',
				'product_description' => 'Description',
				'goes_well_with'      => 'Va bien avec',
				'see_all_colors'      => 'Toutes les couleurs',
				'select_options'      => 'Choisir les options',
				'select_attribute'    => 'Choisir : %s',
				'select_variation'    => 'Choisir une variation',
				'previous_variation'  => 'Variation précédente',
				'next_variation'      => 'Variation suivante',
				'back_to_product'     => 'Retour au produit',
				'wishlist_product'    => 'Enregistrer le produit',
				'close_product'       => 'Fermer le produit',
				'close_panel'         => 'Fermer le panneau',
				'delivery'            => 'Livraison',
				'day'                 => 'jour',
				'days'                => 'jours',
				'score'               => 'Note',
				'review'              => 'avis',
				'reviews'             => 'avis',
				'off'                 => 'Desactive',
				'discount_off'        => 'REDUCTION',
				'card_banner_text'    => 'Texte',
				'card_banner_percent' => 'Pourcentage',
				'card_banner_amount_off' => 'Montant',
				'card_banner_off'     => 'Desactive',
				'banner_text_default' => 'Texte du bandeau',
				'cart_error'          => 'Impossible d\'ajouter au panier. Veuillez réessayer.',
				'cart_refresh_error'  => 'Impossible de rafraîchir le panier. Veuillez recharger la page.',

				// Suggestion buttons.
				'show_bestsellers'    => 'Voir les meilleures ventes',
				'show_new'            => 'Voir les nouveautés',

				// Name capture.
				'name_greeting'       => 'Bonjour, je suis l\'assistant Niwa. Pouvez-vous me dire votre nom pour que je sache comment m\'adresser à vous ?',
				'name_acknowledgment' => 'Enchanté, {name}. Veuillez entrer votre email pour que nous puissions discuter :)',

				// Email capture.
				'email_prompt'        => 'Enchanté, {name}. Veuillez entrer votre email pour que nous puissions discuter :)',
				'email_prompt_no_name' => 'Veuillez entrer votre email pour que nous puissions discuter :)',
				'email_placeholder'   => 'votre.email@exemple.fr',
				'email_label'         => 'E-mail',
				'email_continue'      => 'Continuer',
				'email_invalid'       => 'Veuillez entrer une adresse e-mail valide',
				'email_thanks'        => 'Merci ! Votre e-mail a été enregistré.',
				'email_connected'     => 'Merci, nous sommes maintenant connectés ! Comment puis-je vous aider ?',
				'email_unlock_chat'   => 'Veuillez entrer votre e-mail pour débloquer l\'assistant IA.',

				// Phone capture.
				'phone_prompt'        => 'Merci, {name}. Pouvez-vous me donner votre numéro de téléphone ?',
				'phone_prompt_no_name' => 'Pouvez-vous me donner votre numéro de téléphone ?',
				'phone_placeholder'   => '+33 1 23 45 67 89',
				'phone_label'         => 'Téléphone',
				'phone_invalid'       => 'Veuillez entrer un numéro de téléphone valide',
				'phone_thanks'        => 'Merci ! Votre numéro de téléphone a été enregistré.',
				'phone_connected'     => 'Merci ! Comment puis-je vous aider ?',

				// Coupon.
				'coupon_copy'         => 'Copier le code',
				'coupon_copied'       => 'Copié !',
			),
		);

		if ( isset( $i18n[ $lang ] ) ) {
			$labels = array_merge( $labels, $i18n[ $lang ] );
		}
		$labels['greeting'] = Niwa_AI_Constants::frontend_default_greeting( $lang );

		return $labels;
	}

	/**
	 * Get product-card banner text.
	 *
	 * @param array $opts Merged options.
	 * @return string
	 */
	private function get_product_card_banner_text( $opts ) {
		if ( ! empty( $opts['product_card_banner_text'] ) ) {
			return sanitize_text_field( (string) $opts['product_card_banner_text'] );
		}

		$lang = isset( $opts['ui_language'] ) ? sanitize_key( $opts['ui_language'] ) : 'en';
		$key  = 'product_card_banner_text_' . $lang;

		if ( ! empty( $opts[ $key ] ) ) {
			return sanitize_text_field( (string) $opts[ $key ] );
		}

		if ( ! empty( $opts['product_card_banner_text_en'] ) ) {
			return sanitize_text_field( (string) $opts['product_card_banner_text_en'] );
		}

		return 'Banner text';
	}

	/**
	 * Build Leave Stoper settings for frontend.
	 *
	 * @param array $opts Plugin options.
	 * @return array Leave Stoper configuration.
	 */
	/**
	 * Build Lead Form Generator frontend config (NiwaLF localize target).
	 *
	 * Resolves completion_media_id to a URL + coarse media type ("image" |
	 * "video") consumed by the JS modules. Returns a config shape that also
	 * ships translatable UI strings via the i18n bag.
	 *
	 * @param array $lf_opts Sanitized lead-form option (output of Schema::get()).
	 * @return array Config payload for wp_localize_script.
	 */
	private function build_lead_form_frontend_config( $lf_opts ) {
		$media_id   = isset( $lf_opts['completion_media_id'] ) ? (int) $lf_opts['completion_media_id'] : 0;
		$media_url  = '';
		$media_type = '';

		if ( $media_id > 0 ) {
			$resolved_url = wp_get_attachment_url( $media_id );
			if ( $resolved_url ) {
				$media_url = $resolved_url;
				$mime      = get_post_mime_type( $media_id );
				if ( is_string( $mime ) && strpos( $mime, 'video/' ) === 0 ) {
					$media_type = 'video';
				} else {
					$media_type = 'image';
				}
			}
		}

		$lf_session_status = $this->resolve_lead_form_session_status_from_cookie();
		$already_done      = in_array(
			$lf_session_status,
			array(
				Niwa_AI_Lead_Form_Session_Store::STATUS_COMPLETED,
				Niwa_AI_Lead_Form_Session_Store::STATUS_PARTIAL,
			),
			true
		);

		return array(
			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
			'nonce'   => wp_create_nonce( 'niwa_ai_lf' ),
			'scheduler' => $this->get_scheduler_frontend_config(),
			'config'  => array(
				'enabled'               => ! empty( $lf_opts['enabled'] ),
				'mode'                  => isset( $lf_opts['mode'] ) ? (string) $lf_opts['mode'] : 'chat',
				'trigger'               => isset( $lf_opts['trigger'] ) ? (string) $lf_opts['trigger'] : 'after_first_user_msg',
				'trigger_delay_ms'      => isset( $lf_opts['trigger_delay_ms'] ) ? (int) $lf_opts['trigger_delay_ms'] : 3000,
				'partial_submit'        => ! empty( $lf_opts['partial_submit'] ),
				'completion_text'       => isset( $lf_opts['completion_text'] ) ? (string) $lf_opts['completion_text'] : '',
				'completion_media_url'  => $media_url,
				'completion_media_type' => $media_type,
				'completed'             => $already_done,
				'session_status'        => $lf_session_status,
			),
			'i18n'    => array(
				'required'      => __( 'This field is required.', 'niwa-ai' ),
				'invalidEmail'  => __( 'Please enter a valid email.', 'niwa-ai' ),
				'invalidPhone'  => __( 'Please enter a valid phone number.', 'niwa-ai' ),
				'invalidNumber' => __( 'Please enter a valid number.', 'niwa-ai' ),
				'tooShort'      => __( 'Answer is too short.', 'niwa-ai' ),
				'tooLong'       => __( 'Answer is too long.', 'niwa-ai' ),
				'chooseOption'  => __( 'Please choose an option.', 'niwa-ai' ),
				'continue'      => __( 'Continue', 'niwa-ai' ),
				'send'          => __( 'Send', 'niwa-ai' ),
				'close'         => __( 'Close', 'niwa-ai' ),
			),
		);
	}

	/**
	 * Return the public Scheduler catalog and AJAX credentials.
	 *
	 * @return array
	 */
	private function get_scheduler_frontend_config() {
		if ( ! class_exists( 'Niwa_AI_Appointment_Service' ) || ! is_callable( array( 'Niwa_AI_Appointment_Service', 'get_frontend_config' ) ) ) {
			return array(
				'enabled'     => false,
				'services'    => array(),
				'teamMembers' => array(),
			);
		}

		try {
			$config = Niwa_AI_Appointment_Service::get_frontend_config();
			if ( ! is_array( $config ) ) {
				return array();
			}
			// Labels AND calendar month/weekday names follow the plugin UI
			// language (same setting get_labels() uses), not the WP locale.
			$ui_settings = get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() );
			$lang        = is_array( $ui_settings ) && ! empty( $ui_settings['ui_language'] )
				? sanitize_key( $ui_settings['ui_language'] )
				: 'en';
			$locales     = array(
				'en' => 'en',
				'sr' => 'sr-Latn',
				'hr' => 'hr',
				'de' => 'de',
				'es' => 'es',
				'fr' => 'fr',
			);
			$config['locale'] = isset( $locales[ $lang ] )
				? $locales[ $lang ]
				: str_replace( '_', '-', function_exists( 'determine_locale' ) ? determine_locale() : get_locale() );
			$config['i18n']   = $this->get_scheduler_labels( $lang );
			return $config;
		} catch ( Throwable $error ) {
			return array(
				'enabled'     => false,
				'services'    => array(),
				'teamMembers' => array(),
			);
		}
	}

	/**
	 * Scheduler UI labels in the plugin UI language.
	 *
	 * Mirrors get_labels(): English defaults merged with per-language
	 * overrides keyed by the `ui_language` option (en, sr, hr, de, es, fr).
	 *
	 * @param string $lang Language code.
	 * @return array Labels.
	 */
	private function get_scheduler_labels( $lang = 'en' ) {
		$labels = array(
			'schedulerTitle'          => 'Book an appointment',
			'schedulerUnavailable'    => 'Online booking is not available right now.',
			'schedulerError'          => 'Something went wrong. Please try again.',
			'loadingAvailability'     => 'Loading available dates...',
			'loadingTimes'            => 'Loading available times...',
			'noDates'                 => 'No available dates in this month.',
			'noTimes'                 => 'No available times for this date.',
			'previousMonth'           => 'Previous month',
			'nextMonth'               => 'Next month',
			'service'                 => 'Service',
			'teamMember'              => 'Team member',
			'anyAvailable'            => 'Any available',
			'pickDate'                => 'Pick a date',
			'pickTime'                => 'Pick a time',
			'back'                    => 'Back',
			'yourDetails'             => 'Your details',
			'name'                    => 'Name',
			'phone'                   => 'Phone',
			'email'                   => 'Email',
			'confirmBooking'          => 'Confirm booking',
			'booking'                 => 'Booking your appointment...',
			'bookingConfirmed'        => 'Your appointment is confirmed.',
			'appointmentAnswerPrefix' => 'Appointment',
			'chooseAppointment'       => 'Choose an available date and time in the scheduler.',
			'close'                   => 'Close',
		);

		$i18n = array(
			'sr' => array(
				'schedulerTitle'          => 'Zakažite termin',
				'schedulerUnavailable'    => 'Online zakazivanje trenutno nije dostupno.',
				'schedulerError'          => 'Došlo je do greške. Pokušajte ponovo.',
				'loadingAvailability'     => 'Učitavam slobodne datume...',
				'loadingTimes'            => 'Učitavam slobodne termine...',
				'noDates'                 => 'Nema slobodnih datuma u ovom mesecu.',
				'noTimes'                 => 'Nema slobodnih termina za ovaj datum.',
				'previousMonth'           => 'Prethodni mesec',
				'nextMonth'               => 'Sledeći mesec',
				'service'                 => 'Usluga',
				'teamMember'              => 'Član tima',
				'anyAvailable'            => 'Bilo ko slobodan',
				'pickDate'                => 'Izaberite datum',
				'pickTime'                => 'Izaberite vreme',
				'back'                    => 'Nazad',
				'yourDetails'             => 'Vaši podaci',
				'name'                    => 'Ime',
				'phone'                   => 'Telefon',
				'email'                   => 'Email',
				'confirmBooking'          => 'Potvrdi zakazivanje',
				'booking'                 => 'Zakazujem vaš termin...',
				'bookingConfirmed'        => 'Vaš termin je potvrđen.',
				'appointmentAnswerPrefix' => 'Termin',
				'chooseAppointment'       => 'Izaberite slobodan datum i vreme u kalendaru.',
				'close'                   => 'Zatvori',
			),
			'hr' => array(
				'schedulerTitle'          => 'Zakažite termin',
				'schedulerUnavailable'    => 'Online rezervacija trenutno nije dostupna.',
				'schedulerError'          => 'Došlo je do pogreške. Pokušajte ponovno.',
				'loadingAvailability'     => 'Učitavam slobodne datume...',
				'loadingTimes'            => 'Učitavam slobodne termine...',
				'noDates'                 => 'Nema slobodnih datuma u ovom mjesecu.',
				'noTimes'                 => 'Nema slobodnih termina za ovaj datum.',
				'previousMonth'           => 'Prethodni mjesec',
				'nextMonth'               => 'Sljedeći mjesec',
				'service'                 => 'Usluga',
				'teamMember'              => 'Član tima',
				'anyAvailable'            => 'Bilo tko slobodan',
				'pickDate'                => 'Odaberite datum',
				'pickTime'                => 'Odaberite vrijeme',
				'back'                    => 'Natrag',
				'yourDetails'             => 'Vaši podaci',
				'name'                    => 'Ime',
				'phone'                   => 'Telefon',
				'email'                   => 'Email',
				'confirmBooking'          => 'Potvrdi rezervaciju',
				'booking'                 => 'Rezerviram vaš termin...',
				'bookingConfirmed'        => 'Vaš termin je potvrđen.',
				'appointmentAnswerPrefix' => 'Termin',
				'chooseAppointment'       => 'Odaberite slobodan datum i vrijeme u kalendaru.',
				'close'                   => 'Zatvori',
			),
			'de' => array(
				'schedulerTitle'          => 'Termin buchen',
				'schedulerUnavailable'    => 'Die Online-Buchung ist derzeit nicht verfügbar.',
				'schedulerError'          => 'Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.',
				'loadingAvailability'     => 'Verfügbare Termine werden geladen...',
				'loadingTimes'            => 'Verfügbare Uhrzeiten werden geladen...',
				'noDates'                 => 'Keine verfügbaren Termine in diesem Monat.',
				'noTimes'                 => 'Keine verfügbaren Uhrzeiten für dieses Datum.',
				'previousMonth'           => 'Vorheriger Monat',
				'nextMonth'               => 'Nächster Monat',
				'service'                 => 'Leistung',
				'teamMember'              => 'Teammitglied',
				'anyAvailable'            => 'Beliebig verfügbar',
				'pickDate'                => 'Datum wählen',
				'pickTime'                => 'Uhrzeit wählen',
				'back'                    => 'Zurück',
				'yourDetails'             => 'Ihre Daten',
				'name'                    => 'Name',
				'phone'                   => 'Telefon',
				'email'                   => 'E-Mail',
				'confirmBooking'          => 'Buchung bestätigen',
				'booking'                 => 'Ihr Termin wird gebucht...',
				'bookingConfirmed'        => 'Ihr Termin ist bestätigt.',
				'appointmentAnswerPrefix' => 'Termin',
				'chooseAppointment'       => 'Wählen Sie im Kalender ein freies Datum und eine Uhrzeit.',
				'close'                   => 'Schließen',
			),
			'es' => array(
				'schedulerTitle'          => 'Reservar una cita',
				'schedulerUnavailable'    => 'La reserva online no está disponible en este momento.',
				'schedulerError'          => 'Algo salió mal. Inténtalo de nuevo.',
				'loadingAvailability'     => 'Cargando fechas disponibles...',
				'loadingTimes'            => 'Cargando horarios disponibles...',
				'noDates'                 => 'No hay fechas disponibles este mes.',
				'noTimes'                 => 'No hay horarios disponibles para esta fecha.',
				'previousMonth'           => 'Mes anterior',
				'nextMonth'               => 'Mes siguiente',
				'service'                 => 'Servicio',
				'teamMember'              => 'Miembro del equipo',
				'anyAvailable'            => 'Cualquiera disponible',
				'pickDate'                => 'Elige una fecha',
				'pickTime'                => 'Elige una hora',
				'back'                    => 'Atrás',
				'yourDetails'             => 'Tus datos',
				'name'                    => 'Nombre',
				'phone'                   => 'Teléfono',
				'email'                   => 'Email',
				'confirmBooking'          => 'Confirmar reserva',
				'booking'                 => 'Reservando tu cita...',
				'bookingConfirmed'        => 'Tu cita está confirmada.',
				'appointmentAnswerPrefix' => 'Cita',
				'chooseAppointment'       => 'Elige una fecha y hora disponibles en el calendario.',
				'close'                   => 'Cerrar',
			),
			'fr' => array(
				'schedulerTitle'          => 'Prendre rendez-vous',
				'schedulerUnavailable'    => 'La réservation en ligne n\'est pas disponible pour le moment.',
				'schedulerError'          => 'Une erreur est survenue. Veuillez réessayer.',
				'loadingAvailability'     => 'Chargement des dates disponibles...',
				'loadingTimes'            => 'Chargement des horaires disponibles...',
				'noDates'                 => 'Aucune date disponible ce mois-ci.',
				'noTimes'                 => 'Aucun horaire disponible pour cette date.',
				'previousMonth'           => 'Mois précédent',
				'nextMonth'               => 'Mois suivant',
				'service'                 => 'Service',
				'teamMember'              => 'Membre de l\'équipe',
				'anyAvailable'            => 'N\'importe qui de disponible',
				'pickDate'                => 'Choisissez une date',
				'pickTime'                => 'Choisissez une heure',
				'back'                    => 'Retour',
				'yourDetails'             => 'Vos coordonnées',
				'name'                    => 'Nom',
				'phone'                   => 'Téléphone',
				'email'                   => 'Email',
				'confirmBooking'          => 'Confirmer le rendez-vous',
				'booking'                 => 'Réservation de votre rendez-vous...',
				'bookingConfirmed'        => 'Votre rendez-vous est confirmé.',
				'appointmentAnswerPrefix' => 'Rendez-vous',
				'chooseAppointment'       => 'Choisissez une date et une heure disponibles dans le calendrier.',
				'close'                   => 'Fermer',
			),
		);

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

		return $labels;
	}

	/**
	 * Resolve the lead-form session status for the visitor on this page load.
	 *
	 * Reads the verified server-owned chat session and looks the row up in the
	 * lead-form session store. Returns one of the
	 * STATUS_* constants when a row exists, or an empty string for new visitors,
	 * missing cookie, or any failure. Failure is silent — the caller treats
	 * "unknown" as "not done yet".
	 *
	 * @return string
	 */
	private function resolve_lead_form_session_status_from_cookie() {
		if ( ! class_exists( 'Niwa_AI_Lead_Form_Session_Store' ) ) {
			return '';
		}

		$session_id = Niwa_AI_Session_Utils::resolve_verified_session( false );
		if ( '' === $session_id ) {
			return '';
		}

		try {
			$store = new Niwa_AI_Lead_Form_Session_Store();
			$row   = $store->find_by_session_id( $session_id );
			if ( ! is_array( $row ) || empty( $row['status'] ) ) {
				return '';
			}
			return (string) $row['status'];
		} catch ( \Throwable $e ) {
			return '';
		}
	}

	/**
	 * Build the poll-interval block for the localized NiwaAI config.
	 * Pulls values from Niwa_AI_Performance_Settings so admin users can
	 * tune them at runtime. Falls back to historical hardcoded defaults
	 * if the class is somehow unavailable (e.g. autoloader hiccup).
	 *
	 * Browser-side consumers:
	 *   - assets/js/modules/admin-chat-client.js — `admin_history_poll_ms`
	 *   - assets/js/niwa-admin-embeddings.js — `embeddings_status_poll_ms`
	 *
	 * @return array{admin_history_poll_ms:int, embeddings_status_poll_ms:int}
	 */
	private function build_poll_intervals() {
		if ( class_exists( 'Niwa_AI_Performance_Settings' ) ) {
			$perf = Niwa_AI_Performance_Settings::get();
			return array(
				'admin_history_poll_ms'     => isset( $perf['admin_history_poll_ms'] ) ? (int) $perf['admin_history_poll_ms'] : 60000,
				'embeddings_status_poll_ms' => isset( $perf['embeddings_status_poll_ms'] ) ? (int) $perf['embeddings_status_poll_ms'] : 5000,
			);
		}
		return array(
			'admin_history_poll_ms'     => 60000,
			'embeddings_status_poll_ms' => 5000,
		);
	}

	private function build_leave_stoper_settings( $opts ) {
		return array(
			'enabled'            => ! empty( $opts['leave_stoper_enabled'] ),
			'trigger_type'       => $opts['leave_stoper_trigger_type'] ?? 'exit_intent',
			'inactivity_timeout' => max( 10, min( 300, (int) ( $opts['leave_stoper_inactivity_timeout'] ?? 45 ) ) ),
			'open_delay'         => max( 100, min( 2000, (int) ( $opts['leave_stoper_open_delay'] ?? 300 ) ) ),
			'cooldown_minutes'   => max( 1, min( 120, (int) ( $opts['leave_stoper_session_cooldown'] ?? 30 ) ) ),
			'disable_on_mobile'  => ! empty( $opts['leave_stoper_disable_on_mobile'] ),
			'test_mode'          => ! empty( $opts['leave_stoper_test_mode'] ),
			'reset_on_page_load' => ! empty( $opts['leave_stoper_reset_on_page_load'] ),
			'media'              => $this->get_leave_stoper_media( $opts ),
			'message_text'       => sanitize_text_field( $opts['leave_stoper_message_text'] ?? '' ),
		);
	}

	/**
	 * Get Leave Stoper media data.
	 *
	 * @param array $opts Plugin options.
	 * @return array|null Media data or null when unset.
	 */
	private function get_leave_stoper_media( $opts ) {
		$media_id = isset( $opts['leave_stoper_media_id'] ) ? (int) $opts['leave_stoper_media_id'] : 0;

		if ( ! $media_id ) {
			return null;
		}

		$attachment_url = wp_get_attachment_url( $media_id );
		if ( ! $attachment_url ) {
			return null;
		}

		$mime_type = get_post_mime_type( $media_id );
		$alt_text  = $opts['leave_stoper_alt_text'] ?? '';

		if ( '' === $alt_text ) {
			$alt_text = get_post_meta( $media_id, '_wp_attachment_image_alt', true );
		}

		$type = 'image';
		if ( is_string( $mime_type ) && strpos( $mime_type, 'video/' ) === 0 ) {
			$type = 'video';
		}

		return array(
			'id'        => $media_id,
			'url'       => $attachment_url,
			'type'      => $type,
			'mime_type' => $mime_type,
			'alt'       => sanitize_text_field( $alt_text ),
		);
	}

	/**
	 * Build Attention Capture config for frontend.
	 *
	 * @param array $ac_opts Attention capture options.
	 * @return array AC configuration.
	 */
	private function build_ac_config( $ac_opts, $frontend_opts = array() ) {
		$objection_slayers = array();

		// OS2 — scarcity appeal (shown on all product pages).
		if ( ! empty( $ac_opts['nudge_os2_enabled'] ) ) {
			$objection_slayers[] = array(
				'level'             => 2,
				'message_type'      => 'nudge_os2',
				'type'              => 'all_products',
				'url'               => '',
				'match_type'        => 'contains',
				'priority'          => 20,
				'timeout'           => (int) ( $ac_opts['nudge_os2_timeout'] ?? 22 ),
				'message'           => $ac_opts['nudge_os2_message'] ?? '',
				'button_text'       => '',
				'button_reply_text' => '',
				'button_timeout'    => 5,
				'is_cb_mini'        => false,
			);
		}

		// OS3 — CB Mini (discount offer, shown on all product pages).
		if ( ! empty( $ac_opts['nudge_os3_enabled'] ) ) {
			$objection_slayers[] = array(
				'level'             => 3,
				'message_type'      => 'nudge_os3',
				'type'              => 'all_products',
				'url'               => '',
				'match_type'        => 'contains',
				'priority'          => 30,
				'timeout'           => (int) ( $ac_opts['nudge_os3_timeout'] ?? 35 ),
				'message'           => $ac_opts['nudge_os3_message'] ?? '',
				'button_text'       => $ac_opts['nudge_os3_button_text'] ?? '',
				'button_reply_text' => $ac_opts['nudge_os3_button_reply_text'] ?? '',
				'button_timeout'    => (int) ( $ac_opts['nudge_os3_button_timeout'] ?? 5 ),
				'is_cb_mini'        => true,
			);
		}

		// Conditional rules — unlimited repeater (URL-targeted or all-products).
		$conditional_rules = ( isset( $ac_opts['conditional_rules'] ) && is_array( $ac_opts['conditional_rules'] ) )
			? $ac_opts['conditional_rules']
			: array();
		foreach ( $conditional_rules as $rule ) {
			if ( ! is_array( $rule ) || empty( $rule['enabled'] ) ) {
				continue;
			}

			$rid  = isset( $rule['id'] ) ? (int) $rule['id'] : 0;
			$type = ! empty( $rule['all_products'] ) ? 'all_products' : 'url';

			$objection_slayers[] = array(
				'level'             => 100 + $rid,
				'message_type'      => 'nudge_rule_' . $rid,
				'type'              => $type,
				'url'               => $rule['url'] ?? '',
				'match_type'        => $rule['match_type'] ?? 'contains',
				'priority'          => (int) ( $rule['priority'] ?? 10 ),
				'timeout'           => (int) ( $rule['timeout'] ?? 30 ),
				'message'           => $rule['message'] ?? '',
				'button_text'       => $rule['button_text'] ?? '',
				'button_reply_text' => $rule['button_reply_text'] ?? '',
				'button_timeout'    => (int) ( $rule['button_timeout'] ?? 5 ),
				'is_cb_mini'        => false,
			);
		}

		$ignore_pages = array();
		$raw_ignored  = ( isset( $ac_opts['ignore_pages'] ) && is_array( $ac_opts['ignore_pages'] ) )
			? $ac_opts['ignore_pages']
			: array();
		foreach ( $raw_ignored as $page ) {
			if ( ! is_array( $page ) || empty( $page['enabled'] ) || empty( $page['url'] ) ) {
				continue;
			}

			$match_type = isset( $page['match_type'] ) ? sanitize_key( (string) $page['match_type'] ) : 'contains';
			if ( ! in_array( $match_type, array( 'contains', 'equals', 'regex' ), true ) ) {
				$match_type = 'contains';
			}

			$ignore_pages[] = array(
				'id'         => isset( $page['id'] ) ? (int) $page['id'] : 0,
				'enabled'    => true,
				'url'        => sanitize_text_field( (string) $page['url'] ),
				'match_type' => $match_type,
			);
		}

		return array(
			'welcome_enabled'              => ! empty( $ac_opts['welcome_enabled'] ),
			'welcome_message'              => $ac_opts['welcome_custom_message'] ?? '',
				'wakeup_enabled'               => ! empty( $ac_opts['wakeup_enabled'] ),
				'wakeup_timeout'               => (int) ( $ac_opts['wakeup_timeout'] ?? 60 ),
				'wakeup_message'               => $ac_opts['wakeup_custom_message'] ?? '',
				'objection_slayer_enabled'     => ! empty( $ac_opts['objection_slayer_enabled'] ),
				'nudge_os1_enabled'            => ! empty( $ac_opts['nudge_os1_enabled'] ),
				'nudge_os1_ai_enabled'         => ! empty( $ac_opts['nudge_os1_ai_enabled'] ),
				'nudge_timeout'                => (int) ( $ac_opts['nudge_timeout'] ?? 10 ),
				'nudge_sitewide_message'       => $ac_opts['nudge_sitewide_message'] ?? '',
				'product_id'                   => function_exists( 'is_product' ) && is_product() ? (int) get_queried_object_id() : 0,
				'ai_objection_action'          => 'niwa_ai_ac_generate_product_objection',
				'nudge_os3_clarify_message'    => $ac_opts['nudge_os3_clarify_message'] ?? '',
				'objection_slayers'            => $objection_slayers,
				'ignore_pages'                 => $ignore_pages,
				'checkout_incentive_enabled'   => ! empty( $ac_opts['checkout_incentive_enabled'] ),
				'checkout_incentive_timeout'   => (int) ( $ac_opts['checkout_incentive_timeout'] ?? 15 ),
				'checkout_incentive_message'   => $ac_opts['checkout_incentive_custom_message'] ?? '',
				'checkout_finish_enabled'      => ! empty( $ac_opts['checkout_finish_enabled'] ),
				'checkout_finish_message'      => $ac_opts['checkout_finish_custom_message'] ?? '',
				'wc_active'                    => class_exists( 'WooCommerce' ),
				'test_mode'                    => ! empty( $ac_opts['test_mode'] ),
				'pluginURL'                    => NIWA_AI_URL,
				'avatar_url'                   => $this->get_frontend_avatar_url( $frontend_opts ),
				'default_avatar_url'           => $this->get_default_frontend_avatar_url(),
				'ajax_url'                     => admin_url( 'admin-ajax.php' ),
				'nonce'                        => wp_create_nonce( 'niwa_ai_chat_nonce' ),
				'track_action'                 => 'niwa_ai_ac_track_event',
			);
		}

	/**
	 * Build Conversion Busters config for frontend.
	 *
	 * @param array $cb_opts Conversion busters options.
	 * @return array CB configuration.
	 */
	private function build_cb_config( $cb_opts ) {
		$lead_opts = class_exists( 'Niwa_AI_Lead_Capture_Settings' ) ? Niwa_AI_Lead_Capture_Settings::get_settings() : array();
		$clarify_message = '';
		if ( ! empty( $cb_opts['clarify_message'] ) ) {
			$clarify_message = sanitize_textarea_field( $cb_opts['clarify_message'] );
		} else {
			$ac_opts = get_option( Niwa_AI_Constants::OPT_ATTENTION_CAPTURE, array() );
			if ( is_array( $ac_opts ) && ! empty( $ac_opts['nudge_os3_clarify_message'] ) ) {
				$clarify_message = sanitize_textarea_field( $ac_opts['nudge_os3_clarify_message'] );
			}
		}

		return array(
			'enabled'          => ! empty( $cb_opts['enabled'] ),
			'language'         => get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() )['ui_language'] ?? 'en',
			'cart_threshold'   => (int) ( $cb_opts['cart_threshold'] ?? 2 ),
			'session_cooldown' => (int) ( $cb_opts['session_cooldown'] ?? 30 ) * 60 * 1000,
			'clarify_message'  => $clarify_message,
			'email_hunt_enabled' => ! empty( $lead_opts['cb_code_email_hunt_enabled'] ),
			'email_hunt_prompt'  => isset( $lead_opts['cb_code_email_hunt_prompt'] ) ? sanitize_textarea_field( (string) $lead_opts['cb_code_email_hunt_prompt'] ) : '',
			'wc_active'        => class_exists( 'WooCommerce' ),
			'ajax_url'         => admin_url( 'admin-ajax.php' ),
			'nonce'            => wp_create_nonce( 'niwa_ai_chat_nonce' ),
		);
	}

	/**
	 * Build CSS rule that sets --niwa-card-ratio from WooCommerce thumbnail dimensions.
	 *
	 * @return string CSS rule or empty string when WooCommerce is not active.
	 */
	private function get_woo_thumbnail_ratio_css() {
		if ( ! function_exists( 'wc_get_image_size' ) ) {
			return '';
		}

		$size = wc_get_image_size( 'woocommerce_thumbnail' );

		if ( empty( $size['width'] ) || empty( $size['height'] ) ) {
			return '';
		}

		$w = (int) $size['width'];
		$h = (int) $size['height'];

		if ( $w <= 0 || $h <= 0 ) {
			return '';
		}

		$gcd = $this->gcd( $w, $h );

		return sprintf(
			'.niwa-widget { --niwa-card-ratio: %d / %d; }',
			$w / $gcd,
			$h / $gcd
		);
	}

	/**
	 * Greatest common divisor (Euclidean algorithm).
	 *
	 * @param int $a First number.
	 * @param int $b Second number.
	 * @return int GCD.
	 */
	private function gcd( $a, $b ) {
		while ( $b ) {
			$t = $b;
			$b = $a % $b;
			$a = $t;
		}
		return $a;
	}

	/**
	 * Get cached file version for cache busting.
	 *
	 * @param string $file_path Path to file.
	 * @return string Version string.
	 */
	private function get_file_version( $file_path ) {
		if ( file_exists( $file_path ) ) {
			$mtime = filemtime( $file_path );
			if ( $mtime ) {
				return $this->version . '.' . $mtime;
			}
		}
		return $this->version;
	}

	private function get_frontend_avatar_url( $opts = array() ) {
		$opts = is_array( $opts ) ? $opts : array();
		$url  = isset( $opts['frontend_avatar_url'] ) ? esc_url_raw( (string) $opts['frontend_avatar_url'] ) : '';

		return '' !== trim( $url ) ? $url : $this->get_default_frontend_avatar_url();
	}

	private function get_default_frontend_avatar_url() {
		return defined( 'NIWA_AI_DEFAULT_FRONTEND_AVATAR_URL' )
			? NIWA_AI_DEFAULT_FRONTEND_AVATAR_URL
			: NIWA_AI_URL . 'assets/img/niwa-admin-personal-assistant.png';
	}

	/**
	 * Get merged options with defaults.
	 *
	 * @return array Options.
	 */
	private function get_merged_opts() {
		$general = get_option( Niwa_AI_Constants::OPT_SETTINGS, array() );
		$ui      = get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() );
		return array_merge(
			is_array( $general ) ? $general : array(),
			is_array( $ui ) ? $ui : array()
		);
	}

	/**
	 * Get the effective mobile flyout width in vw.
	 *
	 * @param array $opts Plugin options.
	 * @return int
	 */
	private function get_mobile_flyout_width( $opts ) {
		$width = ! empty( $opts['mobile_width_vw'] ) ? (int) $opts['mobile_width_vw'] : self::MOBILE_FLYOUT_WIDTH_DEFAULT;
		return $width >= 1 && $width <= 100 ? $width : self::MOBILE_FLYOUT_WIDTH_DEFAULT;
	}

	/**
	 * Get the effective mobile flyout height in dvh.
	 *
	 * @param array $opts Plugin options.
	 * @return int
	 */
	private function get_mobile_flyout_height( $opts ) {
		$height = ! empty( $opts['mobile_height_dvh'] ) ? (int) $opts['mobile_height_dvh'] : self::MOBILE_FLYOUT_HEIGHT_DEFAULT;
		return $height >= 1 && $height <= 100 ? $height : self::MOBILE_FLYOUT_HEIGHT_DEFAULT;
	}

	/**
	 * Get the effective mobile fullscreen flag.
	 *
	 * @param array $opts Plugin options.
	 * @return bool
	 */
	private function get_mobile_fullscreen_setting( $opts ) {
		if ( ! array_key_exists( 'mobile_fullscreen', $opts ) ) {
			return true;
		}

		return ! empty( $opts['mobile_fullscreen'] );
	}

	/**
	 * Generate responsive CSS for launcher positioning per device.
	 *
	 * Reads desktop/tablet/mobile launcher settings from admin and produces
	 * CSS rules with media queries so the pill and flyout adapt per breakpoint.
	 *
	 * Breakpoints:
	 *   Desktop  — > 1024px (default, no media query)
	 *   Tablet   — 481px – 1024px
	 *   Mobile   — ≤ 480px
	 *
	 * @return string CSS rules (empty string if nothing to inject).
	 */
	private function generate_responsive_launcher_css() {
		$opts           = $this->get_merged_opts();
		$is_centerstage = ! empty( $opts['centerstage_mode'] );
		$css            = '';

		// Gather per-device launcher settings.
		// Use !empty() instead of ?? to handle empty strings from the admin
		// form — (int) '' === 0 which would break flyout dimensions.
		//
		// Desktop/tablet dimensions use pixels; mobile uses vw/dvh percentages.
		// Fullscreen remains a separate flag handled by viewport CSS.
		$desktop_fw = ! empty( $opts['desktop_width'] ) ? max( (int) $opts['desktop_width'], 200 ) : 380;
		$desktop_fh = ! empty( $opts['desktop_height'] ) ? max( (int) $opts['desktop_height'], 200 ) : 680;
		$mobile_fw  = $this->get_mobile_flyout_width( $opts );
		$mobile_fh  = $this->get_mobile_flyout_height( $opts );

		$devices = array(
			'desktop' => array(
				'position' => ! empty( $opts['desktop_launcher_position'] ) ? $opts['desktop_launcher_position'] : 'br',
				'x'        => ! empty( $opts['desktop_launcher_x'] ) ? (int) $opts['desktop_launcher_x'] : 24,
				'y'        => ! empty( $opts['desktop_launcher_y'] ) ? (int) $opts['desktop_launcher_y'] : 24,
				'w'        => ! empty( $opts['desktop_launcher_width'] ) ? (int) $opts['desktop_launcher_width'] : 148,
				'h'        => ! empty( $opts['desktop_launcher_height'] ) ? (int) $opts['desktop_launcher_height'] : 48,
				'fw'       => $desktop_fw,
				'fh'       => $desktop_fh,
			),
			'tablet'  => array(
				'position' => ! empty( $opts['tablet_launcher_position'] ) ? $opts['tablet_launcher_position'] : 'br',
				'x'        => ! empty( $opts['tablet_launcher_x'] ) ? (int) $opts['tablet_launcher_x'] : 20,
				'y'        => ! empty( $opts['tablet_launcher_y'] ) ? (int) $opts['tablet_launcher_y'] : 20,
				'w'        => ! empty( $opts['tablet_launcher_width'] ) ? (int) $opts['tablet_launcher_width'] : 148,
				'h'        => ! empty( $opts['tablet_launcher_height'] ) ? (int) $opts['tablet_launcher_height'] : 48,
				'fw'       => ! empty( $opts['tablet_width'] ) ? max( (int) $opts['tablet_width'], 200 ) : min( $desktop_fw, 340 ),
				'fh'       => ! empty( $opts['tablet_height'] ) ? max( (int) $opts['tablet_height'], 200 ) : min( $desktop_fh, 560 ),
			),
			'mobile'  => array(
				'position' => ! empty( $opts['mobile_launcher_position'] ) ? $opts['mobile_launcher_position'] : 'br',
				'x'        => ! empty( $opts['mobile_launcher_x'] ) ? (int) $opts['mobile_launcher_x'] : 16,
				'y'        => ! empty( $opts['mobile_launcher_y'] ) ? (int) $opts['mobile_launcher_y'] : 16,
				'w'        => ! empty( $opts['mobile_launcher_width'] ) ? (int) $opts['mobile_launcher_width'] : 140,
				'h'        => ! empty( $opts['mobile_launcher_height'] ) ? (int) $opts['mobile_launcher_height'] : 48,
				'fw'       => $mobile_fw,
				'fh'       => $mobile_fh,
			),
		);

		if ( $is_centerstage ) {
			$css .= $this->generate_centerstage_responsive_css( $opts, $devices );
		} else {
			$css .= $this->generate_morph_responsive_css( $devices );
		}

		return $css;
	}

	/**
	 * Generate responsive CSS for morph (side pill) mode.
	 *
	 * @param array $devices Per-device launcher settings.
	 * @return string CSS.
	 */
	private function generate_morph_responsive_css( $devices ) {
		$css = '';
		$sel = '.niwa-widget .niwa-morph';
		$css .= sprintf(
			'%s{--niwa-mobile-flyout-width:%dvw;--niwa-mobile-flyout-height-fallback:%dvh;--niwa-mobile-flyout-height:%ddvh;}',
			$sel,
			$devices['mobile']['fw'],
			$devices['mobile']['fh'],
			$devices['mobile']['fh']
		);

		// Desktop (default — no media query).
		$css .= $this->build_position_rule( $sel, $devices['desktop'] );
		$css .= sprintf(
			'%s.niwa-morph--open{width:%dpx;height:%dpx;max-height:var(--niwa-flyout-max-height,85vh);}',
			$sel,
			$devices['desktop']['fw'],
			$devices['desktop']['fh']
		);

		// Tablet: 481px – 1024px.
		$css .= '@media(min-width:481px) and (max-width:1024px){';
		$css .= $this->build_position_rule( $sel, $devices['tablet'] );
		$css .= sprintf(
			'%s.niwa-morph--open{width:%dpx;height:%dpx;max-height:var(--niwa-flyout-max-height,85vh);}',
			$sel,
			$devices['tablet']['fw'],
			$devices['tablet']['fh']
		);
		$css .= '}';

		// Mobile: ≤ 480px.
		$css .= '@media(max-width:480px){';
		$css .= $this->build_position_rule( $sel, $devices['mobile'] );
		$css .= '}';

		return $css;
	}

	/**
	 * Generate responsive CSS for centerstage mode.
	 *
	 * Centerstage is always horizontally centered — only vertical offset and
	 * pill/flyout dimensions change per breakpoint.
	 *
	 * @param array $opts    Plugin options.
	 * @param array $devices Per-device launcher settings.
	 * @return string CSS.
	 */
	private function generate_centerstage_responsive_css( $opts, $devices ) {
		$css = '';
		$sel = '.niwa-widget .niwa-center';

		// Per-device centerstage pill widths / heights.
		// Use !empty() to guard against empty-string admin values → (int) '' = 0.
		$base_cs_w    = ! empty( $opts['centerstage_pill_width'] ) ? (int) $opts['centerstage_pill_width'] : 224;
		$base_cs_h    = ! empty( $opts['centerstage_pill_height'] ) ? (int) $opts['centerstage_pill_height'] : 48;
		$cs_desktop_w = ! empty( $opts['desktop_centerstage_pill_width'] ) ? (int) $opts['desktop_centerstage_pill_width'] : $base_cs_w;
		$cs_desktop_h = ! empty( $opts['desktop_centerstage_pill_height'] ) ? (int) $opts['desktop_centerstage_pill_height'] : $base_cs_h;
		$cs_tablet_w  = ! empty( $opts['tablet_centerstage_pill_width'] ) ? (int) $opts['tablet_centerstage_pill_width'] : min( $base_cs_w, 212 );
		$cs_tablet_h  = ! empty( $opts['tablet_centerstage_pill_height'] ) ? (int) $opts['tablet_centerstage_pill_height'] : $base_cs_h;
		$cs_mobile_w  = ! empty( $opts['mobile_centerstage_pill_width'] ) ? (int) $opts['mobile_centerstage_pill_width'] : min( $base_cs_w, 196 );
		$cs_mobile_h  = ! empty( $opts['mobile_centerstage_pill_height'] ) ? (int) $opts['mobile_centerstage_pill_height'] : $base_cs_h;

		$cs_desktop_open_w = ! empty( $opts['desktop_centerstage_morph_width'] ) ? max( (int) $opts['desktop_centerstage_morph_width'], 200 ) : 494;
		$cs_desktop_open_h = ! empty( $opts['desktop_centerstage_morph_height'] ) ? max( (int) $opts['desktop_centerstage_morph_height'], 200 ) : 680;
		$cs_tablet_open_w  = ! empty( $opts['tablet_centerstage_morph_width'] ) ? max( (int) $opts['tablet_centerstage_morph_width'], 200 ) : 494;
		$cs_tablet_open_h  = ! empty( $opts['tablet_centerstage_morph_height'] ) ? max( (int) $opts['tablet_centerstage_morph_height'], 200 ) : 560;

		// Desktop (default).
		$css .= sprintf(
			'%s{bottom:%dpx;width:%dpx;height:%dpx;}',
			$sel,
			$devices['desktop']['y'],
			$cs_desktop_w,
			$cs_desktop_h
		);
		$css .= sprintf(
			'%s.niwa-center--open{width:%dpx;height:%dpx;max-height:var(--niwa-flyout-max-height,85vh);}',
			$sel,
			$cs_desktop_open_w,
			$cs_desktop_open_h
		);

		// Tablet.
		$css .= sprintf(
			'@media(min-width:481px) and (max-width:1024px){%s{bottom:%dpx;width:%dpx;height:%dpx;}%s.niwa-center--open{width:%dpx;height:%dpx;max-height:var(--niwa-flyout-max-height,85vh);}}',
			$sel,
			$devices['tablet']['y'],
			$cs_tablet_w,
			$cs_tablet_h,
			$sel,
			$cs_tablet_open_w,
			$cs_tablet_open_h
		);

		// Mobile — use general flyout width (matches original responsive CSS).
		$css .= sprintf(
			'@media(max-width:480px){%s{bottom:%dpx;width:%dpx;height:%dpx;}%s.niwa-center--open{width:%dpx;height:%dpx;max-height:var(--niwa-flyout-max-height,85vh);}}',
			$sel,
			$devices['mobile']['y'],
			$cs_mobile_w,
			$cs_mobile_h,
			$sel,
			$devices['mobile']['fw'],
			$devices['mobile']['fh']
		);

		return $css;
	}

	/**
	 * Build a CSS rule that positions the morph container at a given corner.
	 *
	 * Explicitly sets all four edges (top/right/bottom/left) so that
	 * media-query overrides cleanly reset the previous breakpoint's position.
	 *
	 * @param string $selector CSS selector.
	 * @param array  $config   Position config with keys: position, x, y, w, h.
	 * @return string Single CSS rule.
	 */
	private function build_position_rule( $selector, $config ) {
		$pos = $config['position'];
		$x   = $config['x'];
		$y   = $config['y'];
		$w   = $config['w'];
		$h   = $config['h'];

		switch ( $pos ) {
			case 'bl':
				$edges = sprintf( 'bottom:%dpx;left:%dpx;top:auto;right:auto;', $y, $x );
				break;
			case 'tr':
				$edges = sprintf( 'top:%dpx;right:%dpx;bottom:auto;left:auto;', $y, $x );
				break;
			case 'tl':
				$edges = sprintf( 'top:%dpx;left:%dpx;bottom:auto;right:auto;', $y, $x );
				break;
			case 'br':
			default:
				$edges = sprintf( 'bottom:%dpx;right:%dpx;top:auto;left:auto;', $y, $x );
				break;
		}

		// `--niwa-launcher-width` drives the collapsed pill's `min-width` so
		// content-based sizing (in niwa-morph.css) can expand the pill to fit
		// the full launcher text without ever rendering at the default width
		// first. Height stays fixed so the pill bar keeps its 48px baseline.
		return sprintf(
			'%s{%s--niwa-launcher-width:%dpx;--niwa-launcher-height:%dpx;height:var(--niwa-launcher-height, %dpx);}',
			$selector,
			$edges,
			$w,
			$h,
			$h
		);
	}
}
