<?php
/**
 * Shared middleware request client for public AJAX handlers.
 *
 * @since      0.14.323
 * @package    SiteStaffr
 * @subpackage SiteStaffr/public
 */

/**
 * SiteStaffr Public Middleware Client class.
 *
 * Normalizes outbound middleware request handling so widget handlers keep
 * consistent response parsing without changing endpoint contracts.
 *
 * @since      0.14.323
 * @package    SiteStaffr
 * @subpackage SiteStaffr/public
 * @author     SiteStaffr
 */
class SiteStaffr_Public_Middleware_Client {

	/**
	 * Send raw JSON POST request to middleware.
	 *
	 * Returns the direct wp_remote_request result without normalization.
	 *
	 * @since 0.14.323
	 *
	 * @param string       $url     Target URL.
	 * @param array        $headers Request headers.
	 * @param array|string $body    Request body.
	 * @param int          $timeout Timeout seconds.
	 * @return array|WP_Error
	 */
	public static function post_json_raw( $url, $headers, $body, $timeout = 15 ) {
		$request_body = is_string( $body ) ? $body : wp_json_encode( $body );

		return wp_remote_request(
			$url,
			array(
				'method'  => self::http_method_post(),
				'headers' => $headers,
				'body'    => $request_body,
				'timeout' => $timeout,
			)
		);
	}

	/**
	 * Send JSON POST request to middleware.
	 *
	 * @since 0.14.323
	 *
	 * @param string       $url     Target URL.
	 * @param array        $headers Request headers.
	 * @param array|string $body    Request body.
	 * @param int          $timeout Timeout seconds.
	 * @return array|WP_Error
	 * @deprecated 0.14.323 Use post_signed_json_with_json_response() for signed JSON calls, post_signed_sdp_with_json_response() for signed SDP calls, or post_json_raw()/request_raw() for raw request needs.
	 * @internal Compatibility wrapper retained for potential external callers.
	 */
	public static function post_json( $url, $headers, $body, $timeout = 15 ) {
		$response = self::post_json_raw( $url, $headers, $body, $timeout );
		return self::normalize_response( $response );
	}

	/**
	 * Send JSON POST request and return normalized + decoded JSON response.
	 *
	 * @since 0.14.323
	 *
	 * @param string       $url     Target URL.
	 * @param array|string $payload Request payload.
	 * @param array        $headers Request headers.
	 * @param int          $timeout Timeout seconds.
	 * @return array{status_code:int,body:string,decoded_body:mixed,json_error:int}|WP_Error
	 */
	public static function post_json_with_json_response( $url, $payload, $headers, $timeout = 15 ) {
		$response = self::post_json_raw( $url, $headers, $payload, $timeout );
		return self::normalize_response_with_json( $response );
	}

	/**
	 * Build middleware endpoint from base URL and API path.
	 *
	 * @since 0.14.323
	 *
	 * @param string $middleware_url Middleware base URL.
	 * @param string $path           API path (for example: /api/realtime/session).
	 * @return string
	 */
	public static function build_middleware_endpoint( $middleware_url, $path ) {
		$base_url = rtrim( (string) $middleware_url, '/' );
		$normalized_path = '/' . ltrim( (string) $path, '/' );
		return $base_url . $normalized_path;
	}

	/**
	 * Realtime session middleware path.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function path_realtime_session() {
		return '/api/realtime/session';
	}

	/**
	 * WebRTC session middleware path.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function path_webrtc_session() {
		return '/api/realtime/session/webrtc';
	}

	/**
	 * Finalize usage middleware path.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function path_finalize_usage() {
		return '/api/usage/finalize';
	}

	/**
	 * Conversation-success middleware event path.
	 *
	 * @since 1.0.2
	 *
	 * @return string
	 */
	public static function path_conversation_success_event() {
		return '/api/events/conversation-success';
	}

	/**
	 * Send notification email middleware path.
	 *
	 * @since 0.14.269
	 *
	 * @return string
	 */
	public static function path_send_notification_email() {
		return '/api/email/send-notification';
	}

	/**
	 * Send test email middleware path.
	 *
	 * @since 0.14.269
	 *
	 * @return string
	 */
	public static function path_send_test_email() {
		return '/api/email/send-test';
	}

	/**
	 * Email request middleware timeout in seconds.
	 *
	 * @since 0.14.269
	 *
	 * @return int
	 */
	public static function timeout_email() {
		return 15;
	}

	/**
	 * Email notification log prefix.
	 *
	 * @since 0.14.269
	 *
	 * @return string
	 */
	public static function log_prefix_email() {
		return 'SiteStaffr Email: ';
	}

	/**
	 * Realtime session middleware timeout in seconds.
	 *
	 * @since 0.14.323
	 *
	 * @return int
	 */
	public static function timeout_realtime_session() {
		return 30;
	}

	/**
	 * WebRTC session middleware timeout in seconds.
	 *
	 * @since 0.14.323
	 *
	 * @return int
	 */
	public static function timeout_webrtc_session() {
		return 60;
	}

	/**
	 * Finalize usage middleware timeout in seconds.
	 *
	 * @since 0.14.323
	 *
	 * @return int
	 */
	public static function timeout_finalize_usage() {
		return 15;
	}

	/**
	 * Conversation-success middleware timeout in seconds.
	 *
	 * @since 1.0.2
	 *
	 * @return int
	 */
	public static function timeout_conversation_success_event() {
		return 15;
	}

	/**
	 * JSON content-type header value.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function content_type_json() {
		return 'application/json';
	}

	/**
	 * SDP content-type header value.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function content_type_sdp() {
		return 'application/sdp';
	}

	/**
	 * Standard JSON extra headers for signed middleware requests.
	 *
	 * @since 0.14.323
	 *
	 * @return array<string,string>
	 */
	public static function extra_headers_json() {
		return array(
			'Content-Type' => self::content_type_json(),
		);
	}

	/**
	 * Standard SDP extra headers for signed middleware requests.
	 *
	 * @since 0.14.323
	 *
	 * @return array<string,string>
	 */
	public static function extra_headers_sdp() {
		return array(
			'Content-Type' => self::content_type_sdp(),
		);
	}

	/**
	 * Realtime-session AJAX log prefix.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_prefix_realtime_session_ajax() {
		return 'SiteStaffr Realtime Session (AJAX): ';
	}

	/**
	 * WebRTC AJAX log prefix.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_prefix_webrtc_sdp_ajax() {
		return 'SiteStaffr WebRTC SDP (AJAX): ';
	}

	/**
	 * Finalize-usage log prefix.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_prefix_finalize_usage() {
		return 'SiteStaffr Finalize Usage: ';
	}

	/**
	 * Conversation-success log prefix.
	 *
	 * @since 1.0.2
	 *
	 * @return string
	 */
	public static function log_prefix_conversation_success() {
		return 'SiteStaffr Conversation Success: ';
	}

