<?php
/**
 * SiteStaffr Fixed Position Widget
 *
 * Handles the [sitestaffr_widget] shortcode for fixed position floating widget.
 * Renders a floating button with WebRTC audio functionality.
 *
 * @link       https://github.com/sitestaffr/sitestaffr-plugin
 * @since      0.11.75
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

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

/**
 * SiteStaffr Fixed Position Widget class.
 *
 * Provides a fixed position floating widget that users can click to start
 * a voice conversation via WebRTC. Widget appearance is customizable via
 * shortcode attributes.
 *
 * @since      0.11.75
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @author     SiteStaffr
 */
class SiteStaffr_Widget {

	/**
	 * Whether the widget has already been rendered on this page load.
	 *
	 * Prevents double-rendering when both the shortcode and auto-display
	 * are active on the same page.
	 *
	 * @since    0.14.262
	 * @var      bool
	 */
	private $rendered = false;

	/**
	 * Initialize the widget.
	 *
	 * Registers the shortcode, auto-display hook, and enqueues assets.
	 *
	 * @since    0.11.75
	 */
	public function __construct() {
		// Register shortcode
		add_shortcode( 'sitestaffr_widget', array( $this, 'render_widget' ) );

		// Auto-display fixed widget on all frontend pages via wp_footer
		if ( ! is_admin() ) {
			add_action( 'wp_footer', array( $this, 'maybe_auto_display' ) );
		}

		// Register shared public AJAX handlers once across widget types.
		SiteStaffr_Public_Ajax_Router::register_shared_handlers( $this );
	}

	/**
	 * Auto-display the fixed widget in the footer if enabled and not already rendered.
	 *
	 * @since    0.14.262
	 */
	public function maybe_auto_display() {
		return SiteStaffr_Widget_Auto_Display_Handler::maybe_auto_display(
			function() {
				return $this->is_rendered();
			},
			function() {
				return $this->render_widget( array() );
			}
		);
	}

	/**
	 * Return whether the widget has already been rendered on this request.
	 *
	 * @since 0.14.262
	 * @return bool
	 */
	public function is_rendered() {
		return SiteStaffr_Widget_Is_Rendered_Handler::is_rendered( $this->rendered );
	}

	/**
	 * 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 fixed position widget.
	 *
	 * Shortcode: [sitestaffr_widget]
	 *
	 * Attributes:
	 *  - position: bottom-right (default), bottom-left
	 *  - background_color: hex color (default: #10b981)
	 *  - icon_color: hex color (default: #ffffff)
	 *  - size: button diameter in px (default: 60px)
	 *
	 * @since    0.11.75
	 * @param    array    $atts    Shortcode attributes.
	 * @return   string            Widget HTML.
	 */
	public function render_widget( $atts ) {
		return SiteStaffr_Widget_Render_Handler::render_widget(
			$this,
			$atts,
			function() {
				$this->rendered = true;
			},
			function( $mode = 'voice', $sounds_enabled = true ) {
				$this->enqueue_assets( $mode, $sounds_enabled );
			},
			function( $icon ) {
				return $this->get_fixed_icon_svg( $icon );
			}
		);
	}

	/**
	 * Enqueue widget assets (CSS and JavaScript).
	 *
	 * Enqueues styles and scripts required for widget 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 widget CSS
		wp_enqueue_style(
			'sitestaffr-widget',
			SITESTAFFR_PLUGIN_URL . 'public/css/sitestaffr-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 widget JavaScript (UI wrapper)
		wp_enqueue_script(
			'sitestaffr-widget',
			SITESTAFFR_PLUGIN_URL . 'public/js/sitestaffr-widget.js',
			array( 'sitestaffr-core' ),
			SITESTAFFR_VERSION,
			true
		);

		// Load settings once for data we need below.
		$settings = get_option( 'sitestaffr_settings', array() );

		// Pass PHP data to JavaScript
		wp_localize_script(
			'sitestaffr-widget',
			'sitestaffr_widget_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' ),
				'webrtc_nonce'     => wp_create_nonce( 'sitestaffr_webrtc_session' ),
				'realtime_nonce'   => wp_create_nonce( 'sitestaffr_realtime_session' ),
				'chat_nonce'              => wp_create_nonce( 'sitestaffr_chat_message' ),
				'knowledge_search_nonce' => wp_create_nonce( 'sitestaffr_knowledge_search' ),
				'chat_greeting'          => self::get_chat_greeting(),
				'current_post_id'  => get_the_ID(),
				'knowledge_mode'   => isset( $settings['knowledge_mode'] ) ? sanitize_key( $settings['knowledge_mode'] ) : 'search',
				'tooltipBehavior'  => self::get_tooltip_behavior(),
			'mode'             => $mode,
				'sounds_enabled'   => $sounds_enabled,
				'entitlement'      => ( class_exists( 'SiteStaffr_Billing_State' ) && SiteStaffr_Billing_State::is_subscription_inactive() ) ? 'inactive' : 'active',
				'strings'          => array(
					'idle'       => __( 'Talk to AI Assistant', 'sitestaffr' ),
					'listening'  => __( 'Listening...', 'sitestaffr' ),
					'thinking'   => __( 'Processing...', 'sitestaffr' ),
					'speaking'   => __( 'Speaking...', 'sitestaffr' ),
					'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 );
		}
	}

	/**
	 * Get the SVG markup for the fixed widget idle-state icon.
	 *
	 * Returns the appropriate SVG based on the selected icon type. All variants
	 * use the class name `sitestaffr-icon-phone` because that is the idle-state
	 * class targeted by the widget JavaScript.
	 *
	 * Get the chat greeting text.
	 *
	 * Uses custom greeting from settings if set, otherwise a default.
	 *
	 * @since 1.5.2
	 * @return string Greeting text.
	 */
	public static function get_tooltip_behavior() {
		$ws = get_option( 'sitestaffr_widget_settings', array() );
		if ( isset( $ws['fixed_tooltip_behavior'] ) ) {
			$behavior = sanitize_key( $ws['fixed_tooltip_behavior'] );
		} elseif ( ! empty( $ws['fixed_tooltip_always_show'] ) ) {
			$behavior = 'always';
		} else {
			$behavior = 'auto';
		}
		$valid = array( 'off', 'auto', 'hover', 'always' );
		return in_array( $behavior, $valid, true ) ? $behavior : 'auto';
	}

