<?php
/**
 * SiteStaffr Button Widget
 *
 * Handles the [sitestaffr_button] shortcode for inline button widget.
 * Renders a customizable button with full Elementor-style styling options.
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      0.11.75
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

/**
 * SiteStaffr Button Widget class.
 *
 * Provides an inline button widget that can be placed anywhere on the site.
 * Supports extensive styling customization via shortcode attributes.
 *
 * @since      0.11.75
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @author     SiteStaffr
 */
class SiteStaffr_Button_Widget {

	/**
	 * Initialize the button widget.
	 *
	 * Registers the shortcode and enqueues assets.
	 *
	 * @since    0.11.75
	 */
	public function __construct() {
		// Register shortcode
		add_shortcode( 'sitestaffr_button', array( $this, 'render_button' ) );

		// Shared handlers are registered once by router (first owner wins).
		SiteStaffr_Public_Ajax_Router::register_shared_handlers( $this );

		// Explicit button-only aliases route to button handler ownership.
		SiteStaffr_Public_Ajax_Router::register_button_alias_handlers( $this );
	}

	/**
	 * Get API secret for HMAC signing.
	 *
	 * Prefers per-site secret (registered sites), falls back to global secret.
	 * This supports gradual migration from global to per-site secrets.
	 *
	 * @since    0.13.72
	 * @return   string    API secret for HMAC signing.
	 */
	private function get_api_secret() {
		// Try per-site secret first (v0.13.72+)
		$site_api_secret = get_option( 'sitestaffr_site_api_secret' );
		if ( ! empty( $site_api_secret ) ) {
			return $site_api_secret;
		}

		// Fall back to global secret (backward compatibility)
		$global_api_secret = get_option( 'sitestaffr_api_secret' );
		if ( ! empty( $global_api_secret ) ) {
			return $global_api_secret;
		}

		// No secret available (should not happen after activation)
		return '';
	}

	/**
	 * Build runtime custom instructions passed to middleware for Realtime sessions.
	 *
	 * Adds non-negotiable caller-name handling rules while preserving business-provided
	 * custom instructions from plugin settings.
	 *
	 * @since 0.13.85
	 * @param array $saved_settings Plugin settings array.
	 * @return string Combined instruction text.
	 */
	private function get_realtime_custom_instructions( $saved_settings ) {
		return isset( $saved_settings['custom_instructions'] ) ? sanitize_textarea_field( $saved_settings['custom_instructions'] ) : '';
	}