	/**
	 * Shared logger suffix for request-failed branches.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_request_failed() {
		return 'Request failed - ';
	}

	/**
	 * Shared logger suffix for invalid JSON middleware responses.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_invalid_json_response_from_middleware() {
		return 'Invalid JSON response from middleware';
	}

	/**
	 * Shared logger suffix for missing API secret diagnostics.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_missing_api_secret() {
		return 'Missing API secret';
	}

	/**
	 * Shared logger suffix for missing middleware URL diagnostics.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_missing_middleware_url() {
		return 'Missing middleware URL';
	}

	/**
	 * Shared logger sprintf format for middleware status/body logs.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_fmt_middleware_returned_status_body() {
		return 'Middleware returned %s';
	}

	/**
	 * Widget-token nonce-failure log format (full message).
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_fmt_widget_token_nonce_failed_for_ip() {
		return 'SiteStaffr Widget Token (AJAX): Nonce verification failed for IP %s';
	}

	/**
	 * Shared logger suffix for nonce-failure IP diagnostics.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_nonce_failed_for_ip() {
		return 'Nonce verification failed for IP %s';
	}

	/**
	 * Shared logger suffix for nonce-failure diagnostics.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_nonce_failed() {
		return 'Nonce verification failed';
	}

	/**
	 * Shared logger suffix for missing-site-token diagnostics.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_missing_site_token() {
		return 'Missing site token';
	}

	/**
	 * Shared logger suffix for missing site-token request diagnostics.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function log_suffix_missing_site_token_in_request() {
		return 'Missing site token in request';
	}

	/**
	 * Signed-request signature header name.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function header_signature() {
		return 'X-SiteStaffr-Signature';
	}

	/**
	 * Signed-request timestamp header name.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function header_timestamp() {
		return 'X-SiteStaffr-Timestamp';
	}

	/**
	 * Signed-request business info header name.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function header_business_info() {
		return 'X-SiteStaffr-Business-Info';
	}

	/**
	 * Signed-request visitor IP header name.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function header_visitor_ip() {
		return 'X-SiteStaffr-Visitor-IP';
	}

	/**
	 * Middleware invalid-response message text.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_invalid_middleware_response() {
		return 'Invalid response from middleware';
	}

	/**
	 * Middleware service-unavailable message text.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_service_temporarily_unavailable() {
		return 'Service temporarily unavailable';
	}

	/**
	 * Middleware generic error prefix.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_middleware_error_prefix() {
		return 'Middleware error: ';
	}

	/**
	 * Middleware connection failure prefix.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_failed_to_connect_to_middleware_prefix() {
		return 'Failed to connect to middleware: ';
	}

	/**
	 * Finalize-usage request failure message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_finalize_failed() {
		return 'Failed to finalize usage';
	}

	/**
	 * Finalize-usage generic failure message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_usage_finalization_failed() {
		return 'Usage finalization failed';
	}

	/**
	 * Conversation-success request failure message.
	 *
	 * @since 1.0.2
	 *
	 * @return string
	 */
	public static function msg_conversation_success_request_failed() {
		return 'Failed to record conversation success';
	}

	/**
	 * Conversation-success no-op message.
	 *
	 * @since 1.0.2
	 *
	 * @return string
	 */
	public static function msg_conversation_success_not_recorded() {
		return 'Conversation success not recorded';
	}

	/**
	 * Missing API secret configuration message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_config_missing_api_secret() {
		return 'Configuration error: Missing API secret';
	}

	/**
	 * Missing middleware URL configuration message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_config_missing_middleware_url() {
		return 'Configuration error: Missing middleware URL';
	}

	/**
	 * Generic nonce/honeypot security failure message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_security_verification_failed() {
		return 'Security verification failed';
	}

	/**
	 * Generic security failure message used by save/finalize handlers.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_security_check_failed() {
		return 'Security check failed';
	}

	/**
	 * Global service unavailable message used by token-rate-limit branch.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_service_temporarily_unavailable_try_again_later() {
		return 'Service temporarily unavailable. Please try again later.';
	}

	/**
	 * Generic request rate-limit message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_too_many_requests_try_again_minute() {
		return 'Too many requests. Please try again in about a minute.';
	}

	/**
	 * Realtime session rate-limit message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_too_many_session_requests() {
		return 'Too many session requests. Please try again later.';
	}

	/**
	 * WebRTC session rate-limit message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_too_many_webrtc_session_requests() {
		return 'Too many WebRTC session requests. Please try again later.';
	}

	/**
	 * Save-conversation rate-limit message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_too_many_save_requests() {
		return 'Too many save requests. Please try again later.';
	}

	/**
	 * Visitor banned message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_visitor_banned() {
		return 'This visitor is banned.';
	}

	/**
	 * Missing site-token configuration message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_config_missing_site_token() {
		return 'Configuration error: Missing site token';
	}

	/**
	 * Missing site-token request message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_missing_site_token() {
		return 'Missing site token';
	}

	/**
	 * Missing SDP offer message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_missing_sdp_offer() {
		return 'Missing SDP offer';
	}

	/**
	 * Invalid SDP format message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_invalid_sdp_format() {
		return 'Invalid SDP format';
	}

	/**
	 * SDP size-limit exceeded message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_sdp_offer_too_large() {
		return 'SDP offer too large';
	}

	/**
	 * Missing required-fields message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_missing_required_fields() {
		return 'Missing required fields';
	}

	/**
	 * Invalid transcript format message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_invalid_transcript_format() {
		return 'Invalid transcript format';
	}

	/**
	 * Empty transcript message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_transcript_cannot_be_empty() {
		return 'Transcript cannot be empty';
	}

	/**
	 * Missing finalize session_id message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_missing_required_field_session_id() {
		return 'Missing required field: session_id';
	}

	/**
	 * Missing billing initialization message.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function msg_billing_not_initialized_for_site() {
		return 'Billing is not initialized for this site';
	}

	/**
	 * HTTP 500 status code helper.
	 *
	 * @since 0.14.323
	 *
	 * @return int
	 */
	public static function http_500() {
		return 500;
	}

	/**
	 * HTTP 502 status code helper.
	 *
	 * @since 0.14.323
	 *
	 * @return int
	 */
	public static function http_502() {
		return 502;
	}

	/**
	 * HTTP 403 status code helper.
	 *
	 * @since 0.14.323
	 *
	 * @return int
	 */
	public static function http_403() {
		return 403;
	}

	/**
	 * HTTP POST method helper.
	 *
	 * @since 0.14.323
	 *
	 * @return string
	 */
	public static function http_method_post() {
		return 'POST';
	}

	/**
	 * Get shared middleware preflight configuration for public AJAX handlers.
	 *
	 * @since 0.14.323
	 *
	 * @param string $api_secret API secret candidate from handler context.
	 * @return array{api_secret:string,middleware_url:string,missing_api_secret:bool,missing_middleware_url:bool}
	 */
	public static function get_public_preflight_config( $api_secret ) {
		$middleware_url = defined( 'SITESTAFFR_MIDDLEWARE_URL' ) ? SITESTAFFR_MIDDLEWARE_URL : '';
		$api_secret = is_string( $api_secret ) ? $api_secret : '';

		return array(
			'api_secret'           => $api_secret,
			'middleware_url'       => str_replace( 'wss://', 'https://', str_replace( 'ws://', 'http://', $middleware_url ) ),
			'missing_api_secret'   => empty( $api_secret ),
			'missing_middleware_url' => empty( $middleware_url ),
		);
	}