	public static function get_chat_greeting() {
		$settings = get_option( 'sitestaffr_settings', array() );
		$custom   = isset( $settings['custom_greeting'] ) ? trim( sanitize_text_field( $settings['custom_greeting'] ) ) : '';
		if ( $custom ) {
			return $custom;
		}
		$name = isset( $settings['business_name'] ) ? trim( sanitize_text_field( $settings['business_name'] ) ) : '';
		if ( $name ) {
			return sprintf(
				/* translators: %s: business name */
				__( 'Hi! Thanks for reaching out to %s. How can I help you today?', 'sitestaffr' ),
				$name
			);
		}
		return __( 'Hi! How can I help you today?', 'sitestaffr' );
	}

	/**
	 * @since    0.14.88
	 * @param    string    $icon    Icon type: 'phone', 'microphone', 'chat', 'headset'.
	 * @return   string             Safe SVG markup.
	 */
	private function get_fixed_icon_svg( $icon ) {
		switch ( $icon ) {
			case 'microphone':
				return '<svg class="sitestaffr-icon sitestaffr-icon-phone" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>';
			case 'chat':
				return '<svg class="sitestaffr-icon sitestaffr-icon-phone" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>';
			case 'headset':
				return '<svg class="sitestaffr-icon sitestaffr-icon-phone" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>';
			case 'sitestaffr':
				$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
					// Replace the outer SVG tag with our standardized attributes
					$svg_content = preg_replace(
						'/<svg[^>]*>/',
						'<svg class="sitestaffr-icon sitestaffr-icon-phone" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="74 104 412 412" fill="currentColor" stroke="none" aria-hidden="true">',
						$svg_content,
						1
					);
					return $svg_content;
				}
				// Fall through to phone if file missing
			case 'phone':
			default:
				return '<svg class="sitestaffr-icon sitestaffr-icon-phone" xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>';
		}
	}

	/**
	 * 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() {
		return SiteStaffr_Widget_Token_Handler::ajax_get_token( $this, $this->get_client_ip() );
	}

	/**
	 * 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() {
		return SiteStaffr_Widget_Realtime_Session_Handler::ajax_get_realtime_session(
			$this,
			$this->get_client_ip(),
			function() {
				return $this->get_api_secret();
			},
			function( $saved_settings ) {
				return $this->get_realtime_custom_instructions( $saved_settings );
			},
			function( $business_info ) {
				return SiteStaffr_Business_Enrichment::enrich_business_info( $business_info );
			}
		);
	}

	/**
	 * 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() {
		return SiteStaffr_Widget_WebRTC_Session_Handler::ajax_webrtc_session(
			$this,
			$this->get_client_ip(),
			function() {
				return $this->get_api_secret();
			},
			function( $saved_settings ) {
				return $this->get_realtime_custom_instructions( $saved_settings );
			},
			function( $business_info ) {
				return SiteStaffr_Business_Enrichment::enrich_business_info( $business_info );
			}
		);
	}

	/**
	 * 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.49
	 * @return void
	 */
	public function ajax_save_conversation() {
		return SiteStaffr_Public_Conversation_Usage::ajax_save_conversation( $this->get_client_ip() );
	}

	/**
	 * AJAX handler for usage finalization.
	 *
	 * Sends finalized conversation duration to middleware billing endpoint.
	 *
	 * @since 0.14.2
	 * @return void
	 */
	public function ajax_finalize_usage() {
		return SiteStaffr_Widget_Finalize_Usage_Handler::ajax_finalize_usage(
			$this,
			function() {
				return $this->get_api_secret();
			}
		);
	}

	/**
	 * 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 ) {
		return SiteStaffr_Public_Conversation_Usage::parse_realtime_usage_payload( $usage_json );
	}

	/**
	 * 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.
	 */
	private function log_realtime_usage_metadata( $call_id, $realtime_usage ) {
		return SiteStaffr_Public_Conversation_Usage::log_realtime_usage_metadata( $call_id, $realtime_usage );
	}
}




