<?php
/**
 * Chat Renderer
 *
 * Renders the chat widget HTML for frontend display.
 * Uses morph-based layout with three variants: right, left, centerstage.
 *
 * @package Niwa_AI
 * @since   0.2.0
 */

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

class Niwa_AI_Chat_Renderer {

	/**
	 * Plugin instance reference.
	 *
	 * @var Niwa_AI_Plugin
	 */
	private $plugin;

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

	/**
	 * Render floating morph widget into wp_footer.
	 *
	 * Outputs a single .niwa-morph container that morphs from pill to full chat,
	 * or a .niwa-center container for centerstage mode.
	 */
	public function render_flyout() {
		if ( $this->is_runtime_conflict_context() ) {
			return;
		}

		$opts    = $this->get_merged_opts();
		$enabled = ! isset( $opts['enable_flyout'] ) || intval( $opts['enable_flyout'] ) === 1;

		if ( ! $enabled ) {
			return;
		}

		if ( $this->should_hide_niwa_on_current_page( $opts ) ) {
			return;
		}

		$launcher       = $this->get_launcher_settings( $opts );
		$labels         = $this->get_ui_copy( $opts );
		$is_centerstage = ! empty( $opts['centerstage_mode'] );

		$theme_mode = $opts['chat_theme_mode'] ?? 'neutral';

		$placeholder    = isset( $opts['message_placeholder'] ) && '' !== trim( (string) $opts['message_placeholder'] )
			? $opts['message_placeholder']
			: $labels['placeholder'];

		$banner_message = isset( $opts['banner_message'] ) && '' !== trim( (string) $opts['banner_message'] )
			? $opts['banner_message']
			: $labels['banner_message'];

		$use_default_launcher_settings = ! empty( $opts['launcher_use_default_settings'] );
		$has_custom_launcher_text      = ! $use_default_launcher_settings && isset( $opts['launcher_label'] ) && '' !== trim( (string) $opts['launcher_label'] );
		$launcher_text                 = $has_custom_launcher_text
			? $opts['launcher_label']
			: $labels['launcher_text'];
		$launcher_mobile_text          = $use_default_launcher_settings ? 'Niwa AI' : $launcher_text;
		$initial_launcher_text         = wp_is_mobile() ? $launcher_mobile_text : $launcher_text;
		$launcher_text_color = ! $use_default_launcher_settings && ! empty( $opts['launcher_label_color'] )
			? sanitize_hex_color( $opts['launcher_label_color'] )
			: '';

		if ( $is_centerstage ) {
			$this->render_centerstage( $opts, $labels, $theme_mode, $placeholder, $banner_message );
		} else {
			$this->render_morph(
				$opts,
				$labels,
				$launcher,
				$theme_mode,
				$placeholder,
				$banner_message,
				$initial_launcher_text,
				$launcher_text_color,
				$launcher_text,
				$launcher_mobile_text,
				$has_custom_launcher_text
			);
		}
	}

