<?php
/**
 * AI Client Proxy.
 *
 * PROXIES ALL AI REQUESTS VIA MIDDLEWARE
 *
 * All AI requests are routed through the SiteStaffr middleware which uses
 * a middleware-managed backend. This provides centralized API key management
 * and consistent AI processing across all features.
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 * @since      1.0.0
 * @updated    0.6.4 - Switched from legacy API to middleware
 * @updated    0.7.5 - Renamed from legacy client class to SiteStaffr_AI_Client
 */

/**
 * AI Client Proxy class.
 *
 * Provides methods for sending messages to AI via middleware,
 * validating authentication, logging usage metrics, and handling errors gracefully.
 *
 * @since 1.0.0
 */
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin table names are internally derived from $wpdb->prefix and values are placeholder-prepared.
class SiteStaffr_AI_Client {

	/**
	 * Middleware API endpoint for conversations
	 *
	 * @since 0.6.4
	 * @var string
	 */
	/**
	 * Get the API endpoint URL for receptionist requests.
	 *
	 * @return string Full URL to the middleware receptionist endpoint.
	 */
	public static function get_api_endpoint() {
		return rtrim( SITESTAFFR_MIDDLEWARE_URL, '/' ) . '/api/v1/receptionist';
	}

	/**
	 * DEPRECATED: API Secret constant removed for security (v0.10.101)
	 *
	 * SECURITY: API secret is now stored in WordPress options table (sitestaffr_api_secret)
	 * instead of being hardcoded in source code. This prevents exposure in Git history.
	 *
	 * Use get_api_secret() method to retrieve the secret from WordPress options.
	 *
	 * @since 0.9.49
	 * @deprecated 0.10.101
	 */

	/**
	 * Default AI model (for logging purposes)
	 *
	 * @since 1.0.0
	 * @updated 0.9.27 - Changed from gemini-2.0-flash-exp to gemini-2.5-flash
	 * @updated 0.13.77 - Changed default to middleware-managed model compatibility
	 * @var string
	 */
	const DEFAULT_MODEL = 'middleware-default';

	/**
	 * Request timeout (seconds)
	 *
	 * Increased from 8s to 15s to accommodate slower middleware responses.
	 * External call routing has strict limits, but WordPress needs time for:
	 * - Middleware call (3-5s including AI processing)
	 * - Response processing (1s)
	 * - TwiML generation (1s)
	 * Total: 15s provides adequate buffer without risking timeout.
	 *
	 * @since 1.0.0
	 * @updated 0.9.12 - Increased from 8s to 15s
	 * @var int
	 */
	const TIMEOUT = 15;

	/**
	 * Site token for middleware authentication
	 *
	 * @since 0.6.4
	 * @var string
	 */
	private $site_token;

	/**
	 * Constructor.
	 *
	 * Initializes the AI client with site token for middleware authentication.
	 * The $api_key parameter is kept for backward compatibility but is ignored.
	 *
	 * @since 1.0.0
	 * @param string $api_key Deprecated - kept for backward compatibility, ignored.
	 */
	public function __construct( $api_key = '' ) {
		// Get or generate site token for middleware authentication
		$this->site_token = get_option( 'sitestaffr_site_token' );
		if ( empty( $this->site_token ) ) {
			$this->site_token = wp_generate_password( 32, false );
			update_option( 'sitestaffr_site_token', $this->site_token );
		}
	}