	/**
	 * Send signed JSON POST request and return normalized + decoded JSON response.
	 *
	 * @since 0.14.323
	 *
	 * @param string     $endpoint      Target URL.
	 * @param string     $payload       Raw JSON payload used for signature and request body.
	 * @param string     $api_secret    Shared API secret.
	 * @param array|null $business_info Optional business info payload.
	 * @param string     $visitor_ip    Optional visitor IP.
	 * @param int        $timeout       Timeout seconds.
	 * @param array      $extra_headers Additional headers (e.g., Content-Type).
	 * @return array{status_code:int,body:string,decoded_body:mixed,json_error:int}|WP_Error
	 */
	public static function post_signed_json_with_json_response( $endpoint, $payload, $api_secret, $business_info = null, $visitor_ip = '', $timeout = 15, $extra_headers = array() ) {
		$headers = self::build_signed_headers(
			$payload,
			$api_secret,
			$business_info,
			$visitor_ip,
			$extra_headers
		);

		return self::post_json_with_json_response(
			$endpoint,
			$payload,
			$headers,
			$timeout
		);
	}

	/**
	 * Send raw middleware request using wp_remote_request().
	 *
	 * Returns the direct wp_remote_request result without normalization.
	 *
	 * @since 0.14.323
	 *
	 * @param string $url  Target URL.
	 * @param array  $args Request args.
	 * @return array|WP_Error
	 */
	public static function request_raw( $url, $args ) {
		return wp_remote_request( $url, $args );
	}

	/**
	 * Send generic middleware request using wp_remote_request().
	 *
	 * @since 0.14.323
	 *
	 * @param string $url  Target URL.
	 * @param array  $args Request args.
	 * @return array|WP_Error
	 * @deprecated 0.14.323 Use post_signed_json_with_json_response() for signed JSON calls, post_signed_sdp_with_json_response() for signed SDP calls, or post_json_raw()/request_raw() for raw request needs.
	 * @internal Compatibility wrapper retained for potential external callers.
	 */
	public static function request( $url, $args ) {
		$response = self::request_raw( $url, $args );
		return self::normalize_response( $response );
	}

	/**
	 * Send request and return normalized + decoded JSON response.
	 *
	 * @since 0.14.323
	 *
	 * @param string $url  Target URL.
	 * @param array  $args Request args.
	 * @return array{status_code:int,body:string,decoded_body:mixed,json_error:int}|WP_Error
	 */
	public static function request_with_json_response( $url, $args ) {
		$response = self::request_raw( $url, $args );
		return self::normalize_response_with_json( $response );
	}

	/**
	 * Send signed SDP request and return normalized + decoded JSON response.
	 *
	 * @since 0.14.323
	 *
	 * @param string $endpoint      Target URL.
	 * @param string $sdp_offer     Raw SDP payload used for signature and request body.
	 * @param string $api_secret    Shared API secret.
	 * @param array  $business_info Business info payload for header inclusion.
	 * @param string $visitor_ip    Visitor IP for header inclusion.
	 * @param int    $timeout       Timeout seconds.
	 * @param array  $extra_headers Additional headers (e.g., Content-Type).
	 * @return array{status_code:int,body:string,decoded_body:mixed,json_error:int}|WP_Error
	 */
	public static function post_signed_sdp_with_json_response( $endpoint, $sdp_offer, $api_secret, $business_info, $visitor_ip, $timeout = 60, $extra_headers = array() ) {
		$headers = self::build_signed_headers(
			$sdp_offer,
			$api_secret,
			$business_info,
			$visitor_ip,
			$extra_headers
		);

		return self::request_with_json_response(
			$endpoint,
			array(
				'method'  => self::http_method_post(),
				'headers' => $headers,
				'body'    => $sdp_offer,
				'timeout' => $timeout,
			)
		);
	}

	/**
	 * Backward-compatible alias for signed SDP request helper.
	 *
	 * @since 0.14.323
	 *
	 * @param string $endpoint      Target URL.
	 * @param string $sdp_offer     Raw SDP payload used for signature and request body.
	 * @param string $api_secret    Shared API secret.
	 * @param array  $business_info Business info payload for header inclusion.
	 * @param string $visitor_ip    Visitor IP for header inclusion.
	 * @param int    $timeout       Timeout seconds.
	 * @param array  $extra_headers Additional headers (e.g., Content-Type).
	 * @return array{status_code:int,body:string,decoded_body:mixed,json_error:int}|WP_Error
	 */
	public static function request_signed_sdp_with_json_response( $endpoint, $sdp_offer, $api_secret, $business_info, $visitor_ip, $timeout = 60, $extra_headers = array() ) {
		return self::post_signed_sdp_with_json_response(
			$endpoint,
			$sdp_offer,
			$api_secret,
			$business_info,
			$visitor_ip,
			$timeout,
			$extra_headers
		);
	}

	/**
	 * Decode JSON response body.
	 *
	 * @since 0.14.323
	 *
	 * @param string $body Response body.
	 * @return array{data:mixed,json_error:int}
	 */
	public static function decode_json_body( $body ) {
		$data = json_decode( $body, true );
		return array(
			'data'       => $data,
			'json_error' => json_last_error(),
		);
	}

	/**
	 * Normalize middleware response and decode JSON body in one pass.
	 *
	 * @since 0.14.323
	 *
	 * @param array|WP_Error $response Raw or normalized response.
	 * @return array{status_code:int,body:string,decoded_body:mixed,json_error:int}|WP_Error
	 */
	public static function normalize_response_with_json( $response ) {
		$normalized_response = self::normalize_response( $response );
		if ( is_wp_error( $normalized_response ) ) {
			return $normalized_response;
		}

		$decoded_response = self::decode_json_body( $normalized_response['body'] );

		return array(
			'status_code'  => $normalized_response['status_code'],
			'body'         => $normalized_response['body'],
			'decoded_body' => $decoded_response['data'],
			'json_error'   => $decoded_response['json_error'],
		);
	}

	/**
	 * Validate decoded JSON payload and required keys.
	 *
	 * @since 0.14.323
	 *
	 * @param int   $json_error    json_last_error() value from decode step.
	 * @param mixed $decoded_body  Decoded JSON response body.
	 * @param array $required_keys Required keys expected in decoded body.
	 * @return array{ok:bool,reason:string}
	 */
	public static function validate_json_payload( $json_error, $decoded_body = null, $required_keys = array() ) {
		if ( JSON_ERROR_NONE !== (int) $json_error ) {
			return array(
				'ok'     => false,
				'reason' => 'json_error',
			);
		}

		if ( ! is_array( $required_keys ) || empty( $required_keys ) ) {
			return array(
				'ok'     => true,
				'reason' => '',
			);
		}

		if ( ! is_array( $decoded_body ) ) {
			return array(
				'ok'     => false,
				'reason' => 'missing_required_key',
			);
		}

		foreach ( $required_keys as $required_key ) {
			if ( ! isset( $decoded_body[ $required_key ] ) ) {
				return array(
					'ok'     => false,
					'reason' => 'missing_required_key',
				);
			}
		}

		return array(
			'ok'     => true,
			'reason' => '',
		);
	}

	/**
	 * Check whether status code matches expected value.
	 *
	 * @since 0.14.323
	 *
	 * @param int $status_code HTTP status code.
	 * @param int $expected    Expected HTTP status code.
	 * @return bool
	 */
	public static function is_expected_status( $status_code, $expected = 200 ) {
		return (int) $status_code === (int) $expected;
	}