	/**
	 * Render the button widget.
	 *
	 * Shortcode: [sitestaffr_button]
	 *
	 * Full styling options available (see implementation plan Section 4).
	 *
	 * @since    0.11.75
	 * @param    array    $atts    Shortcode attributes.
	 * @return   string            Button HTML.
	 */
	public function render_button( $atts ) {
		// Get saved widget settings
		$widget_settings = SiteStaffr_Widget_Settings_Provider::get_widget_settings();
		$default_button_border_radius = $this->get_button_border_radius_default( $widget_settings );

		// Parse attributes with defaults (use saved settings if available)
		$atts = shortcode_atts(
			array(
				// Text
				'text'             => isset( $widget_settings['button_text'] ) ? $widget_settings['button_text'] : 'Contact Us',
				'font_size'        => isset( $widget_settings['button_font_size'] ) ? $widget_settings['button_font_size'] : '16px',
				'font_weight'      => isset( $widget_settings['button_font_weight'] ) ? $widget_settings['button_font_weight'] : '600',
				'text_color'       => isset( $widget_settings['button_text_color'] ) ? $widget_settings['button_text_color'] : '#ffffff',
				'text_transform'   => isset( $widget_settings['button_text_transform'] ) ? $widget_settings['button_text_transform'] : 'none',

				// Background
				'background_color' => isset( $widget_settings['button_background_color'] ) ? $widget_settings['button_background_color'] : '#1FB6CC',
				'hover_background' => isset( $widget_settings['button_hover_background'] ) ? $widget_settings['button_hover_background'] : '#17A2B8',
				'gradient'         => isset( $widget_settings['button_gradient'] ) ? ( $widget_settings['button_gradient'] ? 'on' : 'off' ) : 'on',
				'gradient_end'     => isset( $widget_settings['button_gradient_end'] ) ? $widget_settings['button_gradient_end'] : '#10b981',

				// Border
				'border_radius'    => $default_button_border_radius,
				'border_width'     => isset( $widget_settings['button_border_width'] ) ? $widget_settings['button_border_width'] : '0',
				'border_color'     => isset( $widget_settings['button_border_color'] ) ? $widget_settings['button_border_color'] : '#1FB6CC',
				'border_style'     => isset( $widget_settings['button_border_style'] ) ? $widget_settings['button_border_style'] : 'solid',

				// Shadow
				'box_shadow'       => isset( $widget_settings['button_box_shadow'] ) ? ( $widget_settings['button_box_shadow'] ? 'on' : 'off' ) : 'on',
				'shadow_color'     => isset( $widget_settings['button_shadow_color'] ) ? $widget_settings['button_shadow_color'] : 'rgba(0,0,0,0.2)',
				'shadow_blur'      => isset( $widget_settings['button_shadow_blur'] ) ? $widget_settings['button_shadow_blur'] : '10px',
				'shadow_offset'    => isset( $widget_settings['button_shadow_offset'] ) ? $widget_settings['button_shadow_offset'] : '4px',

				// Size
				'padding_x'        => isset( $widget_settings['button_padding_x'] ) ? $widget_settings['button_padding_x'] : '24px',
				'padding_y'        => isset( $widget_settings['button_padding_y'] ) ? $widget_settings['button_padding_y'] : '12px',
				'full_width'       => isset( $widget_settings['button_full_width'] ) && $widget_settings['button_full_width'] ? 'on' : 'off',

				// Icon
				'icon'             => isset( $widget_settings['button_icon'] ) ? $widget_settings['button_icon'] : 'sitestaffr',
				'icon_position'    => isset( $widget_settings['button_icon_position'] ) ? $widget_settings['button_icon_position'] : 'left',
				'icon_size'        => isset( $widget_settings['button_icon_size'] ) ? $widget_settings['button_icon_size'] : '18px',
				'icon_color'       => isset( $widget_settings['button_icon_color'] ) ? $widget_settings['button_icon_color'] : '#ffffff',
				'hover_icon_color' => isset( $widget_settings['button_hover_icon_color'] ) ? $widget_settings['button_hover_icon_color'] : '',

				// Animation
				'hover_animation'  => isset( $widget_settings['button_hover_animation'] ) ? $widget_settings['button_hover_animation'] : 'none',

				// States
				'listening_text'   => isset( $widget_settings['button_listening_text'] ) ? $widget_settings['button_listening_text'] : 'Listening...',
				'listening_color'  => isset( $widget_settings['button_listening_color'] ) ? $widget_settings['button_listening_color'] : '#e74c3c',

				// Persona
				'persona'          => '',

				// Variant
				'variant'          => '',

				// Mode
				'mode'             => '',
				'sounds'           => 'on',
			),
			$atts,
			'sitestaffr_button'
		);

		// Sanitize all attributes
		$text             = sanitize_text_field( $atts['text'] );
		$font_size        = sanitize_text_field( $atts['font_size'] );
		$font_weight      = sanitize_text_field( $atts['font_weight'] );
		$text_color       = sanitize_hex_color( $atts['text_color'] );
		$text_transform   = sanitize_text_field( $atts['text_transform'] );
		$background_color = sanitize_hex_color( $atts['background_color'] );
		$hover_background = sanitize_hex_color( $atts['hover_background'] );
		$gradient         = sanitize_text_field( $atts['gradient'] );
		$gradient_end     = sanitize_hex_color( $atts['gradient_end'] );
		$border_radius    = $this->normalize_border_radius_shorthand( sanitize_text_field( $atts['border_radius'] ), $default_button_border_radius );
		$border_width     = sanitize_text_field( $atts['border_width'] );
		$border_color     = sanitize_hex_color( $atts['border_color'] );
		$border_style = sanitize_key( $atts['border_style'] );
		$valid_border_styles = array( 'none', 'solid', 'dashed', 'dotted', 'double' );
		if ( ! in_array( $border_style, $valid_border_styles, true ) ) {
			$border_style = 'solid';
		}
		$box_shadow       = sanitize_text_field( $atts['box_shadow'] );
		$shadow_color     = sanitize_text_field( $atts['shadow_color'] );
		$shadow_blur      = sanitize_text_field( $atts['shadow_blur'] );
		$shadow_offset    = sanitize_text_field( $atts['shadow_offset'] );
		$padding_x        = sanitize_text_field( $atts['padding_x'] );
		$padding_y        = sanitize_text_field( $atts['padding_y'] );
		$full_width       = sanitize_text_field( $atts['full_width'] );
		$icon             = sanitize_text_field( $atts['icon'] );
		$icon_position    = sanitize_text_field( $atts['icon_position'] );
		$icon_size        = sanitize_text_field( $atts['icon_size'] );
		$icon_color       = sanitize_hex_color( $atts['icon_color'] );
		$hover_icon_color = ! empty( $atts['hover_icon_color'] ) ? sanitize_hex_color( $atts['hover_icon_color'] ) : '';
		$hover_animation  = sanitize_text_field( $atts['hover_animation'] );
		$listening_text   = sanitize_text_field( $atts['listening_text'] );
		$listening_color  = sanitize_hex_color( $atts['listening_color'] );
		$persona          = sanitize_text_field( $atts['persona'] );
		$variant          = sanitize_key( $atts['variant'] );

		// Resolve button mode (voice, text, or both)
		$mode_default = isset( $widget_settings['button_mode'] ) ? sanitize_key( $widget_settings['button_mode'] ) : 'both';
		$mode = ! empty( $atts['mode'] ) ? sanitize_key( $atts['mode'] ) : $mode_default;
		if ( ! in_array( $mode, array( 'voice', 'text', 'both' ), true ) ) {
			$mode = 'voice';
		}

		// Override to text-only when voice minutes are exhausted.
		if ( 'text' !== $mode && class_exists( 'SiteStaffr_Billing_State' ) ) {
			$balance = SiteStaffr_Billing_State::get_minutes_balance();
			if ( is_array( $balance ) && 0 === (int) $balance['total'] ) {
				$mode = 'text';
			}
		}

		$sounds_enabled = ( 'off' !== sanitize_key( $atts['sounds'] ) );

		if ( empty( $icon_color ) ) {
			$icon_color = $text_color;
		}

		// Validate enums
		$valid_transforms = array( 'none', 'uppercase', 'lowercase', 'capitalize' );
		if ( ! in_array( $text_transform, $valid_transforms, true ) ) {
			$text_transform = 'none';
		}

		$valid_icons = array( 'microphone', 'phone', 'chat', 'headset', 'sitestaffr', 'none' );
		if ( ! in_array( $icon, $valid_icons, true ) ) {
			$icon = 'microphone';
		}

		$valid_icon_positions = array( 'left', 'right' );
		if ( ! in_array( $icon_position, $valid_icon_positions, true ) ) {
			$icon_position = 'left';
		}

		$valid_animations = array( 'none', 'scale', 'glow', 'pulse' );
		if ( ! in_array( $hover_animation, $valid_animations, true ) ) {
			$hover_animation = 'none';
		}

		// Enqueue button assets
		$this->enqueue_assets( $mode, $sounds_enabled );

		// Pro-only features: variant="hero".
		$billing_cache = SiteStaffr_Billing_State::get_cache();
		$plan_name = isset( $billing_cache['plan_name'] ) ? sanitize_key( $billing_cache['plan_name'] ) : 'trial';
		$is_pro = ( 'pro' === $plan_name );

		if ( ! $is_pro ) {
			if ( 'hero' === $variant ) {
				$variant = '';
			}
		}

		// Get site ID (WordPress site URL)
		$site_id = get_site_url();

		// NOTE: Auth token is NOT embedded in HTML to prevent caching issues
		// Token will be fetched dynamically via AJAX when button is clicked
		// This ensures guests on cached pages get fresh, valid tokens

		// Build CSS custom properties
		$style_vars = array(
			'--btn-text-color: ' . esc_attr( $text_color ),
			'--btn-font-size: ' . esc_attr( $font_size ),
			'--btn-font-weight: ' . esc_attr( $font_weight ),
			'--btn-text-transform: ' . esc_attr( $text_transform ),
			'--btn-padding-x: ' . esc_attr( $padding_x ),
			'--btn-padding-y: ' . esc_attr( $padding_y ),
			'--btn-border-radius: ' . esc_attr( $border_radius ),
			'--btn-border-width: ' . esc_attr( $border_width ),
			'--btn-border-style: ' . esc_attr( $border_style ),
			'--btn-icon-size: ' . esc_attr( $icon_size ),
			'--btn-icon-color: ' . esc_attr( $icon_color ),
		);

		if ( ! empty( $hover_icon_color ) ) {
			$style_vars[] = '--btn-hover-icon-color: ' . esc_attr( $hover_icon_color );
		}

		// Background color or gradient
		if ( 'on' === $gradient && ! empty( $gradient_end ) ) {
			$style_vars[] = '--btn-background: linear-gradient(135deg, ' . esc_attr( $background_color ) . ' 0%, ' . esc_attr( $gradient_end ) . ' 100%)';
			$style_vars[] = '--btn-hover-background: linear-gradient(135deg, ' . esc_attr( $hover_background ) . ' 0%, ' . esc_attr( $gradient_end ) . ' 100%)';
		} else {
			$style_vars[] = '--btn-background: ' . esc_attr( $background_color );
			$style_vars[] = '--btn-hover-background: ' . esc_attr( $hover_background );
		}

		// Border color
		if ( ! empty( $border_color ) && $border_width !== '0' && $border_style !== 'none' ) {
			$style_vars[] = '--btn-border-color: ' . esc_attr( $border_color );
		}

		// Box shadow
		if ( 'on' === $box_shadow ) {
			$style_vars[] = '--btn-box-shadow: 0 ' . esc_attr( $shadow_offset ) . ' ' . esc_attr( $shadow_blur ) . ' ' . esc_attr( $shadow_color );
		}

		// Build CSS class list
		$classes = array( 'sitestaffr-button-widget' );
		$classes[] = 'icon-position-' . $icon_position;
		$classes[] = 'hover-animation-' . $hover_animation;
		if ( 'on' === $full_width ) {
			$classes[] = 'full-width';
		}
		if ( 'hero' === $variant ) {
			$classes[] = 'sitestaffr-button--hero';
		}

		// Build button HTML
		ob_start();
		?>
		<div class="sitestaffr-button-container"
		     data-sitestaffr-widget="button"
		     data-site-id="<?php echo esc_attr( $site_id ); ?>"
		     data-listening-text="<?php echo esc_attr( $listening_text ); ?>"
		     data-idle-text="<?php echo esc_attr( $text ); ?>"
		     data-persona="<?php echo esc_attr( $persona ); ?>"
		     data-mode="<?php echo esc_attr( $mode ); ?>"
		     style="--widget-bg-color: <?php echo esc_attr( $background_color ); ?>; --widget-hover-bg-color: <?php echo esc_attr( $hover_background ); ?>; --widget-icon-color: <?php echo esc_attr( $text_color ); ?>">

			<button type="button"
			        class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
			        style="<?php echo esc_attr( implode( '; ', $style_vars ) ); ?>"
			        aria-label="<?php echo esc_attr( $text ); ?>">

				<?php if ( 'none' !== $icon && 'left' === $icon_position ) : ?>
				<span class="sitestaffr-button-icon">
					<?php echo wp_kses( $this->get_icon_svg( $icon ), self::get_svg_allowed_html() ); ?>
				</span>
				<?php endif; ?>

				<!-- Microphone Icon (listening state only — shared behavior with widget) -->
				<span class="sitestaffr-button-mic">
					<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
						<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
						<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
						<line x1="12" y1="19" x2="12" y2="23"></line>
						<line x1="8" y1="23" x2="16" y2="23"></line>
					</svg>
				</span>

				<!-- Loading Spinner (hidden by default, shown BEFORE text when active) -->
				<span class="sitestaffr-button-spinner">
					<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
						<line x1="12" y1="2" x2="12" y2="6"></line>
						<line x1="12" y1="18" x2="12" y2="22"></line>
						<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
						<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
						<line x1="2" y1="12" x2="6" y2="12"></line>
						<line x1="18" y1="12" x2="22" y2="12"></line>
						<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
						<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
					</svg>
				</span>

				<!-- Sound Wave (speaking state) -->
				<span class="sitestaffr-button-soundwave">
					<span></span>
					<span></span>
					<span></span>
				</span>

				<span class="sitestaffr-button-text"><?php echo esc_html( $text ); ?></span>

				<?php if ( 'none' !== $icon && 'right' === $icon_position ) : ?>
				<span class="sitestaffr-button-icon">
					<?php echo wp_kses( $this->get_icon_svg( $icon ), self::get_svg_allowed_html() ); ?>
				</span>
				<?php endif; ?>
			</button>

			<!-- Audio Element (hidden) -->
			<audio class="sitestaffr-button-audio" style="display: none;"></audio>
		</div>

		<?php if ( 'onboarding' === $persona ) : ?>
		<div class="sitestaffr-onboarding-card" hidden>
			<h3><?php echo esc_html__( 'Your Details', 'sitestaffr' ); ?></h3>
			<div class="sitestaffr-onboarding-field" data-field="name">
				<span class="sitestaffr-onboarding-label"><?php echo esc_html__( 'Name', 'sitestaffr' ); ?></span>
				<span class="sitestaffr-onboarding-value"></span>
			</div>
			<div class="sitestaffr-onboarding-field" data-field="business_name">
				<span class="sitestaffr-onboarding-label"><?php echo esc_html__( 'Business Name', 'sitestaffr' ); ?></span>
				<span class="sitestaffr-onboarding-value"></span>
			</div>
			<div class="sitestaffr-onboarding-field" data-field="website_url">
				<span class="sitestaffr-onboarding-label"><?php echo esc_html__( 'Website', 'sitestaffr' ); ?></span>
				<span class="sitestaffr-onboarding-value"></span>
			</div>
			<div class="sitestaffr-onboarding-field" data-field="email">
				<span class="sitestaffr-onboarding-label"><?php echo esc_html__( 'Email', 'sitestaffr' ); ?></span>
				<span class="sitestaffr-onboarding-value"></span>
			</div>
			<div class="sitestaffr-onboarding-field" data-field="phone">
				<span class="sitestaffr-onboarding-label"><?php echo esc_html__( 'Phone', 'sitestaffr' ); ?></span>
				<span class="sitestaffr-onboarding-value"></span>
			</div>
			<div class="sitestaffr-onboarding-success" hidden>
				<div class="sitestaffr-onboarding-success__icon">
					<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
						<circle cx="24" cy="24" r="22" stroke="#10b981" stroke-width="3" fill="none" class="sitestaffr-checkmark-circle"/>
						<path d="M14 24l7 7 13-13" stroke="#10b981" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round" class="sitestaffr-checkmark-check"/>
					</svg>
				</div>
				<h3 class="sitestaffr-onboarding-success__title"><?php echo esc_html__( "You're all set!", 'sitestaffr' ); ?></h3>
				<p class="sitestaffr-onboarding-success__text"><?php echo esc_html__( 'Our team will reach out soon to get your voice agent live. Get ready for something awesome!', 'sitestaffr' ); ?></p>
			</div>
		</div>
		<?php endif; ?>

		<?php
		return ob_get_clean();
	}