	/**
	 * Render right/left morph layout.
	 *
	 * @param array  $opts           Plugin options.
	 * @param array  $labels         UI copy.
	 * @param array  $launcher       Launcher positioning settings.
	 * @param string $theme_mode     Theme mode (neutral|light|dark|auto).
	 * @param string $placeholder    Input placeholder text.
	 * @param string $banner_message Welcome banner message.
	 * @param string $launcher_text         Initial pill label text.
	 * @param string $launcher_text_color   Pill label text color override.
	 * @param string $launcher_desktop_text Desktop pill label text.
	 * @param string $launcher_mobile_text  Mobile pill label text.
	 * @param bool   $launcher_auto_fit     Whether the frontend label should shrink to fit custom text.
	 */
	private function render_morph( $opts, $labels, $launcher, $theme_mode, $placeholder, $banner_message, $launcher_text, $launcher_text_color, $launcher_desktop_text, $launcher_mobile_text, $launcher_auto_fit ) {
		$position = $launcher['position'];
		$is_left  = ( 'bl' === $position );

		$morph_classes = array( 'niwa-morph' );
		if ( $is_left ) {
			$morph_classes[] = 'niwa-morph--left';
		}

		$wordmark_url = NIWA_AI_URL . 'assets/img/niwa-wordmark.svg';
		$logo_url     = $this->get_frontend_avatar_url( $opts );

		// Element IDs vary by position so JS morph-controller can distinguish left/right.
		$suffix           = $is_left ? 'Left' : '';
		$id_morph         = 'niwaMorph' . $suffix;
		$id_chathead      = 'niwaChathead' . $suffix;
		$id_chathead_input = 'niwaChatheadInput' . $suffix;
		$id_chathead_send = 'niwaChatheadSend' . $suffix;
		$id_badge         = 'niwaBadge' . $suffix;
		$label_style      = $launcher_text_color ? ' style="color:' . esc_attr( $launcher_text_color ) . ';"' : '';

		// Pre-calculate launcher pill width in PHP so the pill paints at its
		// final size on first render (zero flash from a default width to a
		// measured width). The width is derived from the launcher label text
		// that is actually rendered (desktop vs mobile is picked upstream via
		// wp_is_mobile()), so SSR output and the DOM agree on the first paint.
		$launcher_width_px = self::estimate_launcher_pill_width( $launcher_text );
		$launcher_height_px = max( 32, intval( isset( $launcher['h'] ) ? $launcher['h'] : 48 ) );
		$morph_style      = sprintf(
			'--niwa-launcher-width:%dpx;--niwa-launcher-height:%dpx;',
			$launcher_width_px,
			$launcher_height_px
		);

		?>
		<div class="niwa-widget niwa-widget--floating niwa-widget--frontend-shell" data-niwa-theme="<?php echo esc_attr( $theme_mode ); ?>">
			<div class="<?php echo esc_attr( implode( ' ', $morph_classes ) ); ?>"
				id="<?php echo esc_attr( $id_morph ); ?>"
				style="<?php echo esc_attr( $morph_style ); ?>"
				aria-label="<?php echo esc_attr( $labels['open_label'] ); ?>">

				<!-- Chat content (hidden until open) -->
				<div class="niwa-morph__chat">
					<div class="niwa-morph__header">
						<img src="<?php echo esc_url( $wordmark_url ); ?>"
							alt="Niwa" class="niwa-morph__wordmark" width="40" height="10" />
						<button class="niwa-morph__close<?php echo $is_left ? ' niwa-morph__close--left' : ''; ?>" aria-label="<?php echo esc_attr( $labels['close_label'] ); ?>">
							<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
								stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
								<line x1="18" y1="6" x2="6" y2="18"></line>
								<line x1="6" y1="6" x2="18" y2="18"></line>
							</svg>
						</button>
					</div>
					<div class="niwa-morph__body" role="log" aria-live="polite" aria-relevant="additions">
						<!-- Welcome section -->
						<div class="niwa-welcome">
							<div class="niwa-welcome__icon" aria-hidden="true">
								<img src="<?php echo esc_url( $logo_url ); ?>"
									alt="" width="64" height="64" />
							</div>
							<div class="niwa-welcome__text">
								<?php echo esc_html( $banner_message ); ?>
							</div>
						</div>
						<!-- Messages inserted here by JS -->
					</div>
				</div>

				<!-- Pill (always visible at bottom) -->
				<div class="niwa-morph__pill"
					id="<?php echo esc_attr( $id_chathead ); ?>"
					role="button"
					tabindex="0"
					data-launcher-text="<?php echo esc_attr( $launcher_desktop_text ); ?>"
					data-launcher-text-mobile="<?php echo esc_attr( $launcher_mobile_text ); ?>"
					data-launcher-width-desktop="<?php echo esc_attr( self::estimate_launcher_pill_width( $launcher_desktop_text ) ); ?>"
					data-launcher-width-mobile="<?php echo esc_attr( self::estimate_launcher_pill_width( $launcher_mobile_text ) ); ?>"
					data-launcher-auto-fit="<?php echo $launcher_auto_fit ? '1' : '0'; ?>">
					<span class="niwa-morph__orb" aria-hidden="true">
						<span class="niwa-orb"></span>
					</span>
					<span class="niwa-morph__label"<?php echo $label_style; ?>><?php echo esc_html( $launcher_text ); ?></span>
					<span class="niwa-morph__badge" id="<?php echo esc_attr( $id_badge ); ?>"></span>

					<!-- Input (hidden until expanded) -->
					<div class="niwa-morph__input">
						<textarea class="niwa-morph__textarea"
							id="<?php echo esc_attr( $id_chathead_input ); ?>"
							name="message"
							placeholder="<?php echo esc_attr( $placeholder ); ?>"
							aria-label="<?php echo esc_attr( $placeholder ); ?>"
							rows="1"></textarea>
						<button class="niwa-morph__send niwa-morph__send--disabled"
							id="<?php echo esc_attr( $id_chathead_send ); ?>"
							type="button"
							aria-label="<?php esc_attr_e( 'Send', 'niwa-ai' ); ?>">
							<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
								<path d="M16 22L16 10M16 10L11 15M16 10L21 15" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
							</svg>
						</button>
					</div>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Render centerstage layout.
	 *
	 * @param array  $opts           Plugin options.
	 * @param array  $labels         UI copy.
	 * @param string $theme_mode     Theme mode (neutral|light|dark|auto).
	 * @param string $placeholder    Input placeholder text.
	 * @param string $banner_message Welcome banner message.
	 */
	private function render_centerstage( $opts, $labels, $theme_mode, $placeholder, $banner_message ) {
		$pill_text = isset( $opts['centerstage_pill_text'] ) && '' !== trim( (string) $opts['centerstage_pill_text'] )
			? $opts['centerstage_pill_text']
			: $labels['launcher_text'];

		$wordmark_url = NIWA_AI_URL . 'assets/img/niwa-wordmark.svg';
		$logo_url     = $this->get_frontend_avatar_url( $opts );

		?>
		<div class="niwa-widget niwa-widget--floating niwa-widget--frontend-shell" data-niwa-theme="<?php echo esc_attr( $theme_mode ); ?>">
			<div class="niwa-center"
				id="niwaCenter"
				aria-label="<?php echo esc_attr( $labels['open_label'] ); ?>">

				<!-- Chat content (hidden until expanded) -->
				<div class="niwa-center__chat">
					<div class="niwa-center__header">
						<img class="niwa-morph__wordmark" src="<?php echo esc_url( $wordmark_url ); ?>" alt="Niwa" width="40" height="10">
						<button class="niwa-center__close" aria-label="<?php echo esc_attr( $labels['close_label'] ); ?>">
							<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
								stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
								<line x1="18" y1="6" x2="6" y2="18"></line>
								<line x1="6" y1="6" x2="18" y2="18"></line>
							</svg>
						</button>
					</div>
					<div class="niwa-center__body" role="log" aria-live="polite" aria-relevant="additions">
						<!-- Welcome section -->
						<div class="niwa-welcome">
							<div class="niwa-welcome__icon" aria-hidden="true">
								<img src="<?php echo esc_url( $logo_url ); ?>"
									alt="" width="64" height="64" />
							</div>
							<div class="niwa-welcome__text">
								<?php echo esc_html( $banner_message ); ?>
							</div>
						</div>
						<!-- Messages inserted here by JS -->
					</div>
				</div>

				<!-- Center pill (always at bottom) -->
				<div class="niwa-center__pill" id="niwaCenterPill" role="button" tabindex="0">
					<span class="niwa-center__badge" id="niwaCenterBadge"></span>
					<span class="niwa-morph__orb" aria-hidden="true">
						<span class="niwa-orb"></span>
					</span>
					<span class="niwa-center__label"><?php echo esc_html( $pill_text ); ?></span>

					<div class="niwa-center__input">
						<textarea class="niwa-center__textarea"
							id="niwaCenterInput"
							name="message"
							placeholder="<?php echo esc_attr( $placeholder ); ?>"
							aria-label="<?php echo esc_attr( $placeholder ); ?>"
							rows="1"></textarea>
					</div>
					<button class="niwa-center__send niwa-center__send--disabled"
						id="niwaCenterSend"
						type="button"
						aria-label="<?php esc_attr_e( 'Send', 'niwa-ai' ); ?>">
						<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
							<path d="M16 22L16 10M16 10L11 15M16 10L21 15" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
						</svg>
					</button>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Render chat shortcode.
	 *
	 * @return string HTML output.
	 */
	public function render_shortcode() {
		if ( $this->is_runtime_conflict_context() ) {
			return '';
		}

		$opts   = $this->get_merged_opts();
		if ( $this->should_hide_niwa_on_current_page( $opts ) ) {
			return '';
		}

		$labels = $this->get_ui_copy( $opts );

		$theme_mode = $opts['chat_theme_mode'] ?? 'neutral';

		$placeholder = isset( $opts['message_placeholder'] ) && '' !== trim( (string) $opts['message_placeholder'] )
			? $opts['message_placeholder']
			: __( 'Ask anything...', 'niwa-ai' );

		$banner_message = isset( $opts['banner_message'] ) && '' !== trim( (string) $opts['banner_message'] )
			? $opts['banner_message']
			: $labels['banner_message'];

		$logo_url = $this->get_frontend_avatar_url( $opts );

		ob_start();
		?>
		<div class="niwa-widget niwa-widget--inline" data-niwa-theme="<?php echo esc_attr( $theme_mode ); ?>">
			<div class="niwa-shortcode">
				<div class="niwa-shortcode__body" role="log" aria-live="polite" aria-relevant="additions">
					<!-- Welcome section -->
					<div class="niwa-welcome">
						<div class="niwa-welcome__icon" aria-hidden="true">
							<img src="<?php echo esc_url( $logo_url ); ?>"
								alt="" width="64" height="64" />
						</div>
						<div class="niwa-welcome__text">
							<?php echo esc_html( $banner_message ); ?>
						</div>
					</div>
					<!-- Messages inserted here by JS -->
				</div>
				<div class="niwa-shortcode__input">
					<textarea class="niwa-morph__textarea"
						name="message"
						placeholder="<?php echo esc_attr( $placeholder ); ?>"
						aria-label="<?php echo esc_attr( $placeholder ); ?>"
						rows="1"></textarea>
					<button class="niwa-morph__send niwa-morph__send--disabled"
						type="button"
						aria-label="<?php esc_attr_e( 'Send', 'niwa-ai' ); ?>">
						<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
							<path d="M16 22L16 10M16 10L11 15M16 10L21 15" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
						</svg>
					</button>
				</div>
			</div>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * Render a button or internal link that opens the standalone scheduler.
	 *
	 * @param array       $atts    Shortcode attributes.
	 * @param string|null $content Optional shortcode content used as the label.
	 * @return string
	 */
	public function render_booking_shortcode( $atts = array(), $content = null ) {
		$copy = $this->get_ui_copy( $this->get_merged_opts() );
		$atts = shortcode_atts(
			array(
				'type'           => 'button',
				'label'          => '',
				'class'          => '',
				'service_id'     => '',
				'team_member_id' => '',
				'title'          => '',
			),
			is_array( $atts ) ? $atts : array(),
			'niwa_booking'
		);

		$label = trim( wp_strip_all_tags( null !== $content && '' !== trim( $content ) ? $content : $atts['label'] ) );
		if ( '' === $label ) {
			$label = $copy['booking_label'];
		}

		$custom_classes = array_filter( array_map( 'sanitize_html_class', preg_split( '/\s+/', trim( (string) $atts['class'] ) ) ?: array() ) );
		$is_link        = 'link' === sanitize_key( (string) $atts['type'] );
		$classes        = array_merge( array( 'niwa-booking-trigger', 'niwa-booking-trigger--' . ( $is_link ? 'link' : 'button' ) ), $custom_classes );
		$attributes     = array(
			'class'                         => implode( ' ', $classes ),
			'data-niwa-booking'             => '1',
			'data-niwa-booking-service'     => sanitize_text_field( (string) $atts['service_id'] ),
			'data-niwa-booking-team-member' => sanitize_text_field( (string) $atts['team_member_id'] ),
			'data-niwa-booking-title'       => sanitize_text_field( (string) $atts['title'] ),
			'aria-haspopup'                 => 'dialog',
		);

		$html_attributes = '';
		foreach ( $attributes as $name => $value ) {
			if ( '' !== $value ) {
				$html_attributes .= ' ' . $name . '="' . esc_attr( $value ) . '"';
			}
		}

		if ( $is_link ) {
			return '<a href="#niwa-booking"' . $html_attributes . '>' . esc_html( $label ) . '</a>';
		}

		return '<button type="button"' . $html_attributes . '>' . esc_html( $label ) . '</button>';
	}

	/**
	 * Skip rendering in editor/preview/admin runtimes where the widget causes
	 * UI conflicts and is not intended to be interactive.
	 *
	 * @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() || 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;
	}

	/**
	 * Decide whether the frontend Niwa widget should be hidden on this request.
	 *
	 * @param array $opts UI settings.
	 * @return bool
	 */
	private function should_hide_niwa_on_current_page( $opts ) {
		$opts = is_array( $opts ) ? $opts : array();

		if ( ! empty( $opts['hide_niwa_on_blog_pages'] ) && $this->is_blog_request() ) {
			return true;
		}

		if ( empty( $opts['hide_niwa_on_custom_pages'] ) ) {
			return false;
		}

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

		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) || empty( $rule['enabled'] ) || empty( $rule['url'] ) ) {
				continue;
			}

			if ( $this->page_rule_matches_current_url( $rule ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Blog request coverage for posts, the blog index, and blog tax archives.
	 *
	 * @return bool
	 */
	private function is_blog_request() {
		if ( function_exists( 'is_home' ) && is_home() ) {
			return true;
		}

		if ( function_exists( 'is_singular' ) && is_singular( 'post' ) ) {
			return true;
		}

		foreach ( array( 'is_category', 'is_tag', 'is_date', 'is_author' ) as $conditional ) {
			if ( function_exists( $conditional ) && call_user_func( $conditional ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Match one saved custom-page rule against the current URL/path.
	 *
	 * @param array $rule Custom page rule.
	 * @return bool
	 */
	private function page_rule_matches_current_url( $rule ) {
		$pattern = isset( $rule['url'] ) ? trim( (string) $rule['url'] ) : '';
		if ( '' === $pattern ) {
			return false;
		}

		$current_url  = $this->get_current_request_url();
		$current_path = $this->get_current_request_path_with_query( $current_url );
		$match_type   = isset( $rule['match_type'] ) ? strtolower( (string) $rule['match_type'] ) : 'contains';

		if ( 'equals' === $match_type ) {
			$normalized_pattern = $this->normalize_url_match_value( $pattern );
			return $this->normalize_url_match_value( $current_url ) === $normalized_pattern
				|| $this->normalize_url_match_value( $current_path ) === $normalized_pattern;
		}

		if ( 'regex' === $match_type ) {
			$result = @preg_match( '~' . str_replace( '~', '\~', $pattern ) . '~i', $current_url . "\n" . $current_path );
			return 1 === $result;
		}

		return false !== strpos( $current_url, $pattern ) || false !== strpos( $current_path, $pattern );
	}

	/**
	 * Build the current request URL from server globals.
	 *
	 * @return string
	 */
	private function get_current_request_url() {
		$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
		$uri  = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : '/';
		$uri  = '/' . ltrim( $uri, '/' );

		if ( '' === $host ) {
			return home_url( $uri );
		}

		$is_ssl = function_exists( 'is_ssl' ) && is_ssl();
		return ( $is_ssl ? 'https://' : 'http://' ) . $host . $uri;
	}

	/**
	 * Extract path plus query from a full URL.
	 *
	 * @param string $url Full URL.
	 * @return string
	 */
	private function get_current_request_path_with_query( $url ) {
		$parts = wp_parse_url( $url );
		$path  = isset( $parts['path'] ) && '' !== $parts['path'] ? $parts['path'] : '/';
		if ( isset( $parts['query'] ) && '' !== $parts['query'] ) {
			$path .= '?' . $parts['query'];
		}
		return $path;
	}

	/**
	 * Normalize URL/path values for equals matching.
	 *
	 * @param string $value URL or path.
	 * @return string
	 */
	private function normalize_url_match_value( $value ) {
		return rtrim( (string) $value, '/' );
	}

	/**
	 * Get launcher positioning settings.
	 *
	 * @param array $opts Plugin options.
	 * @return array Launcher settings.
	 */
	private function get_launcher_settings( $opts ) {
		$position = ! empty( $opts['desktop_launcher_position'] )
			? $opts['desktop_launcher_position']
			: ( $opts['position'] ?? 'br' );

		$x = ! empty( $opts['desktop_launcher_x'] )
			? $opts['desktop_launcher_x']
			: ( $opts['offset_x'] ?? 24 );

		$y = ! empty( $opts['desktop_launcher_y'] )
			? $opts['desktop_launcher_y']
			: ( $opts['offset_y'] ?? 24 );

		return array(
			'position' => $position,
			'x'        => $x,
			'y'        => $y,
		);
	}

	private function get_frontend_avatar_url( $opts ) {
		$opts = is_array( $opts ) ? $opts : array();
		$url  = isset( $opts['frontend_avatar_url'] ) ? esc_url_raw( (string) $opts['frontend_avatar_url'] ) : '';
		$default_url = defined( 'NIWA_AI_DEFAULT_FRONTEND_AVATAR_URL' )
			? NIWA_AI_DEFAULT_FRONTEND_AVATAR_URL
			: NIWA_AI_URL . 'assets/img/niwa-admin-personal-assistant.png';

		return '' !== trim( $url ) ? $url : $default_url;
	}

	/**
	 * Get lightweight UI copy used by the renderer.
	 *
	 * @param array $opts Plugin options.
	 * @return array
	 */
	private function get_ui_copy( $opts ) {
		$lang = isset( $opts['ui_language'] ) ? (string) $opts['ui_language'] : 'en';
		$copy = array(
			'launcher_text'   => 'Ask Niwa AI',
			'booking_label'   => 'Book now',
			'placeholder'     => 'Ask Niwa AI...',
			'open_label'      => 'Open chat',
			'close_label'     => 'Close chat',
			'welcome_heading' => 'How can I help?',
			'welcome_copy'    => 'Ask about products, orders, support or anything else on this site.',
			'banner_message'  => 'How can I help?',
			'greeting'        => Niwa_AI_Constants::FRONTEND_DEFAULT_GREETING,
		);

		$translations = array(
			'sr' => array(
				'launcher_text'   => 'Pitaj Niwa AI',
				'booking_label'   => 'Zakažite termin',
				'placeholder'     => 'Pitaj Niwa AI...',
				'open_label'      => 'Otvori chat',
				'close_label'     => 'Zatvori chat',
				'welcome_heading' => 'Kako mogu da pomognem?',
				'welcome_copy'    => 'Pitaj za proizvode, porudzbine, podrsku ili bilo sta drugo na ovom sajtu.',
				'banner_message'  => 'Kako mogu da pomognem?',
			),
			'hr' => array(
				'launcher_text'   => 'Pitaj Niwa AI',
				'booking_label'   => 'Rezervirajte termin',
				'placeholder'     => 'Pitaj Niwa AI...',
				'open_label'      => 'Otvori chat',
				'close_label'     => 'Zatvori chat',
				'welcome_heading' => 'Kako mogu pomoci?',
				'welcome_copy'    => 'Pitaj za proizvode, narudzbe, podrsku ili bilo sto drugo na ovoj stranici.',
				'banner_message'  => 'Kako mogu pomoci?',
			),
			'de' => array(
				'launcher_text'   => 'Frage Niwa AI',
				'booking_label'   => 'Termin buchen',
				'placeholder'     => 'Frage Niwa AI...',
				'open_label'      => 'Chat oeffnen',
				'close_label'     => 'Chat schliessen',
				'welcome_heading' => 'Wie kann ich helfen?',
				'welcome_copy'    => 'Frag nach Produkten, Bestellungen, Support oder allem anderen auf dieser Website.',
				'banner_message'  => 'Wie kann ich helfen?',
			),
			'es' => array(
				'launcher_text'   => 'Pregunta a Niwa AI',
				'booking_label'   => 'Reservar ahora',
				'placeholder'     => 'Pregunta a Niwa AI...',
				'open_label'      => 'Abrir chat',
				'close_label'     => 'Cerrar chat',
				'welcome_heading' => 'Como puedo ayudarte?',
				'welcome_copy'    => 'Pregunta por productos, pedidos, soporte o cualquier otra cosa de este sitio.',
				'banner_message'  => 'Como puedo ayudarte?',
			),
			'fr' => array(
				'launcher_text'   => 'Demander a Niwa AI',
				'booking_label'   => 'Réserver',
				'placeholder'     => 'Demander a Niwa AI...',
				'open_label'      => 'Ouvrir le chat',
				'close_label'     => 'Fermer le chat',
				'welcome_heading' => 'Comment puis-je aider ?',
				'welcome_copy'    => 'Pose une question sur les produits, les commandes, le support ou tout autre sujet du site.',
				'banner_message'  => 'Comment puis-je aider ?',
			),
		);

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

		return $copy;
	}

	/**
	 * 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() );
		$opts    = array_merge(
			is_array( $general ) ? $general : array(),
			is_array( $ui ) ? $ui : array()
		);

		$defaults = array(
			'enable_flyout'                => 1,
			'hide_niwa_on_blog_pages'      => 0,
			'hide_niwa_on_custom_pages'    => 0,
			'hide_niwa_custom_pages'       => array(),
			'centerstage_mode'             => 0,
			'centerstage_pill_width'       => 224,
			'centerstage_pill_height'      => 48,
			'centerstage_pill_text'        => 'Ask Niwa',
			'centerstage_pill_text_color'  => '#374151',
			'centerstage_pill_arrow_color' => '#374151',
			'launcher_label'               => '',
			'launcher_use_default_settings' => 1,
			'launcher_label_color'         => '',
			'launcher_icon'                => '',
			'desktop_launcher_position'    => 'br',
			'desktop_launcher_x'           => 24,
			'desktop_launcher_y'           => 24,
			'desktop_width'                => 380,
			'desktop_height'               => 680,
			'message_placeholder'          => '',
			'chat_theme_mode'              => 'neutral',
			'banner_message'               => '',
			'greeting_text'                => '',
		);

		return wp_parse_args( $opts, $defaults );
	}

	/**
	 * Estimate the final pixel width of the launcher pill for a given label.
	 *
	 * The goal is to emit a definitive `--niwa-launcher-width` in SSR so the
	 * pill paints at its final size on first frame (no snap from a default
	 * width to a measured width once JS runs).
	 *
	 * Formula (matches current pill layout in niwa-morph.css):
	 *   pill_width ≈ padding_left + orb + gap + text_width + label_pad_right + padding_right
	 *   text_width ≈ char_count × font_size × CHAR_WIDTH_FACTOR
	 *
	 * CHAR_WIDTH_FACTOR (0.48) was empirically calibrated to the default
	 * stack at 14px — close to actual rendering for mixed Latin text while
	 * leaving a small safety margin to avoid clipping.
	 *
	 * @param string $text Launcher label text.
	 * @return int Estimated pill width in pixels.
	 */
	public static function estimate_launcher_pill_width( $text ) {
		$text = is_string( $text ) ? $text : '';

		// Count characters (Unicode-aware). Fall back to strlen if mbstring unavailable.
		if ( function_exists( 'mb_strlen' ) ) {
			$char_count = mb_strlen( $text, 'UTF-8' );
		} else {
			$char_count = strlen( $text );
		}

		$font_size          = 14;    // --niwa-font-size-md
		$char_width_factor  = 0.48;  // Empirically calibrated.
		$orb_width          = 24;    // .niwa-morph__orb width
		$gap                = 8;     // --niwa-space-sm between orb and label
		$label_padding_right = 4;    // --niwa-space-xs on the label
		$padding_horizontal = 32;    // .niwa-morph__pill padding: 0 var(--niwa-space-md) => 16*2
		// Safety buffer at the end of the text. Because `.niwa-morph__label`
		// uses `overflow: clip`, ANY sub-pixel underestimate clips the last
		// letter. Sub-pixel rounding, kerning, and per-glyph width variance
		// (wide glyphs like "m", "w", Cyrillic, diacritics) can push the
		// real label width a few pixels beyond a chars × avg-width estimate.
		$safety_buffer      = 12;

		$text_width = (int) ceil( $char_count * $font_size * $char_width_factor );

		return $padding_horizontal + $orb_width + $gap + $text_width + $label_padding_right + $safety_buffer;
	}
}