	/**
	 * Build normalized "status - body" string for middleware logs.
	 *
	 * @since 0.14.323
	 *
	 * @param int    $status_code HTTP status code.
	 * @param string $body        Response body.
	 * @return string
	 */
	public static function get_status_body_for_log( $status_code, $body ) {
		return sprintf( '%d - %s', (int) $status_code, (string) $body );
	}

	/**
	 * Build middleware error response payload.
	 *
	 * @since 0.14.323
	 *
	 * @param string     $message Error message string.
	 * @param mixed|null $details Optional details payload.
	 * @param array      $extra   Optional extra keys for handler-specific payloads.
	 * @return array
	 */
	public static function build_middleware_error_payload( $message, $details = null, $extra = array() ) {
		$payload = array(
			'message' => $message,
		);

		if ( null !== $details ) {
			$payload['details'] = $details;
		}

		if ( is_array( $extra ) ) {
			foreach ( $extra as $key => $value ) {
				$payload[ $key ] = $value;
			}
		}

		return $payload;
	}

	/**
	 * Extract a string error message from WP HTTP WP_Error payloads.
	 *
	 * @since 0.14.323
	 *
	 * @param mixed $error Error candidate from WP HTTP request.
	 * @return string
	 */
	public static function get_wp_http_error_message( $error ) {
		if ( ! is_wp_error( $error ) ) {
			return '';
		}

		$message = $error->get_error_message();
		return is_string( $message ) ? $message : '';
	}

	/**
	 * Extract entitlement-block payload when present.
	 *
	 * @since 0.14.323
	 *
	 * @param mixed $decoded_body Decoded JSON response body.
	 * @return array|null
	 */
	public static function extract_entitlement_block( $decoded_body ) {
		if ( ! is_array( $decoded_body ) ) {
			return null;
		}

		if ( ! isset( $decoded_body['error_code'] ) || 'ENTITLEMENT_BLOCKED' !== $decoded_body['error_code'] ) {
			return null;
		}

		return array(
			'error_code'       => 'ENTITLEMENT_BLOCKED',
			'reason'           => isset( $decoded_body['reason'] ) ? sanitize_text_field( $decoded_body['reason'] ) : 'unknown',
			'suggested_action' => isset( $decoded_body['suggested_action'] ) ? sanitize_text_field( $decoded_body['suggested_action'] ) : '',
		);
	}

	/**
	 * Extract a generic forbidden error payload from middleware response.
	 *
	 * @since 0.14.327
	 *
	 * @param mixed $decoded_body Decoded JSON response body.
	 * @return array|null
	 */
	public static function extract_forbidden_error( $decoded_body ) {
		if ( ! is_array( $decoded_body ) ) {
			return null;
		}

		$message = '';
		if ( isset( $decoded_body['error'] ) && is_string( $decoded_body['error'] ) ) {
			$message = sanitize_text_field( $decoded_body['error'] );
		} elseif ( isset( $decoded_body['message'] ) && is_string( $decoded_body['message'] ) ) {
			$message = sanitize_text_field( $decoded_body['message'] );
		}

		if ( '' === $message ) {
			return null;
		}

		return array(
			'error_code' => 'FORBIDDEN',
			'message'    => $message,
		);
	}

	/**
	 * Determine whether a forbidden message indicates HMAC signature mismatch.
	 *
	 * @since 0.14.273
	 *
	 * @param string $message Forbidden message.
	 * @return bool
	 */
	public static function is_invalid_auth_signature_message( $message ) {
		$normalized = strtolower( trim( (string) $message ) );
		return '' !== $normalized && false !== strpos( $normalized, 'invalid authentication signature' );
	}

	/**
	 * Resolve the freshest available API secret from options.
	 *
	 * @since 0.14.273
	 *
	 * @param string $fallback Fallback secret.
	 * @return string
	 */
	public static function get_current_api_secret( $fallback = '' ) {
		if ( function_exists( 'get_option' ) ) {
			$site_api_secret = (string) get_option( 'sitestaffr_site_api_secret', '' );
			if ( '' !== $site_api_secret ) {
				return $site_api_secret;
			}

			$global_api_secret = (string) get_option( 'sitestaffr_api_secret', '' );
			if ( '' !== $global_api_secret ) {
				return $global_api_secret;
			}
		}

		return (string) $fallback;
	}

	/**
	 * Attempt site-registration refresh to recover stale authentication secrets.
	 *
	 * @since 0.14.273
	 * @return bool True when registration refresh succeeded.
	 */
	public static function attempt_site_registration_recovery() {
		if ( ! class_exists( 'SiteStaffr_Site_Registration' ) ) {
			$registration_file = defined( 'SITESTAFFR_PLUGIN_DIR' ) ? SITESTAFFR_PLUGIN_DIR . 'includes/class-sitestaffr-site-registration.php' : '';
			if ( '' !== $registration_file && file_exists( $registration_file ) ) {
				require_once $registration_file;
			}
		}

		if ( ! class_exists( 'SiteStaffr_Site_Registration' ) || ! method_exists( 'SiteStaffr_Site_Registration', 'register_site' ) ) {
			return false;
		}

		try {
			$result = SiteStaffr_Site_Registration::register_site();
			return true === $result;
		} catch ( Exception $exception ) {
			return false;
		}
	}

	/**
	 * Get freshest billing identifiers from options.
	 *
	 * @since 0.14.274
	 *
	 * @param string $fallback_account_id Fallback account ID.
	 * @param string $fallback_installation_id Fallback installation ID.
	 * @return array{account_id:string,installation_id:string}
	 */
	public static function get_current_billing_identifiers( $fallback_account_id = '', $fallback_installation_id = '' ) {
		$account_id = (string) $fallback_account_id;
		$installation_id = (string) $fallback_installation_id;

		if ( function_exists( 'get_option' ) ) {
			$current_account_id = (string) get_option( 'sitestaffr_account_id', '' );
			$current_installation_id = (string) get_option( 'sitestaffr_installation_id', '' );

			if ( '' !== $current_account_id ) {
				$account_id = $current_account_id;
			}
			if ( '' !== $current_installation_id ) {
				$installation_id = $current_installation_id;
			}
		}

		return array(
			'account_id'      => $account_id,
			'installation_id' => $installation_id,
		);
	}

	/**
	 * Determine if entitlement block is a minutes exhaustion response.
	 *
	 * @since 0.14.274
	 *
	 * @param array|null $blocked_data Entitlement block payload.
	 * @return bool
	 */
	public static function is_minutes_exhausted_block( $blocked_data ) {
		return is_array( $blocked_data )
			&& isset( $blocked_data['reason'] )
			&& 'minutes_exhausted' === (string) $blocked_data['reason'];
	}