	/**
	 * Build button border-radius shorthand from new per-side settings.
	 *
	 * Falls back to legacy single radius setting if present.
	 *
	 * @since 0.15.0
	 * @param array $widget_settings Saved widget settings.
	 * @return string
	 */
	private function get_button_border_radius_default( $widget_settings ) {
		$legacy_value = isset( $widget_settings['button_border_radius'] ) ? sanitize_text_field( $widget_settings['button_border_radius'] ) : '';

		$top = isset( $widget_settings['button_border_radius_top'] )
			? sanitize_text_field( $widget_settings['button_border_radius_top'] )
			: ( '' !== $legacy_value ? $legacy_value : '80px' );
		$right = isset( $widget_settings['button_border_radius_right'] )
			? sanitize_text_field( $widget_settings['button_border_radius_right'] )
			: ( '' !== $legacy_value ? $legacy_value : '80px' );
		$bottom = isset( $widget_settings['button_border_radius_bottom'] )
			? sanitize_text_field( $widget_settings['button_border_radius_bottom'] )
			: ( '' !== $legacy_value ? $legacy_value : '80px' );
		$left = isset( $widget_settings['button_border_radius_left'] )
			? sanitize_text_field( $widget_settings['button_border_radius_left'] )
			: ( '' !== $legacy_value ? $legacy_value : '80px' );

		return $this->normalize_border_radius_shorthand( trim( $top . ' ' . $right . ' ' . $bottom . ' ' . $left ), '80px 80px 80px 80px' );
	}

	/**
	 * Normalize border-radius shorthand to a safe four-value px string.
	 *
	 * @since 0.15.0
	 * @param string $value Raw border radius value.
	 * @param string $fallback Default fallback shorthand.
	 * @return string
	 */
	private function normalize_border_radius_shorthand( $value, $fallback ) {
		$parts = preg_split( '/\s+/', trim( $value ) );
		$parts = array_values( array_filter( $parts ) );

		if ( empty( $parts ) ) {
			return $fallback;
		}

		if ( 1 === count( $parts ) ) {
			$normalized = $this->normalize_radius_part( $parts[0], '0px' );
			return trim( $normalized . ' ' . $normalized . ' ' . $normalized . ' ' . $normalized );
		}

		if ( 4 === count( $parts ) ) {
			$normalized_parts = array();
			foreach ( $parts as $part ) {
				$normalized_parts[] = $this->normalize_radius_part( $part, '0px' );
			}
			return implode( ' ', $normalized_parts );
		}

		return $fallback;
	}

	/**
	 * Normalize a single radius part to px.
	 *
	 * @since 0.15.0
	 * @param string $value Raw radius part.
	 * @param string $default_px Default px fallback.
	 * @return string
	 */
	private function normalize_radius_part( $value, $default_px ) {
		if ( preg_match( '/^(\d{1,3})px$/', $value, $matches ) ) {
			return intval( $matches[1] ) . 'px';
		}
		if ( preg_match( '/^\d{1,3}$/', $value ) ) {
			return intval( $value ) . 'px';
		}
		return $default_px;
	}