	/**
	 * Send message to AI via middleware.
	 *
	 * Sends a conversation message array to the SiteStaffr middleware.
	 * Accepts legacy-compatible message format and converts to middleware format.
	 *
	 * @since 1.0.0
	 *
	 * @param array $messages Conversation messages in AI API format (legacy-compatible).
	 *                        Example: [['role' => 'user', 'content' => 'Hello, how can I schedule an appointment?']]
	 * @param array $options  Optional parameters (model, max_tokens, temperature, system, call_id).
	 *                        Note: Some parameters may not be supported by middleware.
	 *                        call_id: Required for context chaining (tracks last_response_id)
	 *
	 * @return array|WP_Error Response array on success, WP_Error on failure.
	 */
	public function send_message( $messages, $options = array() ) {
		// Validate site token
		if ( empty( $this->site_token ) ) {
			return new WP_Error(
				'missing_site_token',
				'Site token not configured for middleware authentication.'
			);
		}

		// Default options
		$defaults = array(
			'model'       => self::DEFAULT_MODEL,
			'max_tokens'  => 300,
			'temperature' => 0.7,
			'system'      => '',
		);
		$options = wp_parse_args( $options, $defaults );

		// Format messages for middleware
		// Extract just the latest user message - middleware will handle context from business_info
		// Don't send system instructions as part of message (causes AI to respond to the instructions!)
		$latest_message = '';

		// Get the last message from the array
		if ( ! empty( $messages ) ) {
			$last_msg = end( $messages );
			$latest_message = isset( $last_msg['content'] ) ? $last_msg['content'] : '';
		}

		// Build conversation history (EXCLUDE current/last message)
		// Middleware will add current message separately to avoid duplication
		$conversation_history = array();
		$message_count = count( $messages );

		// Only include messages BEFORE the last one (exclude current message)
		for ( $i = 0; $i < $message_count - 1; $i++ ) {
			$message = $messages[ $i ];
			$conversation_history[] = array(
				'role'    => isset( $message['role'] ) ? $message['role'] : 'user',
				'content' => isset( $message['content'] ) ? $message['content'] : '',
			);
		}

		// Get business context for AI
		$settings = get_option( 'sitestaffr_settings', array() );

		// Get business hours - ONLY from settings (never from scraped knowledge)
		// Business hours are user-configured data, not scraped content (BUG-019 fix)
		$business_hours = '';
		if ( ! empty( $settings['business_hours'] ) ) {
			// BUG-025 fix: Validate business hours to prevent corrupt data like "Minutes seconds"
			$hours_lower = strtolower( trim( $settings['business_hours'] ) );
			$corrupt_patterns = array( 'minutes seconds', 'minutes', 'seconds' );

			if ( ! in_array( $hours_lower, $corrupt_patterns, true ) ) {
				$business_hours = $settings['business_hours'];
			} else {
				// Log corruption and skip corrupted hours
				SiteStaffr_Logger::legacy( 'SiteStaffr AI Client: Corrupt business hours detected and ignored: "' . $settings['business_hours'] . '"' );
				$business_hours = '';
			}
		}

		$business_info = array(
			'business_name'        => isset( $settings['business_name'] ) ? $settings['business_name'] : '',
			'business_hours'       => $business_hours,  // Trust that data has been validated at save time
			'business_description' => isset( $settings['business_context'] ) ? $settings['business_context'] : '',
			'business_phone'       => isset( $settings['business_phone'] ) ? $settings['business_phone'] : '',
			'custom_instructions'  => isset( $settings['custom_instructions'] ) ? sanitize_textarea_field( $settings['custom_instructions'] ) : '',
			'custom_greeting'      => isset( $settings['custom_greeting'] ) ? sanitize_text_field( $settings['custom_greeting'] ) : '',
			'custom_greeting_tone' => isset( $settings['custom_greeting_tone'] ) ? sanitize_key( $settings['custom_greeting_tone'] ) : 'warm_neutral',
		);
		$business_info = $this->enrich_business_info_with_fallbacks( $business_info );

		// Get selected AI model from settings.
		$selected_model = isset( $settings['ai_model'] ) ? $settings['ai_model'] : self::DEFAULT_MODEL;

		// Safety fallback: map deprecated model aliases to middleware default.
		if ( is_string( $selected_model ) && strpos( strtolower( $selected_model ), 'claude-' ) === 0 ) {
			SiteStaffr_Logger::legacy( "SiteStaffr AI Client: Deprecated model '{$selected_model}' mapped to " . self::DEFAULT_MODEL );
			$selected_model = self::DEFAULT_MODEL;
		}

		// Get last_response_id for context chaining (when call_id is provided)
		$previous_response_id = null;
		if ( ! empty( $options['call_id'] ) ) {
			$call_id = intval( $options['call_id'] );
			$table_name = $wpdb->prefix . 'phoneease_calls';

			$previous_response_id = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT last_response_id FROM {$table_name} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- trusted plugin table name.
					$call_id
				)
			);

			}

		// Build request body for middleware
		$timestamp = time(); // Unix timestamp for replay attack prevention
		$body = array(
			'site_token'           => $this->site_token,  // Note: middleware expects snake_case
			'message'              => $latest_message,    // Just the latest user message
			'conversation_history' => $conversation_history,  // Full conversation for context
			'context'              => 'conversation',     // Phone call context
			'business_info'        => $business_info,     // Business context (middleware uses this for system prompt)
			'system_prompt'        => $options['system'], // CRITICAL: Full 187-line system prompt from WordPress (includes caller ID, detailed instructions, etc.)
			'model'                => $selected_model,    // Selected AI model from settings
			'timestamp'            => $timestamp,         // SECURITY: Timestamp for signature validation
		);

		// Add previous_response_id for context chaining (if available)
		if ( ! empty( $previous_response_id ) ) {
			$body['previous_response_id'] = $previous_response_id;
		}

		// Allow $options['model'] to override setting (for multi-model testing via TestModel parameter)
		if ( ! empty( $options['model'] ) ) {
			$body['model'] = $options['model'];
			if ( is_string( $body['model'] ) && strpos( strtolower( $body['model'] ), 'claude-' ) === 0 ) {
				SiteStaffr_Logger::legacy( "SiteStaffr AI Client: Deprecated override model '{$body['model']}' mapped to " . self::DEFAULT_MODEL );
				$body['model'] = self::DEFAULT_MODEL;
			}
		}

		// SECURITY: Generate HMAC signature for request authentication
		$signature = $this->generate_request_signature( $body, $timestamp );

		// Send request with comprehensive timing and retry logic
		$start_time = microtime( true );
		$max_retries = 1; // Retry once before giving up
		$retry_delay_ms = 1000; // 1 second delay between retries
		$response = null;
		$last_error = null;

		for ( $attempt = 0; $attempt <= $max_retries; $attempt++ ) {
			if ( $attempt > 0 ) {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					SiteStaffr_Logger::legacy( "│ SiteStaffr: Retry attempt $attempt after " . ( $retry_delay_ms / 1000 ) . "s delay..." );
				}
				usleep( $retry_delay_ms * 1000 ); // Convert ms to microseconds
			}

			$attempt_start = microtime( true );

			$response = wp_remote_post(
				self::get_api_endpoint(),
				array(
					'headers' => array(
						'Content-Type'          => 'application/json',
						'X-SiteStaffr-Signature' => $signature,
						'X-SiteStaffr-Timestamp' => $timestamp,
					),
					'body'    => wp_json_encode( $body ),
					'timeout' => self::TIMEOUT,
				)
			);

			$attempt_time_ms = round( ( microtime( true ) - $attempt_start ) * 1000 );

			// Check if request succeeded
			if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
				if ( $attempt > 0 && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
					SiteStaffr_Logger::legacy( "│ SiteStaffr: Retry successful on attempt $attempt!" );
				}
				break; // Success - exit retry loop
			}

			// Store error for later logging
			$last_error = is_wp_error( $response ) ? $response : new WP_Error(
				'http_error',
				'HTTP ' . wp_remote_retrieve_response_code( $response )
			);

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_msg = is_wp_error( $response ) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code( $response );
				SiteStaffr_Logger::legacy( "│ SiteStaffr: Attempt $attempt failed - $error_msg (took {$attempt_time_ms}ms)" );
			}
		}

		$response_time_ms = round( ( microtime( true ) - $start_time ) * 1000 );

		// Handle errors (after all retries exhausted)
		if ( is_wp_error( $response ) ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				SiteStaffr_Logger::legacy( '│ ERROR: ' . $response->get_error_message() );
				SiteStaffr_Logger::legacy( '│ Error Code: ' . $response->get_error_code() );
				SiteStaffr_Logger::legacy( '│ All retry attempts exhausted' );
				SiteStaffr_Logger::legacy( '└─────────────────────────────────────────────────────────────' );
			}
			return $response;
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		$response_body = wp_remote_retrieve_body( $response );

		if ( $response_code !== 200 ) {
			// ENHANCED ERROR LOGGING for debugging
			SiteStaffr_Logger::legacy( "SiteStaffr Middleware Error: HTTP $response_code" );
			SiteStaffr_Logger::legacy( "SiteStaffr Middleware Response Body: " . $response_body );
			SiteStaffr_Logger::legacy( "SiteStaffr Middleware Request was: " . wp_json_encode( $body ) );

			return new WP_Error(
				'api_error',
				sprintf( 'Middleware returned error: %s - %s', $response_code, $response_body ),
				array( 'response' => $response_body )
			);
		}

		// Parse response
		$data = json_decode( $response_body, true );

		if ( json_last_error() !== JSON_ERROR_NONE ) {
			return new WP_Error( 'json_error', 'Failed to parse middleware response' );
		}

		// Check for middleware error
		if ( ! isset( $data['success'] ) || ! $data['success'] ) {
			// Check if this is a fallback_mode response (AI service unavailable)
			if ( isset( $data['fallback_mode'] ) && $data['fallback_mode'] === true ) {
				SiteStaffr_Logger::legacy( 'SiteStaffr: AI service unavailable - fallback_mode triggered' );
				// Return special error code to trigger voicemail fallback
				return new WP_Error(
					'ai_fallback_mode',
					isset( $data['message'] ) ? $data['message'] : 'AI service temporarily unavailable',
					array( 'fallback_mode' => true )
				);
			}

			$error_msg = isset( $data['error'] ) ? $data['error'] : 'Unknown middleware error';
			SiteStaffr_Logger::legacy( 'SiteStaffr Middleware Error: ' . $error_msg );
			return new WP_Error( 'middleware_error', $error_msg );
		}

		// Convert middleware response to legacy format for backward compatibility
		$ai_response_text = isset( $data['ai_response'] ) ? $data['ai_response'] : '';

		// Extract model used from middleware response (for automatic model tracking)
		$model_used = isset( $data['model_used'] ) ? $data['model_used'] : self::DEFAULT_MODEL;

		// Extract token usage from middleware response (if available)
		$usage_data = array(
			'input_tokens'  => 0,
			'output_tokens' => 0,
		);

		if ( isset( $data['usage'] ) && is_array( $data['usage'] ) ) {
			// Middleware returns normalized usage format
			if ( isset( $data['usage']['input_tokens'] ) ) {
				$usage_data['input_tokens'] = intval( $data['usage']['input_tokens'] );
			}
			if ( isset( $data['usage']['output_tokens'] ) ) {
				$usage_data['output_tokens'] = intval( $data['usage']['output_tokens'] );
			}
			// Also check for cache read tokens (prompt caching)
			if ( isset( $data['usage']['cache_read_input_tokens'] ) ) {
				$usage_data['cache_read_input_tokens'] = intval( $data['usage']['cache_read_input_tokens'] );
			}
			// Calculate total tokens
			$usage_data['total_tokens'] = $usage_data['input_tokens'] + $usage_data['output_tokens'];
		}

		// Save response_id to database for context chaining (if provided and call_id available)
		if ( isset( $data['response_id'] ) && ! empty( $options['call_id'] ) ) {
			$call_id = intval( $options['call_id'] );
			$response_id = sanitize_text_field( $data['response_id'] );
			$table_name = $wpdb->prefix . 'phoneease_calls';

			$update_result = $wpdb->update(
				$table_name,
				array( 'last_response_id' => $response_id ),
				array( 'id' => $call_id ),
				array( '%s' ),
				array( '%d' )
			);

			if ( $update_result === false ) {
				SiteStaffr_Logger::legacy( "SiteStaffr: Failed to save response_id - {$wpdb->last_error}" );
			}
		}

		// Build response in legacy API format so existing code works
		$claude_format_response = array(
			'id'      => 'msg_' . wp_generate_password( 20, false ),
			'type'    => 'message',
			'role'    => 'assistant',
			'content' => array(
				array(
					'type' => 'text',
					'text' => $ai_response_text,
				),
			),
			'model'   => $model_used,  // Use actual model from middleware response
			'usage'   => $usage_data,  // Token usage from middleware (extracted above)
			'_response_time_ms' => $response_time_ms,
			'_model_used'       => $model_used,  // Store for easy access by conversation manager
		);

		return $claude_format_response;
	}

	/**
	 * Validate site token by sending test request to middleware.
	 *
	 * Sends a minimal test message to verify middleware connection is working.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if valid, false otherwise.
	 */
	public function validate_api_key() {
		$test_messages = array(
			array(
				'role'    => 'user',
				'content' => 'Test',
			),
		);

		$response = $this->send_message( $test_messages );

		return ! is_wp_error( $response );
	}

	/**
	 * Log API usage to database.
	 *
	 * Stores API usage metrics (tokens, response time, errors) to wp_sitestaffr_ai_metadata table
	 * for cost tracking and performance monitoring.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $call_id      Call ID from wp_sitestaffr_calls.
	 * @param array $api_response Response from AI API or WP_Error object.
	 * @param bool  $is_error     Whether this was an error response.
	 *
	 * @return int|false Insert ID on success, false on failure.
	 */
	public function log_api_usage( $call_id, $api_response, $is_error = false ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'phoneease_ai_metadata';

		// Initialize data array with defaults
		$data = array(
			'call_id'              => $call_id,
			'model_used'           => self::DEFAULT_MODEL,
			'prompt_tokens'        => 0,
			'completion_tokens'    => 0,
			'cached_tokens'        => 0,
			'total_tokens'         => 0,
			'api_response_time_ms' => 0,
			'error_message'        => null,
			'created_at'           => current_time( 'mysql' ),
		);

		// Check if $api_response is a WP_Error object (happens on timeout/network errors)
		if ( is_wp_error( $api_response ) ) {
			// Handle WP_Error - don't try to access array keys
			$data['error_message'] = $api_response->get_error_message();
		} else {
			// Handle successful response or error array - safe to access array keys
			$data['model_used']           = isset( $api_response['model'] ) ? $api_response['model'] : self::DEFAULT_MODEL;
			$data['api_response_time_ms'] = isset( $api_response['_response_time_ms'] ) ? $api_response['_response_time_ms'] : 0;

			if ( $is_error ) {
				$data['error_message'] = isset( $api_response['error']['message'] ) ? $api_response['error']['message'] : 'Unknown error';
			} else {
				// Extract token usage from successful response
				if ( isset( $api_response['usage'] ) ) {
					$data['prompt_tokens']     = $api_response['usage']['input_tokens'];
					$data['completion_tokens'] = $api_response['usage']['output_tokens'];
					$data['cached_tokens']     = isset( $api_response['usage']['cache_read_input_tokens'] ) ? $api_response['usage']['cache_read_input_tokens'] : 0;
					$data['total_tokens']      = $data['prompt_tokens'] + $data['completion_tokens'] + $data['cached_tokens'];
				}
			}
		}

		$result = $wpdb->insert(
			$table_name,
			$data,
			array( '%d', '%s', '%d', '%d', '%d', '%d', '%d', '%s', '%s' )
		);

		if ( $result === false ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				SiteStaffr_Logger::legacy( 'SiteStaffr: Failed to log API usage - ' . $wpdb->last_error );
			}
			return false;
		}

		return $wpdb->insert_id;
	}

	/**
	 * Extract text from AI API response.
	 *
	 * Parses the AI API response structure to extract the assistant's text message.
	 * Response is in legacy API format for backward compatibility; this method finds the first text block.
	 *
	 * @since 1.0.0
	 *
	 * @param array $response AI API response array (legacy-compatible format).
	 *
	 * @return string|WP_Error Extracted text on success, WP_Error on failure.
	 */
	public function extract_response_text( $response ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( ! isset( $response['content'] ) || ! is_array( $response['content'] ) ) {
			return new WP_Error( 'invalid_response', 'Invalid AI API response structure' );
		}

		// Response uses legacy API format (array of content blocks)
		// Find the first text block
		foreach ( $response['content'] as $block ) {
			if ( isset( $block['type'] ) && $block['type'] === 'text' ) {
				return $block['text'];
			}
		}

		return new WP_Error( 'no_text_content', 'No text content in AI API response' );
	}

	/**
	 * Enrich business_info payload with deterministic fallbacks when fields are missing.
	 *
	 * @since 0.14.255
	 *
	 * @param array $business_info Business info payload.
	 * @return array
	 */
	private function enrich_business_info_with_fallbacks( $business_info ) {
		if ( ! is_array( $business_info ) ) {
			return array();
		}

		$description = isset( $business_info['business_description'] ) && is_string( $business_info['business_description'] )
			? trim( $business_info['business_description'] )
			: '';
		$phone = isset( $business_info['business_phone'] ) && is_string( $business_info['business_phone'] )
			? trim( $business_info['business_phone'] )
			: '';
		$custom_instructions = isset( $business_info['custom_instructions'] ) && is_string( $business_info['custom_instructions'] )
			? trim( $business_info['custom_instructions'] )
			: '';

		$missing_description_response = __( "I don't have specific details about our services right now, but I'd be happy to have someone from our team follow up with more information. Would you like that?", 'sitestaffr' );
		$missing_phone_response = __( "I don't have a public business phone number available right now, but I can have someone from our team follow up with you directly.", 'sitestaffr' );

		$runtime_instructions = array();
		if ( '' === $description ) {
			$business_info['business_description'] = __( 'No business description has been provided yet. Do not guess services; use the configured fallback response and offer follow-up.', 'sitestaffr' );
			$runtime_instructions[] = sprintf(
				/* translators: %s is a fallback response the AI should use */
				__( 'If asked what the business does and no business description is configured, reply exactly: %s', 'sitestaffr' ),
				$missing_description_response
			);
		} else {
			$business_info['business_description'] = $description;
		}

		if ( '' === $phone ) {
			$runtime_instructions[] = sprintf(
				/* translators: %s is a fallback response the AI should use */
				__( 'If asked for the business phone number and none is configured, reply exactly: %s', 'sitestaffr' ),
				$missing_phone_response
			);
		}

		$business_info['business_phone'] = $phone;
		$business_info['business_description_available'] = ( '' !== $description );
		$business_info['business_phone_available'] = ( '' !== $phone );
		$business_info['missing_description_response'] = $missing_description_response;
		$business_info['missing_business_phone_response'] = $missing_phone_response;

		if ( ! empty( $runtime_instructions ) ) {
			$business_info['custom_instructions'] = trim( $custom_instructions . "\n\n" . implode( "\n", $runtime_instructions ) );
		} else {
			$business_info['custom_instructions'] = $custom_instructions;
		}

		return $business_info;
	}

	/**
	 * Get API secret from WordPress options.
	 *
	 * SECURITY: Retrieves the API secret from WordPress options table.
	 * Auto-generates a new secret if one doesn't exist (for edge cases).
	 *
	 * @since 0.10.101
	 * @return string API secret for HMAC signing.
	 */
	private function get_api_secret() {
		// SECURITY: Always retrieve secret from WordPress options - NEVER hardcode secrets
		$api_secret = get_option( 'sitestaffr_api_secret' );

		// Auto-generate if missing (should not happen after activation, but safety check)
		if ( empty( $api_secret ) ) {
			$api_secret = bin2hex( random_bytes( 32 ) );
			update_option( 'sitestaffr_api_secret', $api_secret );
			SiteStaffr_Logger::legacy( 'SiteStaffr AI Client: Auto-generated missing API secret (this should not happen - check activation hook)' );
		}

		return $api_secret;
	}

	/**
	 * Generate HMAC signature for request authentication.
	 *
	 * SECURITY: This prevents unauthorized access to the middleware by signing requests.
	 * The middleware validates this signature using the same secret.
	 *
	 * Signature calculation:
	 * 1. JSON encode the request body (ensures consistent serialization)
	 * 2. Generate HMAC-SHA256 using API secret from WordPress options
	 * 3. Encode as base64 for transmission
	 *
	 * @since 0.9.49
	 * @updated 0.10.101 - Use get_api_secret() instead of hardcoded constant
	 * @param array $body      Request body to sign.
	 * @param int   $timestamp Request timestamp used by X-SiteStaffr-Timestamp.
	 * @return string Base64-encoded HMAC signature.
	 */
	private function generate_request_signature( $body, $timestamp ) {
		// JSON encode for consistent string representation
		$payload = wp_json_encode( $body );
		$payload_to_sign = (string) $timestamp . '.' . $payload;

		// Generate HMAC-SHA256 signature using secret from WordPress options
		$signature = hash_hmac( 'sha256', $payload_to_sign, $this->get_api_secret(), true );

		// Encode as base64 for HTTP header transmission
		return base64_encode( $signature );
	}
}
// phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