	/**
	 * Get locally cached total remaining minutes when available.
	 *
	 * @since 0.14.274
	 *
	 * @return int|null Null when unavailable.
	 */
	public static function get_cached_remaining_minutes_total() {
		$cache = array();

		if ( ! class_exists( 'SiteStaffr_Billing_State' ) ) {
			$billing_state_file = defined( 'SITESTAFFR_PLUGIN_DIR' ) ? SITESTAFFR_PLUGIN_DIR . 'includes/class-sitestaffr-billing-state.php' : '';
			if ( '' !== $billing_state_file && file_exists( $billing_state_file ) ) {
				require_once $billing_state_file;
			}
		}

		if ( class_exists( 'SiteStaffr_Billing_State' ) ) {
			if ( method_exists( 'SiteStaffr_Billing_State', 'maybe_refresh_cache' ) ) {
				SiteStaffr_Billing_State::maybe_refresh_cache( true );
			}
			if ( method_exists( 'SiteStaffr_Billing_State', 'get_cache' ) ) {
				$cache = SiteStaffr_Billing_State::get_cache();
			}
		}

		if ( empty( $cache ) && function_exists( 'get_option' ) ) {
			$cache = get_option( 'sitestaffr_billing_cache', array() );
		}

		if ( ! is_array( $cache ) || empty( $cache ) ) {
			return null;
		}

		$seconds_authoritative = ! empty( $cache['seconds_authoritative'] );
		if ( $seconds_authoritative ) {
			$included_seconds = isset( $cache['included_seconds_remaining'] ) ? max( 0, (int) $cache['included_seconds_remaining'] ) : 0;
			$addon_seconds = isset( $cache['addon_seconds_remaining'] ) ? max( 0, (int) $cache['addon_seconds_remaining'] ) : 0;
			return (int) floor( ( $included_seconds + $addon_seconds ) / 60 );
		}

		$included_minutes = isset( $cache['included_minutes_remaining'] ) ? max( 0, (int) $cache['included_minutes_remaining'] ) : 0;
		$addon_minutes = isset( $cache['addon_minutes_remaining'] ) ? max( 0, (int) $cache['addon_minutes_remaining'] ) : 0;
		return $included_minutes + $addon_minutes;
	}