	/**
	 * Return allowed HTML tags and attributes for SVG output escaping.
	 *
	 * @since 1.3.4
	 * @return array
	 */
	public static function get_svg_allowed_html() {
		static $cache = null;
		if ( null !== $cache ) {
			return $cache;
		}
		$cache = array(
			'svg'     => array(
				'xmlns'           => true,
				'width'           => true,
				'height'          => true,
				'viewbox'         => true,
				'fill'            => true,
				'stroke'          => true,
				'stroke-width'    => true,
				'stroke-linecap'  => true,
				'stroke-linejoin' => true,
				'aria-hidden'     => true,
				'class'           => true,
			),
			'path'    => array( 'd' => true ),
			'line'    => array(
				'x1' => true,
				'y1' => true,
				'x2' => true,
				'y2' => true,
			),
			'circle'  => array(
				'cx'           => true,
				'cy'           => true,
				'r'            => true,
				'fill'         => true,
				'stroke'       => true,
				'stroke-width' => true,
			),
			'rect'    => array(
				'x'      => true,
				'y'      => true,
				'width'  => true,
				'height' => true,
				'rx'     => true,
				'ry'     => true,
				'fill'   => true,
			),
			'polygon' => array( 'points' => true, 'fill' => true ),
			'g'       => array( 'transform' => true, 'fill' => true ),
		);
		return $cache;
	}

	/**
	 * Get icon SVG markup.
	 *
	 * Returns the SVG code for the specified icon type.
	 *
	 * @since    0.11.75
	 * @param    string    $icon    Icon type (microphone, phone, chat).
	 * @return   string             SVG markup (safe HTML).
	 */
	private function get_icon_svg( $icon ) {
		if ( 'sitestaffr' === $icon ) {
			$svg_file = SITESTAFFR_PLUGIN_DIR . 'assets/images/sitestaffr-icon.svg';
			if ( file_exists( $svg_file ) ) {
				$svg_content = file_get_contents( $svg_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local file
				return preg_replace(
					'/<svg[^>]*>/',
					'<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="74 104 412 412" fill="currentColor" stroke="none" aria-hidden="true">',
					$svg_content,
					1
				);
			}
			return '';
		}

		$svgs = array(
			'microphone' => '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>',
			'phone'      => '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>',
			'chat'       => '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>',
			'headset'    => '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>',
		);

		// Return SVG if exists, empty string otherwise
		// SVG is hardcoded and safe (no user input)
		return isset( $svgs[ $icon ] ) ? $svgs[ $icon ] : '';
	}

	/**
	 * Enqueue button widget assets (CSS and JavaScript).
	 *
	 * Enqueues styles and scripts required for button functionality.
	 *
	 * @since    0.11.75
	 */
	private function enqueue_assets( $mode = 'voice', $sounds_enabled = true ) {
		if ( class_exists( 'SiteStaffr_Billing_State' ) && SiteStaffr_Billing_State::is_cutover_enabled() ) {
			SiteStaffr_Billing_State::maybe_refresh_cache( false );
		}

		// Enqueue button CSS
		wp_enqueue_style(
			'sitestaffr-button-widget',
			SITESTAFFR_PLUGIN_URL . 'public/css/sitestaffr-button-widget.css',
			array(),
			SITESTAFFR_VERSION,
			'all'
		);

		// Enqueue SiteStaffr Core (shared logic)
		wp_enqueue_script(
			'sitestaffr-core',
			SITESTAFFR_PLUGIN_URL . 'public/js/sitestaffr-core.js',
			array(),
			SITESTAFFR_VERSION,
			true
		);

		// Enqueue knowledge search module (voice RAG relay).
		wp_enqueue_script(
			'sitestaffr-knowledge-search',
			SITESTAFFR_PLUGIN_URL . 'public/js/sitestaffr-knowledge-search.js',
			array(),
			SITESTAFFR_VERSION,
			true
		);

		// Enqueue button JavaScript (UI wrapper)
		wp_enqueue_script(
			'sitestaffr-button-widget',
			SITESTAFFR_PLUGIN_URL . 'public/js/sitestaffr-button-widget.js',
			array( 'sitestaffr-core' ),
			SITESTAFFR_VERSION,
			true
		);

		// Pass PHP data to JavaScript
		wp_localize_script(
			'sitestaffr-button-widget',
			'sitestaffr_button_data',
			array(
				'middleware_url' => esc_url_raw( SITESTAFFR_MIDDLEWARE_URL ),
				'ajax_url'       => admin_url( 'admin-ajax.php' ),
				'plugin_url'     => SITESTAFFR_PLUGIN_URL,
				'token_nonce'    => wp_create_nonce( 'sitestaffr_widget_token' ),
				'save_nonce'     => wp_create_nonce( 'sitestaffr_save_conversation' ),
				'finalize_nonce' => wp_create_nonce( 'sitestaffr_finalize_usage' ),
				'realtime_nonce' => wp_create_nonce( 'sitestaffr_realtime_session' ),
				'webrtc_nonce'   => wp_create_nonce( 'sitestaffr_webrtc_session' ),
				'chat_nonce'              => wp_create_nonce( 'sitestaffr_chat_message' ),
				'knowledge_search_nonce' => wp_create_nonce( 'sitestaffr_knowledge_search' ),
				'chat_greeting'          => SiteStaffr_Widget::get_chat_greeting(),
				'mode'           => $mode,
				'sounds_enabled' => $sounds_enabled,
				'entitlement'    => ( class_exists( 'SiteStaffr_Billing_State' ) && SiteStaffr_Billing_State::is_subscription_inactive() ) ? 'inactive' : 'active',
				'strings'        => array(
					'error'      => __( 'Error - Please try again', 'sitestaffr' ),
					'permission' => __( 'Microphone permission required', 'sitestaffr' ),
				),
			)
		);

		// Enqueue chat panel assets for text/both modes, or when plan is inactive (contact form fallback needs them)
		$needs_chat_assets = in_array( $mode, array( 'text', 'both' ), true )
			|| ( class_exists( 'SiteStaffr_Billing_State' ) && SiteStaffr_Billing_State::is_subscription_inactive() );
		if ( $needs_chat_assets ) {
			wp_enqueue_style( 'sitestaffr-chat-panel', SITESTAFFR_PLUGIN_URL . 'public/css/sitestaffr-chat-panel.css', array(), SITESTAFFR_VERSION );
			wp_enqueue_script( 'sitestaffr-text-chat', SITESTAFFR_PLUGIN_URL . 'public/js/sitestaffr-text-chat.js', array(), SITESTAFFR_VERSION, true );
		}
	}

	/**
	 * AJAX handler for token generation.
	 *
	 * Generates a fresh JWT token for WebSocket authentication.
	 * Uses admin-ajax.php instead of REST API for better compatibility
	 * with caching plugins and security plugins.
	 *
	 * Protected with multiple security layers: nonce verification, honeypot,
	 * rate limiting, and pattern detection to prevent bot attacks.
	 *
	 * @since    0.12.15
	 * @since    0.13.55 Added nonce verification, honeypot, global rate limit, and pattern detection.
	 */
	public function ajax_get_token() {
		$ip_address = $this->get_client_ip();

		// Security guards: nonce, honeypot, ban (rate limits stay below admin bypass).
		SiteStaffr_Public_Request_Guards::enforce_public_guards( array(
			'nonce_action' => 'sitestaffr_widget_token',
			'ip'           => $ip_address,
			'honeypot'     => true,
			'ban_check'    => true,
		) );

		// 3. Skip rate limiting for logged-in administrators (allows testing)
		if ( current_user_can( 'manage_options' ) ) {
			// Admin user - skip directly to token generation
			if ( ! class_exists( 'SiteStaffr_Auth_Token' ) ) {
				require_once SITESTAFFR_PLUGIN_DIR . 'includes/class-sitestaffr-auth-token.php';
			}
			$site_id = get_site_url();
			$user_id = get_current_user_id();
			$token = SiteStaffr_Auth_Token::generate_token( $site_id, $user_id, 600 );

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				SiteStaffr_Logger::legacy( sprintf(
					'SiteStaffr Widget Token (AJAX): Admin bypass - generated token for user_id=%d, IP=%s',
					$user_id,
					$ip_address
				) );
			}

			wp_send_json_success( array(
				'token'      => $token,
				'site_id'    => $site_id,
				'expires_in' => 600,
			) );
			return;
		}

		// 4. Global rate limiting (distributed attack protection)
		$global_limit_key = 'sitestaffr_token_global_limit';
		if ( ! SiteStaffr_Public_Request_Guards::allow_rate_limited_request( $global_limit_key, 100, 60 ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Widget Token (AJAX): Global rate limit exceeded (100/minute)' );
			SiteStaffr_Public_Request_Guards::send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_service_temporarily_unavailable_try_again_later(),
				'retry_after' => 60,
				'code'        => 'rate_limited',
			), 503 );
			return;
		}