	/**
	 * Determine whether entitlement conflict warrants identity self-heal.
	 *
	 * @since 0.14.274
	 *
	 * @param array|null $blocked_data Entitlement block payload.
	 * @param string     $account_id Current account ID.
	 * @param string     $installation_id Current installation ID.
	 * @return bool
	 */
	public static function should_attempt_entitlement_identity_recovery( $blocked_data, $account_id = '', $installation_id = '' ) {
		if ( ! self::is_minutes_exhausted_block( $blocked_data ) ) {
			return false;
		}

		$cached_minutes_total = self::get_cached_remaining_minutes_total();
		if ( is_int( $cached_minutes_total ) && $cached_minutes_total > 0 ) {
			return true;
		}

		if ( null === $cached_minutes_total && ( '' !== (string) $account_id || '' !== (string) $installation_id ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Attempt deep identity recovery when entitlement state conflicts with local cache.
	 *
	 * Rotates installation_id, re-registers site, and refreshes billing cache once.
	 *
	 * @since 0.14.274
	 * @return bool True when recovery completed and registration succeeded.
	 */
	public static function attempt_entitlement_identity_recovery() {
		$lock_key = 'sitestaffr_entitlement_identity_recovery_lock';
		if ( function_exists( 'get_transient' ) && false !== get_transient( $lock_key ) ) {
			return false;
		}
		if ( function_exists( 'set_transient' ) ) {
			set_transient( $lock_key, '1', 300 );
		}

		if ( ! class_exists( 'SiteStaffr_Site_Registration' ) ) {
			$registration_file = defined( 'SITESTAFFR_PLUGIN_DIR' ) ? SITESTAFFR_PLUGIN_DIR . 'includes/class-sitestaffr-site-registration.php' : '';
			if ( '' !== $registration_file && file_exists( $registration_file ) ) {
				require_once $registration_file;
			}
		}

		if ( ! class_exists( 'SiteStaffr_Site_Registration' ) || ! method_exists( 'SiteStaffr_Site_Registration', 'register_site' ) ) {
			return false;
		}

		try {
			if ( function_exists( 'delete_option' ) ) {
				delete_option( 'sitestaffr_installation_id' );
			}

			if ( method_exists( 'SiteStaffr_Site_Registration', 'get_or_create_installation_id' ) ) {
				SiteStaffr_Site_Registration::get_or_create_installation_id();
			}

			$registered = SiteStaffr_Site_Registration::register_site();
			if ( true !== $registered ) {
				return false;
			}

			if ( ! class_exists( 'SiteStaffr_Billing_State' ) ) {
				$billing_state_file = defined( 'SITESTAFFR_PLUGIN_DIR' ) ? SITESTAFFR_PLUGIN_DIR . 'includes/class-sitestaffr-billing-state.php' : '';
				if ( '' !== $billing_state_file && file_exists( $billing_state_file ) ) {
					require_once $billing_state_file;
				}
			}
			if ( class_exists( 'SiteStaffr_Billing_State' ) && method_exists( 'SiteStaffr_Billing_State', 'maybe_refresh_cache' ) ) {
				SiteStaffr_Billing_State::maybe_refresh_cache( true );
			}

			return true;
		} catch ( Exception $exception ) {
			return false;
		}
	}

	/**
	 * Build the realtime session success payload returned to browser callers.
	 *
	 * @since 0.14.323
	 *
	 * @param mixed $decoded_body Decoded middleware response body.
	 * @return mixed
	 */
	public static function build_realtime_session_success_payload( $decoded_body ) {
		return $decoded_body;
	}

	/**
	 * Build realtime-session request body in signing-safe key order.
	 *
	 * @since 0.14.323
	 *
	 * @param string $site_token    Site token.
	 * @param string $site_url      Site URL.
	 * @param array  $business_info Business info payload.
	 * @param string $account_id    Optional account identifier.
	 * @param string $installation_id Optional installation identifier.
	 * @return array
	 */
	public static function build_realtime_session_request_body( $site_token, $site_url, $business_info, $account_id = '', $installation_id = '' ) {
		$request_body = array(
			'site_token'    => $site_token,
			'site_url'      => $site_url,
			'business_info' => $business_info,
		);

		if ( '' !== (string) $account_id ) {
			$request_body['account_id'] = (string) $account_id;
		}
		if ( '' !== (string) $installation_id ) {
			$request_body['installation_id'] = (string) $installation_id;
		}

		return $request_body;
	}

	/**
	 * Execute realtime-session middleware request and map response branches.
	 *
	 * @since 0.14.323
	 *
	 * @param string $middleware_url Middleware base URL.
	 * @param string $payload        Raw JSON payload (already assembled by caller).
	 * @param string $api_secret     Shared API secret.
	 * @return array{
	 *   ok:bool,
	 *   result:string,
	 *   status_code:int,
	 *   success_payload:mixed|null,
	 *   error_payload:array|null,
	 *   error_message:string,
	 *   status_body_for_log:string
	 * }
	 */
	public static function realtime_session_request( $middleware_url, $payload, $api_secret, $allow_recovery = true ) {
		$endpoint = self::build_middleware_endpoint( $middleware_url, self::path_realtime_session() );

		$normalized_response = self::post_signed_json_with_json_response(
			$endpoint,
			$payload,
			$api_secret,
			null,
			'',
			self::timeout_realtime_session(),
			self::extra_headers_json()
		);

		if ( is_wp_error( $normalized_response ) ) {
			$error_message = self::get_wp_http_error_message( $normalized_response );
			return array(
				'ok'                  => false,
				'result'              => 'request_error',
				'status_code'         => self::http_502(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_failed_to_connect_to_middleware_prefix() . $error_message
				),
				'error_message'       => $error_message,
				'status_body_for_log' => '',
			);
		}

		$status_code = $normalized_response['status_code'];
		$body = $normalized_response['body'];
		if ( self::http_403() === (int) $status_code ) {
			$forbidden = self::extract_forbidden_error( $normalized_response['decoded_body'] );
			if ( null !== $forbidden ) {
				if ( $allow_recovery && self::is_invalid_auth_signature_message( $forbidden['message'] ) && self::attempt_site_registration_recovery() ) {
					$retry_secret = self::get_current_api_secret( $api_secret );
					return self::realtime_session_request( $middleware_url, $payload, $retry_secret, false );
				}

				return array(
					'ok'                  => false,
					'result'              => 'middleware_error',
					'status_code'         => self::http_403(),
					'success_payload'     => null,
					'error_payload'       => self::build_middleware_error_payload(
						$forbidden['message'],
						null,
						array(
							'error_code' => $forbidden['error_code'],
						)
					),
					'error_message'       => '',
					'status_body_for_log' => '',
				);
			}
		}

		if ( ! self::is_expected_status( $status_code ) ) {
			return array(
				'ok'                  => false,
				'result'              => 'middleware_error',
				'status_code'         => (int) $status_code,
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_middleware_error_prefix() . $status_code,
					$body
				),
				'error_message'       => '',
				'status_body_for_log' => self::get_status_body_for_log( $status_code, $body ),
			);
		}

		$data = $normalized_response['decoded_body'];
		$realtime_json_validation = self::validate_json_payload( $normalized_response['json_error'] );
		if ( ! $realtime_json_validation['ok'] ) {
			return array(
				'ok'                  => false,
				'result'              => 'invalid_json',
				'status_code'         => self::http_500(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload( self::msg_invalid_middleware_response() ),
				'error_message'       => '',
				'status_body_for_log' => '',
			);
		}

		return array(
			'ok'                  => true,
			'result'              => 'success',
			'status_code'         => 200,
			'success_payload'     => self::build_realtime_session_success_payload( $data ),
			'error_payload'       => null,
			'error_message'       => '',
			'status_body_for_log' => '',
		);
	}

	/**
	 * Execute WebRTC-session middleware request and map response branches.
	 *
	 * @since 0.14.323
	 *
	 * @param string $middleware_url Middleware base URL.
	 * @param string $site_token     Site token.
	 * @param string $site_url       Site URL query parameter.
	 * @param string $sdp_offer      Raw SDP offer payload.
	 * @param string $api_secret     Shared API secret.
	 * @param array  $business_info  Business context payload.
	 * @param string $ip_address     Visitor IP for forwarding/signature headers.
	 * @param string $account_id     Optional account identifier.
	 * @param string $installation_id Optional installation identifier.
	 * @return array{
	 *   ok:bool,
	 *   result:string,
	 *   status_code:int,
	 *   success_payload:array|null,
	 *   error_payload:array|null,
	 *   error_message:string,
	 *   status_body_for_log:string
	 * }
	 */
	public static function webrtc_session_request( $middleware_url, $site_token, $site_url, $sdp_offer, $api_secret, $business_info, $ip_address, $account_id = '', $installation_id = '', $allow_recovery = true ) {
		$endpoint = self::build_middleware_endpoint( $middleware_url, self::path_webrtc_session() );
		$query_args = array(
			'site_token' => $site_token,
			'site_url'   => $site_url,
		);
		if ( '' !== (string) $account_id ) {
			$query_args['account_id'] = (string) $account_id;
		}
		if ( '' !== (string) $installation_id ) {
			$query_args['installation_id'] = (string) $installation_id;
		}
		$endpoint = add_query_arg( $query_args, $endpoint );

		$normalized_response = self::post_signed_sdp_with_json_response(
			$endpoint,
			$sdp_offer,
			$api_secret,
			$business_info,
			$ip_address,
			self::timeout_webrtc_session(),
			self::extra_headers_sdp()
		);

		if ( is_wp_error( $normalized_response ) ) {
			$error_message = self::get_wp_http_error_message( $normalized_response );
			return array(
				'ok'                  => false,
				'result'              => 'request_error',
				'status_code'         => self::http_502(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_failed_to_connect_to_middleware_prefix() . $error_message
				),
				'error_message'       => $error_message,
				'status_body_for_log' => '',
			);
		}

		$status_code = $normalized_response['status_code'];
		$body = $normalized_response['body'];

		if ( self::http_403() === (int) $status_code ) {
			$blocked_data = self::extract_entitlement_block( $normalized_response['decoded_body'] );
			if ( null !== $blocked_data ) {
				if ( $allow_recovery && self::should_attempt_entitlement_identity_recovery( $blocked_data, $account_id, $installation_id ) && self::attempt_entitlement_identity_recovery() ) {
					$retry_secret = self::get_current_api_secret( $api_secret );
					$retry_identifiers = self::get_current_billing_identifiers( $account_id, $installation_id );
					return self::webrtc_session_request(
						$middleware_url,
						$site_token,
						$site_url,
						$sdp_offer,
						$retry_secret,
						$business_info,
						$ip_address,
						$retry_identifiers['account_id'],
						$retry_identifiers['installation_id'],
						false
					);
				}

				return array(
					'ok'                  => false,
					'result'              => 'middleware_error',
					'status_code'         => self::http_403(),
					'success_payload'     => null,
					'error_payload'       => self::build_middleware_error_payload(
						self::msg_service_temporarily_unavailable(),
						null,
						array(
							'error_code'       => 'ENTITLEMENT_BLOCKED',
							'reason'           => $blocked_data['reason'],
							'suggested_action' => $blocked_data['suggested_action'],
						)
					),
					'error_message'       => '',
					'status_body_for_log' => '',
				);
			}

			$forbidden = self::extract_forbidden_error( $normalized_response['decoded_body'] );
			if ( null !== $forbidden ) {
				if ( $allow_recovery && self::is_invalid_auth_signature_message( $forbidden['message'] ) && self::attempt_site_registration_recovery() ) {
					$retry_secret = self::get_current_api_secret( $api_secret );
					return self::webrtc_session_request(
						$middleware_url,
						$site_token,
						$site_url,
						$sdp_offer,
						$retry_secret,
						$business_info,
						$ip_address,
						$account_id,
						$installation_id,
						false
					);
				}

				return array(
					'ok'                  => false,
					'result'              => 'middleware_error',
					'status_code'         => self::http_403(),
					'success_payload'     => null,
					'error_payload'       => self::build_middleware_error_payload(
						$forbidden['message'],
						null,
						array(
							'error_code' => $forbidden['error_code'],
						)
					),
					'error_message'       => '',
					'status_body_for_log' => '',
				);
			}
		}

		if ( ! self::is_expected_status( $status_code ) ) {
			return array(
				'ok'                  => false,
				'result'              => 'middleware_error',
				'status_code'         => (int) $status_code,
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_middleware_error_prefix() . $status_code,
					$body
				),
				'error_message'       => '',
				'status_body_for_log' => self::get_status_body_for_log( $status_code, $body ),
			);
		}

		$middleware_response = $normalized_response['decoded_body'];
		$webrtc_json_validation = self::validate_json_payload(
			$normalized_response['json_error'],
			$middleware_response,
			array( 'sdp' )
		);
		if ( ! $webrtc_json_validation['ok'] ) {
			return array(
				'ok'                  => false,
				'result'              => 'invalid_json',
				'status_code'         => self::http_502(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload( self::msg_invalid_middleware_response() ),
				'error_message'       => '',
				'status_body_for_log' => '',
			);
		}

		return array(
			'ok'                  => true,
			'result'              => 'success',
			'status_code'         => 200,
			'success_payload'     => self::build_webrtc_success_payload( $middleware_response ),
			'error_payload'       => null,
			'error_message'       => '',
			'status_body_for_log' => '',
		);
	}

	/**
	 * Build finalize-usage payload array in signing-safe key order.
	 *
	 * @since 0.14.323
	 *
	 * @param string $account_id       Account identifier.
	 * @param string $installation_id  Installation identifier.
	 * @param string $session_id       Session identifier.
	 * @param int    $duration_seconds Call duration in seconds.
	 * @param string $site_url         Site URL.
	 * @return array
	 */
	public static function build_finalize_usage_payload( $account_id, $installation_id, $session_id, $duration_seconds, $site_url ) {
		return array(
			'account_id'       => $account_id,
			'installation_id'  => $installation_id,
			'session_id'       => $session_id,
			'duration_seconds' => $duration_seconds,
			'site_url'         => $site_url,
		);
	}

	/**
	 * Normalize email for conversation-success event gating.
	 *
	 * @since 1.0.2
	 *
	 * @param string $email Candidate email.
	 * @return string
	 */
	public static function normalize_conversation_success_email( $email ) {
		$value = strtolower( trim( (string) $email ) );
		if ( '' === $value ) {
			return '';
		}

		if ( ! preg_match( '/^[^\s@]+@[^\s@]+\.[^\s@]+$/', $value ) ) {
			return '';
		}

		return $value;
	}

	/**
	 * Normalize phone for conversation-success event gating.
	 *
	 * Accepts US 10 digits, or 11 digits with leading 1 and normalizes to 10 digits.
	 *
	 * @since 1.0.2
	 *
	 * @param string $phone Candidate phone value.
	 * @return string
	 */
	public static function normalize_conversation_success_phone( $phone ) {
		$digits = preg_replace( '/\D+/', '', (string) $phone );
		$digits = is_string( $digits ) ? $digits : '';

		if ( 10 === strlen( $digits ) ) {
			return $digits;
		}

		if ( 11 === strlen( $digits ) && '1' === substr( $digits, 0, 1 ) ) {
			return substr( $digits, 1 );
		}

		return '';
	}

	/**
	 * Build a normalized conversation-success payload.
	 *
	 * Computes success_reason as valid_email or valid_phone (email preferred).
	 *
	 * @since 1.0.2
	 *
	 * @param string $account_id      Account identifier.
	 * @param string $installation_id Installation identifier.
	 * @param string $session_id      Session identifier.
	 * @param string $site_url        Site URL for request-site matching middleware.
	 * @param string $contact_email   Candidate captured email.
	 * @param string $contact_phone   Candidate captured phone.
	 * @param string $created_at      Optional ISO timestamp.
	 * @return array{
	 *   success:bool,
	 *   success_reason:string,
	 *   contact_email:string,
	 *   contact_phone:string,
	 *   payload:array
	 * }
	 */
	public static function build_conversation_success_payload( $account_id, $installation_id, $session_id, $site_url, $contact_email = '', $contact_phone = '', $created_at = '' ) {
		$normalized_email = self::normalize_conversation_success_email( $contact_email );
		$normalized_phone = self::normalize_conversation_success_phone( $contact_phone );

		$success_reason = '';
		if ( '' !== $normalized_email ) {
			$success_reason = 'valid_email';
		} elseif ( '' !== $normalized_phone ) {
			$success_reason = 'valid_phone';
		}

		$created_at_value = trim( (string) $created_at );
		if ( '' === $created_at_value ) {
			$created_at_value = gmdate( 'c' );
		}

		$payload = array(
			'account_id'      => (string) $account_id,
			'installation_id' => (string) $installation_id,
			'session_id'      => (string) $session_id,
			'site_url'        => (string) $site_url,
			'success_reason'  => $success_reason,
			'created_at'      => $created_at_value,
		);

		if ( '' !== $normalized_email ) {
			$payload['contact_email'] = $normalized_email;
		}
		if ( '' !== $normalized_phone ) {
			$payload['contact_phone'] = $normalized_phone;
		}

		return array(
			'success'        => '' !== $success_reason,
			'success_reason' => $success_reason,
			'contact_email'  => $normalized_email,
			'contact_phone'  => $normalized_phone,
			'payload'        => $payload,
		);
	}

	/**
	 * Execute finalize-usage middleware request and map response branches.
	 *
	 * @since 0.14.323
	 *
	 * @param string $middleware_url Middleware base URL.
	 * @param string $payload_json   Raw JSON payload (already assembled by caller).
	 * @param string $api_secret     Shared API secret.
	 * @return array{
	 *   ok:bool,
	 *   result:string,
	 *   status_code:int,
	 *   success_payload:array|null,
	 *   error_payload:array|null,
	 *   error_message:string,
	 *   status_body_for_log:string
	 * }
	 */
	public static function finalize_usage_request( $middleware_url, $payload_json, $api_secret ) {
		$endpoint = self::build_middleware_endpoint( $middleware_url, self::path_finalize_usage() );

		$normalized_response = self::post_signed_json_with_json_response(
			$endpoint,
			$payload_json,
			$api_secret,
			null,
			'',
			self::timeout_finalize_usage(),
			self::extra_headers_json()
		);

		if ( is_wp_error( $normalized_response ) ) {
			$error_message = self::get_wp_http_error_message( $normalized_response );
			return array(
				'ok'                  => false,
				'result'              => 'request_error',
				'status_code'         => self::http_502(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_finalize_failed(),
					$error_message
				),
				'error_message'       => $error_message,
				'status_body_for_log' => '',
			);
		}

		$finalize_validation = self::validate_finalize_response( $normalized_response );
		if ( ! $finalize_validation['ok'] ) {
			$json_error = isset( $normalized_response['json_error'] ) ? (int) $normalized_response['json_error'] : JSON_ERROR_NONE;

			return array(
				'ok'                  => false,
				'result'              => JSON_ERROR_NONE !== $json_error ? 'invalid_json' : 'middleware_error',
				'status_code'         => (int) $finalize_validation['error_status'],
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_usage_finalization_failed(),
					$finalize_validation['details']
				),
				'error_message'       => '',
				'status_body_for_log' => self::get_status_body_for_log( $finalize_validation['status_code'], $finalize_validation['body'] ),
			);
		}

		return array(
			'ok'                  => true,
			'result'              => 'success',
			'status_code'         => 200,
			'success_payload'     => $finalize_validation['data'],
			'error_payload'       => null,
			'error_message'       => '',
			'status_body_for_log' => '',
		);
	}

	/**
	 * Execute conversation-success middleware request and map response branches.
	 *
	 * @since 1.0.2
	 *
	 * @param string $middleware_url Middleware base URL.
	 * @param string $payload_json   Raw JSON payload (already assembled by caller).
	 * @param string $api_secret     Shared API secret.
	 * @return array{
	 *   ok:bool,
	 *   result:string,
	 *   status_code:int,
	 *   success_payload:array|null,
	 *   error_payload:array|null,
	 *   error_message:string,
	 *   status_body_for_log:string
	 * }
	 */
	public static function conversation_success_request( $middleware_url, $payload_json, $api_secret ) {
		$endpoint = self::build_middleware_endpoint( $middleware_url, self::path_conversation_success_event() );

		$normalized_response = self::post_signed_json_with_json_response(
			$endpoint,
			$payload_json,
			$api_secret,
			null,
			'',
			self::timeout_conversation_success_event(),
			self::extra_headers_json()
		);

		if ( is_wp_error( $normalized_response ) ) {
			$error_message = self::get_wp_http_error_message( $normalized_response );
			return array(
				'ok'                  => false,
				'result'              => 'request_error',
				'status_code'         => self::http_502(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_conversation_success_request_failed(),
					$error_message
				),
				'error_message'       => $error_message,
				'status_body_for_log' => '',
			);
		}

		$status_code = isset( $normalized_response['status_code'] ) ? (int) $normalized_response['status_code'] : 0;
		$body = isset( $normalized_response['body'] ) ? (string) $normalized_response['body'] : '';
		$data = isset( $normalized_response['decoded_body'] ) ? $normalized_response['decoded_body'] : null;
		$json_error = isset( $normalized_response['json_error'] ) ? (int) $normalized_response['json_error'] : JSON_ERROR_NONE;

		if ( ! self::is_expected_status( $status_code ) || JSON_ERROR_NONE !== $json_error || ! is_array( $data ) ) {
			return array(
				'ok'                  => false,
				'result'              => JSON_ERROR_NONE !== $json_error ? 'invalid_json' : 'middleware_error',
				'status_code'         => $status_code > 0 ? $status_code : self::http_500(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_conversation_success_request_failed(),
					$body
				),
				'error_message'       => '',
				'status_body_for_log' => self::get_status_body_for_log( $status_code, $body ),
			);
		}

		if ( empty( $data['success'] ) ) {
			$details = '';
			if ( isset( $data['reason'] ) && is_string( $data['reason'] ) ) {
				$details = $data['reason'];
			}

			return array(
				'ok'                  => false,
				'result'              => 'not_fired',
				'status_code'         => $status_code > 0 ? $status_code : self::http_500(),
				'success_payload'     => null,
				'error_payload'       => self::build_middleware_error_payload(
					self::msg_conversation_success_not_recorded(),
					$details
				),
				'error_message'       => '',
				'status_body_for_log' => self::get_status_body_for_log( $status_code, $body ),
			);
		}

		return array(
			'ok'                  => true,
			'result'              => 'success',
			'status_code'         => 200,
			'success_payload'     => $data,
			'error_payload'       => null,
			'error_message'       => '',
			'status_body_for_log' => '',
		);
	}

	/**
	 * Validate middleware finalize usage response and map shared values.
	 *
	 * @since 0.14.323
	 *
	 * @param array $normalized_response Normalized middleware response data.
	 * @return array{ok:bool,status_code:int,body:string,data:mixed,details:mixed,error_status:int}
	 */
	public static function validate_finalize_response( $normalized_response ) {
		$status_code = isset( $normalized_response['status_code'] ) ? (int) $normalized_response['status_code'] : 0;
		$body = isset( $normalized_response['body'] ) ? (string) $normalized_response['body'] : '';
		$data = isset( $normalized_response['decoded_body'] ) ? $normalized_response['decoded_body'] : null;
		$json_error = isset( $normalized_response['json_error'] ) ? (int) $normalized_response['json_error'] : JSON_ERROR_NONE;
		$ok = self::is_expected_status( $status_code ) && JSON_ERROR_NONE === $json_error && ! empty( $data['success'] );

		return array(
			'ok'           => $ok,
			'status_code'  => $status_code,
			'body'         => $body,
			'data'         => $data,
			'details'      => is_array( $data ) && isset( $data['error'] ) ? $data['error'] : $body,
			'error_status' => $status_code > 0 ? $status_code : 500,
		);
	}

	/**
	 * Build the WebRTC success payload returned to browser callers.
	 *
	 * @since 0.14.323
	 *
	 * @param array $middleware_response Decoded middleware response body.
	 * @return array
	 */
	public static function build_webrtc_success_payload( $middleware_response ) {
		$data = is_array( $middleware_response ) ? $middleware_response : array();

		return array(
			'sdp'                 => isset( $data['sdp'] ) ? $data['sdp'] : '',
			'instructions'        => isset( $data['instructions'] ) ? $data['instructions'] : '',
			'tools'               => isset( $data['tools'] ) ? $data['tools'] : array(),
			'voice'               => isset( $data['voice'] ) ? $data['voice'] : 'marin',
			'model'               => isset( $data['model'] ) ? $data['model'] : '',
			'transcription_model' => isset( $data['transcription_model'] ) ? $data['transcription_model'] : '',
			'greeting_tone'       => isset( $data['greeting_tone'] ) ? $data['greeting_tone'] : 'warm_neutral',
			'custom_greeting'     => isset( $data['custom_greeting'] ) ? $data['custom_greeting'] : '',
			'modes'               => isset( $data['modes'] ) ? $data['modes'] : null,
			'tools_by_mode'       => isset( $data['tools_by_mode'] ) ? $data['tools_by_mode'] : null,
			'lifecycle_responses' => isset( $data['lifecycle_responses'] ) ? $data['lifecycle_responses'] : null,
		);
	}

	/**
	 * Build signed middleware headers using shared HMAC logic.
	 *
	 * @since 0.14.323
	 *
	 * @param string       $payload       Raw payload string used for signature.
	 * @param string       $api_secret    Shared API secret.
	 * @param array|null   $business_info Optional business info payload.
	 * @param string       $ip_address    Optional visitor IP.
	 * @param array        $extra_headers Additional headers (e.g., Content-Type).
	 * @return array
	 */
	public static function build_signed_headers( $payload, $api_secret, $business_info = null, $ip_address = '', $extra_headers = array() ) {
		$headers = is_array( $extra_headers ) ? $extra_headers : array();
		$timestamp = (string) time();
		$payload_to_sign = $timestamp . '.' . (string) $payload;
		$signature_raw = hash_hmac( 'sha256', $payload_to_sign, $api_secret, true );
		$signature = base64_encode( $signature_raw );

		$headers[ self::header_signature() ] = $signature;
		$headers[ self::header_timestamp() ] = $timestamp;

		if ( is_array( $business_info ) ) {
			$headers[ self::header_business_info() ] = base64_encode( wp_json_encode( $business_info ) );
		}

		if ( '' !== $ip_address ) {
			$headers[ self::header_visitor_ip() ] = $ip_address;
		}

		return $headers;
	}

	/**
	 * Normalize a wp_remote_* response into shared shape.
	 *
	 * @since 0.14.323
	 *
	 * @param array|WP_Error $response Raw response.
	 * @return array{status_code:int,body:string}|WP_Error
	 */
	public static function normalize_response( $response ) {
		if ( is_wp_error( $response ) ) {
			return $response;
		}

		if ( is_array( $response ) && array_key_exists( 'status_code', $response ) && array_key_exists( 'body', $response ) ) {
			return array(
				'status_code' => (int) $response['status_code'],
				'body'        => (string) $response['body'],
			);
		}

		return array(
			'status_code' => (int) wp_remote_retrieve_response_code( $response ),
			'body'        => (string) wp_remote_retrieve_body( $response ),
		);
	}
}