		// 5. Per-IP rate limiting (10 per minute)
		$ip_hash = hash( 'sha256', $ip_address );
		$rate_limit_key = 'sitestaffr_token_ip_v2_' . $ip_hash;
		if ( ! SiteStaffr_Public_Request_Guards::allow_rate_limited_request( $rate_limit_key, 10, 60 ) ) {
			// Rate limit exceeded
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				SiteStaffr_Logger::legacy( sprintf(
					'SiteStaffr Widget Token (AJAX): Per-IP rate limit exceeded for IP %s',
					$ip_address
				) );
			}
			SiteStaffr_Public_Request_Guards::send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_too_many_requests_try_again_minute(),
				'retry_after' => 60,
				'code'        => 'rate_limited',
			), 429 );
			return;
		}

		// 6. Suspicious pattern detection (8+ requests in 10 seconds)
		$pattern_key = 'sitestaffr_token_pattern_v2_' . $ip_hash;
		$timestamps = get_transient( $pattern_key );

		if ( $timestamps === false ) {
			$timestamps = array();
		}

		// Remove timestamps older than 10 seconds
		$current_time = time();
		$timestamps = array_filter( $timestamps, function( $ts ) use ( $current_time ) {
			return ( $current_time - $ts ) < 10;
		} );

		// Add current timestamp
		$timestamps[] = $current_time;

		// Check if suspicious pattern detected
		if ( count( $timestamps ) >= 8 ) {
			// Block IP for 2 minutes
			set_transient( 'sitestaffr_token_blocked_v2_' . $ip_hash, true, 120 );
			SiteStaffr_Logger::legacy( sprintf(
				'SiteStaffr Widget Token (AJAX): Suspicious pattern detected - blocking IP %s for 2 minutes',
				$ip_address
			) );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_too_many_requests_try_again_minute(),
				'retry_after' => 120,
				'code'        => 'rate_limited',
			), 429 );
			return;
		}

		// Save timestamps
		set_transient( $pattern_key, $timestamps, 10 );

		// Check if IP is blocked
		if ( get_transient( 'sitestaffr_token_blocked_v2_' . $ip_hash ) ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				SiteStaffr_Logger::legacy( sprintf(
					'SiteStaffr Widget Token (AJAX): Request from blocked IP %s',
					$ip_address
				) );
			}
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_too_many_requests_try_again_minute(),
				'retry_after' => 120,
				'code'        => 'rate_limited',
			), 429 );
			return;
		}

		// Load auth token class if not already loaded
		if ( ! class_exists( 'SiteStaffr_Auth_Token' ) ) {
			require_once SITESTAFFR_PLUGIN_DIR . 'includes/class-sitestaffr-auth-token.php';
		}

		// Generate fresh JWT token (10-minute expiry for security)
		$site_id = get_site_url();
		$user_id = get_current_user_id(); // 0 for guests
		$token = SiteStaffr_Auth_Token::generate_token( $site_id, $user_id, 600 ); // 10 minutes

		// Log token generation (only in development)
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			SiteStaffr_Logger::legacy( sprintf(
				'SiteStaffr Widget Token (AJAX): Generated token for site_id=%s, user_id=%d (guest=%s), IP=%s',
				$site_id,
				$user_id,
				$user_id === 0 ? 'yes' : 'no',
				$ip_address
			) );
		}

		wp_send_json_success( array(
			'token'      => $token,
			'site_id'    => $site_id,
			'expires_in' => 600, // 10 minutes
		) );
	}

	/**
	 * AJAX handler for getting Realtime API session with HMAC authentication.
	 *
	 * This endpoint proxies the request to middleware with proper HMAC signature
	 * to avoid exposing the API secret to the browser.
	 *
	 * Includes business info in request to eliminate middleware → WordPress callback.
	 *
	 * @since    0.13.2
	 * @return   void
	 */
	public function ajax_get_realtime_session() {
		$ip_address = $this->get_client_ip();

		// Security guards: nonce, honeypot, ban, per-IP rate limit.
		SiteStaffr_Public_Request_Guards::enforce_public_guards( array(
			'nonce_action' => 'sitestaffr_realtime_session',
			'ip'           => $ip_address,
			'honeypot'     => true,
			'ban_check'    => true,
			'rate_limit'   => 20,
		) );

		// Get API secret for HMAC signature (per-site or global)
		$preflight_config = SiteStaffr_Public_Middleware_Client::get_public_preflight_config( $this->get_api_secret() );
		$api_secret = $preflight_config['api_secret'];
		if ( $preflight_config['missing_api_secret'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_api_secret() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_api_secret(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		// Get site token
		$site_token = get_option( 'sitestaffr_site_token' );
		if ( empty( $site_token ) ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_site_token() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_site_token(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}
		$account_id = get_option( 'sitestaffr_account_id', '' );
		$installation_id = get_option( 'sitestaffr_installation_id', '' );

		// Fetch business info locally (avoid middleware → WordPress callback)
		$saved_settings = get_option( 'sitestaffr_settings', array() );
		$widget_settings = SiteStaffr_Widget_Settings_Provider::get_widget_settings();

		$business_info = array(
			'business_name'        => isset( $saved_settings['business_name'] ) ? $saved_settings['business_name'] : '',
			'business_description' => isset( $saved_settings['business_context'] ) ? $saved_settings['business_context'] : '',
			'business_hours'       => isset( $saved_settings['business_hours'] ) ? $saved_settings['business_hours'] : '',
			'business_phone'       => isset( $saved_settings['business_phone'] ) ? $saved_settings['business_phone'] : '',
			'custom_greeting'      => isset( $saved_settings['custom_greeting'] ) ? sanitize_text_field( $saved_settings['custom_greeting'] ) : '',
			'custom_greeting_tone' => isset( $saved_settings['custom_greeting_tone'] ) ? sanitize_key( $saved_settings['custom_greeting_tone'] ) : 'warm_neutral',
			'custom_instructions'       => $this->get_realtime_custom_instructions( $saved_settings ),
			'custom_instructions_user'  => SiteStaffr_Business_Enrichment::get_user_custom_instructions(),
			'ai_voice'             => isset( $widget_settings['ai_voice'] ) ? $widget_settings['ai_voice'] : 'marin',
			'notification_email'   => ! empty( $saved_settings['notification_email'] ) ? $saved_settings['notification_email'] : get_option( 'admin_email' ),
		);
		$business_info = SiteStaffr_Business_Enrichment::enrich_business_info( $business_info );
		$business_info = SiteStaffr_Business_Enrichment::attach_hot_chunks( $business_info, 'voice' );

		// Build request body with business info included
		$request_body = SiteStaffr_Public_Middleware_Client::build_realtime_session_request_body(
			$site_token,
			get_site_url(),
			$business_info,
			sanitize_text_field( $account_id ),
			sanitize_text_field( $installation_id )
		);

		// Build signed request payload
		$payload = wp_json_encode( $request_body );

		// Make authenticated request to middleware
		$middleware_url = $preflight_config['middleware_url'];
		if ( $preflight_config['missing_middleware_url'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_middleware_url() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_middleware_url(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		$realtime_operation = SiteStaffr_Public_Middleware_Client::realtime_session_request(
			$middleware_url,
			$payload,
			$api_secret
		);

		if ( 'request_error' === $realtime_operation['result'] ) {
			SiteStaffr_Logger::legacy(
				SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() .
				SiteStaffr_Public_Middleware_Client::log_suffix_request_failed() .
				$realtime_operation['error_message']
			);
			wp_send_json_error( $realtime_operation['error_payload'], $realtime_operation['status_code'] );
			return;
		}

		if ( 'middleware_error' === $realtime_operation['result'] ) {
			SiteStaffr_Logger::legacy( sprintf(
				SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() . SiteStaffr_Public_Middleware_Client::log_fmt_middleware_returned_status_body(),
				$realtime_operation['status_body_for_log']
			) );
			wp_send_json_error( $realtime_operation['error_payload'], $realtime_operation['status_code'] );
			return;
		}

		if ( 'invalid_json' === $realtime_operation['result'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_invalid_json_response_from_middleware() );
			wp_send_json_error( $realtime_operation['error_payload'], $realtime_operation['status_code'] );
			return;
		}

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			SiteStaffr_Logger::legacy( sprintf(
				SiteStaffr_Public_Middleware_Client::log_prefix_realtime_session_ajax() . 'Session created successfully for IP %s',
				$ip_address
			) );
		}

		wp_send_json_success( $realtime_operation['success_payload'], $realtime_operation['status_code'] );
	}

	/**
	 * AJAX handler for WebRTC SDP exchange.
	 *
	 * Proxies SDP offer/answer to middleware with HMAC authentication.
	 * Browser cannot call middleware directly due to HMAC signature requirement.
	 *
	 * @since 0.13.22
	 * @return void
	 */
	public function ajax_webrtc_session() {
		$ip_address = $this->get_client_ip();

		// Security guards: nonce, honeypot, ban, per-IP rate limit.
		SiteStaffr_Public_Request_Guards::enforce_public_guards( array(
			'nonce_action' => 'sitestaffr_webrtc_session',
			'ip'           => $ip_address,
			'honeypot'     => true,
			'ban_check'    => true,
			'rate_limit'   => 20,
		) );

		// Get API secret for HMAC signature (per-site or global)
		$preflight_config = SiteStaffr_Public_Middleware_Client::get_public_preflight_config( $this->get_api_secret() );
		$api_secret = $preflight_config['api_secret'];
		if ( $preflight_config['missing_api_secret'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_api_secret() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_api_secret(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		// Get site token from POST
		$site_token = SiteStaffr_Public_Request_Guards::get_post_text_field( 'site_token' );
		if ( empty( $site_token ) ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_site_token_in_request() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_missing_site_token(),
			), 400 );
			return;
		}
		$account_id = sanitize_text_field( get_option( 'sitestaffr_account_id', '' ) );
		$installation_id = sanitize_text_field( get_option( 'sitestaffr_installation_id', '' ) );

		// Get SDP offer from POST (sanitized via sanitize_textarea_field in request guards).
		$sdp_offer = SiteStaffr_Public_Request_Guards::get_post_raw_field( 'sdp' );
		if ( empty( $sdp_offer ) ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . 'Missing SDP offer in request' );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_missing_sdp_offer(),
			), 400 );
			return;
		}

		// Basic SDP format validation (all SDP starts with "v=0")
		if ( strpos( $sdp_offer, 'v=0' ) !== 0 ) {
			wp_send_json_error( array( 'message' => SiteStaffr_Public_Middleware_Client::msg_invalid_sdp_format() ), 400 );
			return;
		}

		// Size limit (prevent DoS)
		if ( strlen( $sdp_offer ) > 50000 ) {
			wp_send_json_error( array( 'message' => SiteStaffr_Public_Middleware_Client::msg_sdp_offer_too_large() ), 413 );
			return;
		}

		// Fetch business info to include in middleware request (eliminates callback)
		// This prevents the middleware from needing to call back to WordPress REST API
		$saved_settings = get_option( 'sitestaffr_settings', array() );
		$widget_settings = SiteStaffr_Widget_Settings_Provider::get_widget_settings();

		$business_info = array(
			'business_name'        => isset( $saved_settings['business_name'] ) ? $saved_settings['business_name'] : '',
			'business_description' => isset( $saved_settings['business_context'] ) ? $saved_settings['business_context'] : '',
			'business_hours'       => isset( $saved_settings['business_hours'] ) ? $saved_settings['business_hours'] : '',
			'business_phone'       => isset( $saved_settings['business_phone'] ) ? $saved_settings['business_phone'] : '',
			'custom_greeting'      => isset( $saved_settings['custom_greeting'] ) ? sanitize_text_field( $saved_settings['custom_greeting'] ) : '',
			'custom_greeting_tone' => isset( $saved_settings['custom_greeting_tone'] ) ? sanitize_key( $saved_settings['custom_greeting_tone'] ) : 'warm_neutral',
			'custom_instructions'       => $this->get_realtime_custom_instructions( $saved_settings ),
			'custom_instructions_user'  => SiteStaffr_Business_Enrichment::get_user_custom_instructions(),
			'ai_voice'             => isset( $widget_settings['ai_voice'] ) ? $widget_settings['ai_voice'] : 'marin',
			'notification_email'   => ! empty( $saved_settings['notification_email'] ) ? $saved_settings['notification_email'] : get_option( 'admin_email' ),
		);
		$business_info = SiteStaffr_Business_Enrichment::enrich_business_info( $business_info );
		$business_info = SiteStaffr_Business_Enrichment::attach_hot_chunks( $business_info, 'voice' );

		// Get middleware URL
		$middleware_url = $preflight_config['middleware_url'];
		if ( $preflight_config['missing_middleware_url'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_middleware_url() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_middleware_url(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		$webrtc_operation = SiteStaffr_Public_Middleware_Client::webrtc_session_request(
			$middleware_url,
			$site_token,
			get_site_url(),
			$sdp_offer,
			$api_secret,
			$business_info,
			$ip_address,
			$account_id,
			$installation_id
		);

		if ( 'request_error' === $webrtc_operation['result'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_request_failed() . $webrtc_operation['error_message'] );
			wp_send_json_error( $webrtc_operation['error_payload'], $webrtc_operation['status_code'] );
			return;
		}

		if ( 'middleware_error' === $webrtc_operation['result'] ) {
			if ( '' !== $webrtc_operation['status_body_for_log'] ) {
				SiteStaffr_Logger::legacy( sprintf(
					SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . SiteStaffr_Public_Middleware_Client::log_fmt_middleware_returned_status_body(),
					$webrtc_operation['status_body_for_log']
				) );
			}
			wp_send_json_error( $webrtc_operation['error_payload'], $webrtc_operation['status_code'] );
			return;
		}

		if ( 'invalid_json' === $webrtc_operation['result'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . SiteStaffr_Public_Middleware_Client::log_suffix_invalid_json_response_from_middleware() );
			wp_send_json_error( $webrtc_operation['error_payload'], $webrtc_operation['status_code'] );
			return;
		}

		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			SiteStaffr_Logger::legacy( sprintf(
				SiteStaffr_Public_Middleware_Client::log_prefix_webrtc_sdp_ajax() . 'Response received successfully for IP %s',
				$ip_address
			) );
		}

		// Return JSON with SDP and session configuration to browser
		wp_send_json_success( $webrtc_operation['success_payload'] );
	}

	/**
	 * Get client IP address.
	 *
	 * Checks common headers for real IP when behind proxy/CDN.
	 *
	 * @since    0.12.15
	 * @return   string    Client IP address.
	 */
	private function get_client_ip() {
		// Check common headers for real IP (when behind proxy/CDN)
		$headers = array(
			'HTTP_CF_CONNECTING_IP', // Cloudflare
			'HTTP_X_FORWARDED_FOR',  // Standard proxy header
			'HTTP_X_REAL_IP',        // Nginx proxy
			'REMOTE_ADDR',           // Direct connection
		);

		foreach ( $headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below
				$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
				// If X-Forwarded-For contains multiple IPs, use the first one
				if ( strpos( $ip, ',' ) !== false ) {
					$ips = explode( ',', $ip );
					$ip = trim( $ips[0] );
				}
				return $ip;
			}
		}

		return '0.0.0.0'; // Fallback
	}

	/**
	 * AJAX handler for saving conversation transcript.
	 *
	 * Saves full WebRTC conversation to database (call record + transcript turns).
	 * Called by browser when conversation ends.
	 *
	 * @since 0.13.69
	 * @return void
	 */
	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
	public function ajax_save_conversation() {
		// Security guards: nonce, per-IP rate limit.
		$ip_address = $this->get_client_ip();
		SiteStaffr_Public_Request_Guards::enforce_public_guards( array(
			'nonce_action' => 'sitestaffr_save_conversation',
			'ip'           => $ip_address,
			'rate_limit'   => 20,
		) );

		// Get and validate inputs
		$session_id = SiteStaffr_Public_Request_Guards::get_post_text_field( 'session_id' );
		$duration = SiteStaffr_Public_Request_Guards::get_post_absint( 'duration' );
		$transcript_json = SiteStaffr_Public_Request_Guards::get_post_raw_field( 'transcript' );
		$realtime_usage_json = SiteStaffr_Public_Request_Guards::get_post_raw_field( 'realtime_usage' );
		$channel_source = SiteStaffr_Public_Request_Guards::get_post_key_field( 'channel_source' );
		if ( '' === $channel_source ) {
			$channel_source = 'button';
		}

		$persona = SiteStaffr_Public_Request_Guards::get_post_key_field( 'persona' );
		if ( '' === $persona ) {
			$persona = 'default';
		}

		if ( empty( $session_id ) || empty( $transcript_json ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Save Conversation: Missing required fields' );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_missing_required_fields(),
			), 400 );
			return;
		}

		// Parse transcript JSON
		$transcript = json_decode( $transcript_json, true );
		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $transcript ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Save Conversation: Invalid transcript JSON' );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_invalid_transcript_format(),
			), 400 );
			return;
		}

		// Validate transcript structure
		if ( empty( $transcript ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Save Conversation: Empty transcript' );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_transcript_cannot_be_empty(),
			), 400 );
			return;
		}

		$realtime_usage = $this->parse_realtime_usage_payload( $realtime_usage_json );

		$contact_fields = SiteStaffr_Public_Conversation_Usage::parse_contact_fields_payload( SiteStaffr_Public_Request_Guards::get_post_raw_field( 'contact_fields' ) );

		$submission_source = SiteStaffr_Public_Request_Guards::get_post_key_field( 'submission_source' );
		if ( ! in_array( $submission_source, array( 'contact_form' ), true ) ) {
			$submission_source = '';
		}

		global $wpdb;
		$calls_table = $wpdb->prefix . 'phoneease_calls';
		$transcripts_table = $wpdb->prefix . 'phoneease_transcripts';

		// Calculate started_at timestamp (current time - duration)
		$started_at = time() - $duration;
		$service_type = ( 'button' === $channel_source ) ? 'webrtc_button' : 'webrtc_widget';
		$visitor_ip = ( '0.0.0.0' !== $ip_address && filter_var( $ip_address, FILTER_VALIDATE_IP ) ) ? $ip_address : null;

		// Insert call record
		$call_result = $wpdb->insert(
			$calls_table,
			array(
				'session_id'   => $session_id,
				'service_type' => $service_type,
				'persona'      => $persona,
				'from_number'  => null,
				'to_number'    => null,
				'visitor_ip'   => $visitor_ip,
				'call_status'  => 'completed',
				'call_duration' => $duration,
				'started_at'   => $started_at,
				'is_read'      => 0,
				'is_test_call' => 0,
				'created_at'   => current_time( 'mysql' ),
			),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s' )
		);

		if ( $call_result === false ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Save Conversation: Failed to insert call record - ' . $wpdb->last_error );
			wp_send_json_error( array(
				'message' => 'Database error: could not save call record',
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		$call_id = $wpdb->insert_id;

		// Insert transcript turns
		$transcript_errors = 0;
		foreach ( $transcript as $turn ) {
			// Validate turn structure
			if ( ! isset( $turn['role'] ) || ! isset( $turn['text'] ) ) {
				$transcript_errors++;
				continue;
			}

			// Map role: 'user' -> 'caller', 'assistant' -> 'ai'
			$speaker = ( $turn['role'] === 'user' ) ? 'caller' : 'ai';
			$message_text = sanitize_textarea_field( $turn['text'] );
			$timestamp = isset( $turn['timestamp'] ) ? sanitize_text_field( $turn['timestamp'] ) : current_time( 'mysql' );

			// Convert ISO timestamp to MySQL datetime if needed
			if ( strtotime( $timestamp ) !== false ) {
				$timestamp = gmdate( 'Y-m-d H:i:s', strtotime( $timestamp ) );
			} else {
				$timestamp = current_time( 'mysql' );
			}

			$transcript_result = $wpdb->insert(
				$transcripts_table,
				array(
					'call_id'      => $call_id,
					'speaker'      => $speaker,
					'message_text' => $message_text,
					'timestamp'    => $timestamp,
					'created_at'   => current_time( 'mysql' ),
				),
				array( '%d', '%s', '%s', '%s', '%s' )
			);

			if ( $transcript_result === false ) {
				SiteStaffr_Logger::legacy( sprintf(
					'SiteStaffr Save Conversation: Failed to insert transcript turn - %s',
					$wpdb->last_error
				) );
				$transcript_errors++;
			}
		}

		$realtime_usage_logged = false;
		if ( ! empty( $realtime_usage ) ) {
			$realtime_usage_logged = $this->log_realtime_usage_metadata( $call_id, $realtime_usage );
		}

		// Log success
		SiteStaffr_Logger::legacy( sprintf(
			'SiteStaffr Save Conversation: Saved call_id=%d, session_id=%s, duration=%ds, turns=%d, errors=%d, realtime_usage_logged=%s',
			$call_id,
			$session_id,
			$duration,
			count( $transcript ),
			$transcript_errors,
			$realtime_usage_logged ? 'yes' : 'no'
		) );

		// Trigger post-call processing (AI summary generation + email notifications)
		do_action( 'sitestaffr_call_completed', $call_id, 'completed', $contact_fields, $submission_source );

		wp_send_json_success( array(
			'call_id' => $call_id,
			'message' => 'Conversation saved successfully',
		) );
	}
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

	/**
	 * AJAX handler for usage finalization.
	 *
	 * Sends finalized conversation duration to middleware billing endpoint.
	 *
	 * @since 0.14.2
	 * @return void
	 */
	public function ajax_finalize_usage() {
		// Security guard: nonce only.
		SiteStaffr_Public_Request_Guards::enforce_public_guards( array(
			'nonce_action' => 'sitestaffr_finalize_usage',
		) );

		$session_id = SiteStaffr_Public_Request_Guards::get_post_text_field( 'session_id' );
		$duration = SiteStaffr_Public_Request_Guards::get_post_absint( 'duration' );
		$contact_email = SiteStaffr_Public_Request_Guards::get_post_text_field( 'contact_email' );
		$contact_phone = SiteStaffr_Public_Request_Guards::get_post_text_field( 'contact_phone' );

		if ( empty( $session_id ) ) {
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_missing_required_field_session_id(),
			), 400 );
			return;
		}

		$account_id = get_option( 'sitestaffr_account_id', '' );
		$installation_id = get_option( 'sitestaffr_installation_id', '' );

		if ( empty( $account_id ) || empty( $installation_id ) ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_finalize_usage() . 'Missing account_id or installation_id' );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_billing_not_initialized_for_site(),
			), 412 );
			return;
		}

		$preflight_config = SiteStaffr_Public_Middleware_Client::get_public_preflight_config( $this->get_api_secret() );
		$api_secret = $preflight_config['api_secret'];
		if ( $preflight_config['missing_api_secret'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_finalize_usage() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_api_secret() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_api_secret(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		$middleware_url = $preflight_config['middleware_url'];
		if ( $preflight_config['missing_middleware_url'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_finalize_usage() . SiteStaffr_Public_Middleware_Client::log_suffix_missing_middleware_url() );
			wp_send_json_error( array(
				'message' => SiteStaffr_Public_Middleware_Client::msg_config_missing_middleware_url(),
			), SiteStaffr_Public_Middleware_Client::http_500() );
			return;
		}

		$payload_array = SiteStaffr_Public_Middleware_Client::build_finalize_usage_payload(
			$account_id,
			$installation_id,
			$session_id,
			$duration,
			get_site_url()
		);

		$payload_json = wp_json_encode( $payload_array );

		$finalize_operation = SiteStaffr_Public_Middleware_Client::finalize_usage_request(
			$middleware_url,
			$payload_json,
			$api_secret
		);

		if ( 'request_error' === $finalize_operation['result'] ) {
			SiteStaffr_Logger::legacy( SiteStaffr_Public_Middleware_Client::log_prefix_finalize_usage() . SiteStaffr_Public_Middleware_Client::log_suffix_request_failed() . $finalize_operation['error_message'] );
			wp_send_json_error( $finalize_operation['error_payload'], $finalize_operation['status_code'] );
			return;
		}

		if ( ! $finalize_operation['ok'] ) {
			SiteStaffr_Logger::legacy(
				sprintf(
					SiteStaffr_Public_Middleware_Client::log_prefix_finalize_usage() . SiteStaffr_Public_Middleware_Client::log_fmt_middleware_returned_status_body(),
					$finalize_operation['status_body_for_log']
				)
			);
			wp_send_json_error( $finalize_operation['error_payload'], $finalize_operation['status_code'] );
			return;
		}

		$conversation_success = SiteStaffr_Public_Middleware_Client::build_conversation_success_payload(
			$account_id,
			$installation_id,
			$session_id,
			get_site_url(),
			$contact_email,
			$contact_phone
		);

		if ( ! $conversation_success['success'] ) {
			SiteStaffr_Logger::warning( '[ConversationSuccess] not firing: no valid contact' );
			wp_send_json_success( $finalize_operation['success_payload'] );
			return;
		}

		$conversation_success_payload_json = wp_json_encode( $conversation_success['payload'] );
		if ( false !== $conversation_success_payload_json ) {
			$conversation_success_operation = SiteStaffr_Public_Middleware_Client::conversation_success_request(
				$middleware_url,
				$conversation_success_payload_json,
				$api_secret
			);

			if ( ! $conversation_success_operation['ok'] ) {
				if ( 'request_error' === $conversation_success_operation['result'] ) {
					SiteStaffr_Logger::warning(
						SiteStaffr_Public_Middleware_Client::log_prefix_conversation_success()
						. SiteStaffr_Public_Middleware_Client::log_suffix_request_failed()
						. $conversation_success_operation['error_message']
					);
				} else {
					SiteStaffr_Logger::warning(
						sprintf(
							SiteStaffr_Public_Middleware_Client::log_prefix_conversation_success() . SiteStaffr_Public_Middleware_Client::log_fmt_middleware_returned_status_body(),
							$conversation_success_operation['status_body_for_log']
						)
					);
				}
			}
		}

		wp_send_json_success( $finalize_operation['success_payload'] );
	}

	/**
	 * Parse and sanitize Realtime usage payload from browser.
	 *
	 * @since 0.13.81
	 * @param string $usage_json Raw JSON payload from AJAX body.
	 * @return array Sanitized usage payload (empty array if unavailable/invalid).
	 */
	private function parse_realtime_usage_payload( $usage_json ) {
		if ( empty( $usage_json ) ) {
			return array();
		}

		$decoded = json_decode( $usage_json, true );
		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $decoded ) ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Save Conversation: Invalid realtime_usage payload' );
			return array();
		}

		return array(
			'input_tokens'        => isset( $decoded['input_tokens'] ) ? max( 0, intval( $decoded['input_tokens'] ) ) : 0,
			'output_tokens'       => isset( $decoded['output_tokens'] ) ? max( 0, intval( $decoded['output_tokens'] ) ) : 0,
			'cached_input_tokens' => isset( $decoded['cached_input_tokens'] ) ? max( 0, intval( $decoded['cached_input_tokens'] ) ) : 0,
			'response_count'      => isset( $decoded['response_count'] ) ? max( 0, intval( $decoded['response_count'] ) ) : 0,
			'model'               => isset( $decoded['model'] ) ? sanitize_text_field( $decoded['model'] ) : 'realtime-session',
		);
	}

	/**
	 * Store Realtime conversation usage metrics in AI metadata table.
	 *
	 * @since 0.13.81
	 * @param int   $call_id        Saved call ID.
	 * @param array $realtime_usage Sanitized usage payload.
	 * @return bool True on success, false on failure.
	 */
	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
	private function log_realtime_usage_metadata( $call_id, $realtime_usage ) {
		global $wpdb;

		if ( empty( $call_id ) || empty( $realtime_usage ) ) {
			return false;
		}

		$table_name = $wpdb->prefix . 'phoneease_ai_metadata';
		$prompt_tokens = isset( $realtime_usage['input_tokens'] ) ? intval( $realtime_usage['input_tokens'] ) : 0;
		$completion_tokens = isset( $realtime_usage['output_tokens'] ) ? intval( $realtime_usage['output_tokens'] ) : 0;
		$cached_tokens = isset( $realtime_usage['cached_input_tokens'] ) ? intval( $realtime_usage['cached_input_tokens'] ) : 0;
		$response_count = isset( $realtime_usage['response_count'] ) ? intval( $realtime_usage['response_count'] ) : 0;

		// Skip zero-token conversation metadata rows when usage was not returned by Realtime events.
		if ( $response_count <= 0 && $prompt_tokens <= 0 && $completion_tokens <= 0 && $cached_tokens <= 0 ) {
			return false;
		}

		$inserted = $wpdb->insert(
			$table_name,
			array(
				'call_id'           => $call_id,
				'prompt_tokens'     => $prompt_tokens,
				'completion_tokens' => $completion_tokens,
				'cached_tokens'     => $cached_tokens,
				'total_tokens'      => $prompt_tokens + $completion_tokens + $cached_tokens,
				'model_used'        => isset( $realtime_usage['model'] ) ? $realtime_usage['model'] : 'realtime-session',
				'operation_type'    => 'conversation',
				'created_at'        => current_time( 'mysql' ),
			),
			array( '%d', '%d', '%d', '%d', '%d', '%s', '%s', '%s' )
		);

		if ( false === $inserted ) {
			SiteStaffr_Logger::legacy( 'SiteStaffr Save Conversation: Failed to log realtime usage metadata - ' . $wpdb->last_error );
			return false;
		}

		return true;
	}
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
}
