<?php
/**
 * Trait: Niwa_AI_Frontend_Chat
 *
 * Frontend chat AJAX handlers, session management, request execution, and response caching.
 *
 * @package Niwa_AI
 */

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

trait Niwa_AI_Frontend_Chat {

    public function ajax_refresh_chat_nonce() {
        if ( function_exists( 'nocache_headers' ) ) {
            nocache_headers();
        }
		$session = Niwa_AI_Session_Utils::resolve_verified_session( true );

        wp_send_json_success(
            array(
                'nonce'      => wp_create_nonce( 'niwa_ai_chat_nonce' ),
                'expires_in' => intval( apply_filters( 'nonce_life', DAY_IN_SECONDS ) ),
				'session'    => $session,
            )
        );
    }

    private function verify_frontend_chat_ajax_nonce() {
        $nonce = '';
        foreach ( array( 'nonce', '_ajax_nonce' ) as $key ) {
            if ( isset( $_REQUEST[ $key ] ) && is_scalar( $_REQUEST[ $key ] ) ) {
                $nonce = sanitize_text_field( wp_unslash( $_REQUEST[ $key ] ) );
                break;
            }
        }

        if ( ! wp_verify_nonce( $nonce, 'niwa_ai_chat_nonce' ) ) {
            wp_send_json_error(
                array(
                    'error'   => __( 'Security check failed. Please refresh the page and try again.', 'niwa-ai' ),
                    'message' => __( 'Security check failed. Please refresh the page and try again.', 'niwa-ai' ),
                ),
                403
            );
        }
    }

    public function ajax_chat() {
		$request_received_at_ms = microtime( true ) * 1000;
        $this->verify_frontend_chat_ajax_nonce();
		$this->enforce_frontend_chat_payload_size();

	    // Keep AJAX payload JSON-clean even when WP_DEBUG is enabled.
	    if ( function_exists( 'ini_set' ) ) {
	        @ini_set( 'display_errors', '0' );
	    }
	    if ( function_exists( 'set_time_limit' ) ) {
	        @set_time_limit( 60 );
	    }
	    // The reply is stored durably by request_id and can be collected by the
	    // next page. Do not let a browser navigation terminate the PHP turn
	    // before that durable status is written.
	    ignore_user_abort( true );

        $message = isset( $_POST['message'] ) ? sanitize_text_field( wp_unslash( $_POST['message'] ) ) : '';
        if ( '' === trim( $message ) ) {
            $message_text = 'Missing message';
            wp_send_json_error(
                array(
                    'error' => $message_text,
                    'message' => $message_text,
                ),
                400
            );
        }

        $session = '';
        $request_id = isset( $_POST['request_id'] ) ? $this->sanitize_frontend_request_id( wp_unslash( $_POST['request_id'] ) ) : '';
        $include_full_history = $this->should_return_frontend_full_history();

        try {
            $incoming_session = isset( $_POST['session'] ) ? sanitize_text_field( wp_unslash( $_POST['session'] ) ) : '';
            $session = $this->resolve_frontend_session( $incoming_session );
            $this->set_frontend_session_cookie( $session );
            $request_context = $this->build_frontend_chat_request_context_from_post( $session, $request_id, $request_received_at_ms );

            $request_status = $this->get_frontend_chat_request_status( $session, $request_id );
            if ( ! empty( $request_status ) ) {
                if ( 'done' === (string) ( $request_status['status'] ?? '' ) && ! empty( $request_status['payload'] ) && is_array( $request_status['payload'] ) ) {
                    wp_send_json_success( $request_status['payload'] );
                }
                if ( in_array( (string) ( $request_status['status'] ?? '' ), array( 'queued', 'processing' ), true ) ) {
                    wp_send_json_success(
                        array(
                            'queued'      => true,
                            'queue_token' => ! empty( $request_status['queue_token'] ) ? (string) $request_status['queue_token'] : $this->build_frontend_request_poll_token( $session, $request_id ),
                            'request_id'  => $request_id,
                            'session'     => $session,
                            'status'      => (string) ( $request_status['status'] ?? 'processing' ),
                        )
                    );
                }
            }

			$this->enforce_frontend_chat_abuse_limits( $session );

            if ( ! $this->acquire_frontend_chat_lane( $session, 75 ) ) {
                $job = $this->find_or_enqueue_frontend_chat_job( $session, $message, $request_id, $request_context, $include_full_history );
                if ( ! empty( $job['id'] ) ) {
                    $this->set_frontend_chat_request_status(
                        $session,
                        $request_id,
                        'queued',
                        array(
                            'queue_token' => (string) $job['id'],
                        )
                    );
                    $this->dispatch_frontend_chat_worker( 'lane_busy' );
                    wp_send_json_success(
                        array(
                            'queued'      => true,
                            'queue_token' => (string) $job['id'],
                            'request_id'  => $request_id,
                            'session'     => $session,
                            'status'      => 'queued',
                        )
                    );
                }

                $message_text = __( 'This chat session is already processing a reply. Please wait for the current response and try again.', 'niwa-ai' );
                wp_send_json_error(
                    array(
                        'error' => $message_text,
                        'message' => $message_text,
                        'session' => $session,
                        'request_id' => $request_id,
                    ),
                    429
                );
            }
            register_shutdown_function( array( $this, 'release_frontend_chat_lane' ), $session );

            $history = $this->conversation_store ? $this->conversation_store->get( $session ) : array();
            $history = $this->ensure_frontend_opening_context_in_history( $session, $history );

            $this->set_frontend_chat_request_status( $session, $request_id, 'processing' );
            $run = $this->run_frontend_chat_request( $message, $session, $history, $include_full_history, $request_context );
            if ( empty( $run['success'] ) ) {
                $error = isset( $run['error'] ) ? (string) $run['error'] : 'Unknown error';
				$retryable = ! empty( $run['retryable'] );
				$this->set_frontend_chat_request_status(
					$session,
					$request_id,
					'failed',
					array(
						'error'      => $error,
						'retryable'  => $retryable,
						'error_code' => isset( $run['error_code'] ) ? sanitize_key( (string) $run['error_code'] ) : '',
					)
				);
                wp_send_json_error(
                    array(
						'error'          => $error,
						'message'        => $error,
						'request_id'     => $request_id,
						'retryable'      => $retryable,
						'retry_after_ms' => $retryable ? 2500 : 0,
                    ),
					$retryable ? 503 : 500
                );
            }

            $payload = isset( $run['payload'] ) && is_array( $run['payload'] ) ? $run['payload'] : array();
            $this->set_frontend_chat_request_status( $session, $request_id, 'done', array( 'payload' => $payload ) );
            wp_send_json_success( $payload );
        } catch ( \Throwable $e ) {
            $message = 'Chat request failed.';
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                $message .= ' ' . $e->getMessage();
            }
            if ( '' !== $session && '' !== $request_id ) {
				$this->set_frontend_chat_request_status( $session, $request_id, 'failed', array( 'error' => $message, 'retryable' => true ) );
            }
            wp_send_json_error(
                array(
                    'error' => $message,
                    'message' => $message,
                    'request_id' => $request_id,
					'retryable' => true,
					'retry_after_ms' => 2500,
                ),
				503
            );
        } finally {
            if ( '' !== $session ) {
                $this->release_frontend_chat_lane( $session );
            }
        }
    }

    public function ajax_chat_queue_status() {
        $this->verify_frontend_chat_ajax_nonce();
		if ( isset( $_SERVER['CONTENT_LENGTH'] ) && (int) $_SERVER['CONTENT_LENGTH'] > 8192 ) {
			wp_send_json_error( array( 'message' => 'Queue status payload is too large.' ), 413 );
		}

        $job_id = isset( $_POST['queue_token'] ) ? sanitize_text_field( wp_unslash( $_POST['queue_token'] ) ) : '';
        $request_id = isset( $_POST['request_id'] ) ? $this->sanitize_frontend_request_id( wp_unslash( $_POST['request_id'] ) ) : '';
		$incoming_session = isset( $_POST['session'] ) ? sanitize_text_field( wp_unslash( $_POST['session'] ) ) : '';
		$session          = Niwa_AI_Session_Utils::resolve_verified_session( false, $incoming_session );
		if ( '' === $session ) {
			wp_send_json_error(
				array(
					'error'   => 'Invalid frontend session credential.',
					'message' => 'Invalid frontend session credential.',
				),
				403
			);
		}
		$ip = Niwa_AI_Session_Utils::get_client_ip();
		foreach (
			array(
				array( 'front_status_global_minute', 'global', 5000 ),
				array( 'front_status_ip_minute', $ip, 120 ),
				array( 'front_status_session_minute', $session, 120 ),
			) as $limit
		) {
			if ( ! Niwa_AI_Session_Utils::consume_fixed_window_limit( $limit[0], $limit[1], $limit[2], MINUTE_IN_SECONDS ) ) {
				wp_send_json_error( array( 'message' => 'Queue status request limit reached.' ), 429 );
			}
		}
        if ( '' !== $request_id && '' !== $session ) {
            $request_status = $this->get_frontend_chat_request_status( $session, $request_id );
            if ( ! empty( $request_status ) ) {
                $request_status_name = (string) ( $request_status['status'] ?? '' );
                if ( 'done' === $request_status_name && ! empty( $request_status['payload'] ) && is_array( $request_status['payload'] ) ) {
                    wp_send_json_success(
                        array(
                            'done'    => true,
                            'payload' => $request_status['payload'],
                        )
                    );
                }
                if ( 'failed' === $request_status_name ) {
					$retryable = ! empty( $request_status['retryable'] );
                    wp_send_json_error(
                        array(
                            'error'   => isset( $request_status['error'] ) ? (string) $request_status['error'] : 'Queued chat failed.',
                            'message' => isset( $request_status['error'] ) ? (string) $request_status['error'] : 'Queued chat failed.',
							'retryable' => $retryable,
							'retry_after_ms' => $retryable ? 2500 : 0,
                        ),
						$retryable ? 503 : 500
                    );
                }
                if ( in_array( $request_status_name, array( 'queued', 'processing' ), true ) ) {
                    $request_queue_token = ! empty( $request_status['queue_token'] )
                        ? (string) $request_status['queue_token']
                        : $this->build_frontend_request_poll_token( $session, $request_id );
                    if (
                        'processing' === $request_status_name
                        && ! empty( $request_status['queue_token'] )
                        && $this->is_frontend_chat_request_processing_stale( $request_status )
                    ) {
                        $job_id = (string) $request_status['queue_token'];
                    } else {
                        if ( 'queued' === $request_status_name ) {
                            $this->dispatch_frontend_chat_worker( 'status_poll' );
                        }
                        wp_send_json_success(
                            array(
                                'queued'      => true,
                                'queue_token' => $request_queue_token,
                                'request_id'  => $request_id,
                                'session'     => $session,
                                'status'      => $request_status_name,
                            )
                        );
                    }
                }
            }
        }
        if ( '' === $job_id ) {
            wp_send_json_error(
                array(
                    'error'   => 'Missing queue token',
                    'message' => 'Missing queue token',
                ),
                400
            );
        }

		$status = $this->get_frontend_chat_job_status( $job_id, $session );
        if ( 'done' === $status['status'] ) {
            wp_send_json_success(
                array(
                    'done'    => true,
                    'payload' => isset( $status['payload'] ) && is_array( $status['payload'] ) ? $status['payload'] : array(),
                )
            );
        }

        if ( 'failed' === $status['status'] ) {
			$retryable = ! empty( $status['retryable'] );
            wp_send_json_error(
                array(
                    'error'   => isset( $status['error'] ) ? (string) $status['error'] : 'Queued chat failed.',
                    'message' => isset( $status['error'] ) ? (string) $status['error'] : 'Queued chat failed.',
					'retryable' => $retryable,
					'retry_after_ms' => $retryable ? 2500 : 0,
                ),
				$retryable ? 503 : 500
            );
        }

        if ( 'missing' === $status['status'] ) {
            $message = __( 'Queued chat token is no longer available.', 'niwa-ai' );
            wp_send_json_error(
                array(
                    'error'   => $message,
                    'message' => $message,
                ),
                404
            );
        }

        $this->dispatch_frontend_chat_worker( 'status_poll' );

        wp_send_json_success(
            array(
                'queued'      => true,
                'queue_token' => $job_id,
                'request_id'  => $request_id,
                'session'     => $session,
                'status'      => 'pending',
            )
        );
    }

    public function ensure_frontend_session_cookie() {
        $session = $this->resolve_frontend_session( '' );
        $this->set_frontend_session_cookie( $session );
        return $session;
    }

    private function resolve_frontend_session( $incoming_session ) {
		return Niwa_AI_Session_Utils::resolve_verified_session( true, $incoming_session );
    }

    private function set_frontend_session_cookie( $session ) {
		Niwa_AI_Session_Utils::set_session_cookie( $session );
    }

	private function enforce_frontend_chat_payload_size() {
		$content_length = isset( $_SERVER['CONTENT_LENGTH'] ) ? (int) $_SERVER['CONTENT_LENGTH'] : 0;
		$raw_message    = isset( $_POST['message'] ) && is_scalar( $_POST['message'] ) ? (string) wp_unslash( $_POST['message'] ) : '';
		if ( $content_length > 32768 || strlen( $raw_message ) > 8000 ) {
			wp_send_json_error(
				array(
					'error'   => 'Chat request payload is too large.',
					'message' => 'Chat request payload is too large.',
				),
				413
			);
		}
	}

	private function enforce_frontend_chat_abuse_limits( $session ) {
		$ip = Niwa_AI_Session_Utils::get_client_ip();
		$limits = array(
			array( 'front_chat_global_minute', 'global', (int) apply_filters( 'niwa_ai_frontend_chat_global_per_minute', 300 ), MINUTE_IN_SECONDS ),
			array( 'front_chat_ip_minute', $ip, (int) apply_filters( 'niwa_ai_frontend_chat_ip_per_minute', 20 ), MINUTE_IN_SECONDS ),
			array( 'front_chat_session_minute', $session, (int) apply_filters( 'niwa_ai_frontend_chat_session_per_minute', 12 ), MINUTE_IN_SECONDS ),
			array( 'front_chat_global_day', 'global', (int) apply_filters( 'niwa_ai_frontend_chat_global_per_day', 5000 ), DAY_IN_SECONDS ),
			array( 'front_chat_ip_day', $ip, (int) apply_filters( 'niwa_ai_frontend_chat_ip_per_day', 200 ), DAY_IN_SECONDS ),
			array( 'front_chat_session_day', $session, (int) apply_filters( 'niwa_ai_frontend_chat_session_per_day', 100 ), DAY_IN_SECONDS ),
		);

		foreach ( $limits as $limit ) {
			if ( ! Niwa_AI_Session_Utils::consume_fixed_window_limit( $limit[0], $limit[1], $limit[2], $limit[3] ) ) {
				wp_send_json_error(
					array(
						'error'       => 'Chat request limit reached. Please try again later.',
						'message'     => 'Chat request limit reached. Please try again later.',
						'retry_after' => MINUTE_IN_SECONDS,
					),
					429
				);
			}
		}
	}

    private function sanitize_frontend_request_id( $request_id ) {
        if ( ! is_scalar( $request_id ) ) {
            return '';
        }

        $request_id = trim( (string) $request_id );
        if ( '' === $request_id ) {
            return '';
        }

        return substr( preg_replace( '/[^a-zA-Z0-9_\-:.]/', '', $request_id ), 0, 96 );
    }

    private function build_frontend_request_poll_token( $session, $request_id ) {
        $request_id = $this->sanitize_frontend_request_id( $request_id );
        if ( '' === $request_id ) {
            return '';
        }

        return 'freq_' . md5( (string) $session . '|' . $request_id );
    }

    private function frontend_chat_request_cache_key( $session, $request_id ) {
        $request_id = $this->sanitize_frontend_request_id( $request_id );
        if ( '' === trim( (string) $session ) || '' === $request_id ) {
            return '';
        }

        return 'niwa_ai_front_req_' . md5( (string) $session . '|' . $request_id );
    }

    private function get_frontend_chat_request_status( $session, $request_id ) {
        $key = $this->frontend_chat_request_cache_key( $session, $request_id );
        if ( '' === $key ) {
            return array();
        }

        $status = Niwa_AI_Durable_Cache::get( $key );
        $status = is_array( $status ) ? $status : array();
        if (
            $this->is_frontend_chat_request_processing_stale( $status )
            && empty( $status['queue_token'] )
        ) {
            $error = 'Chat request was interrupted. Please try again.';
			$this->set_frontend_chat_request_status( $session, $request_id, 'failed', array( 'error' => $error, 'retryable' => true ) );
            $status['status']     = 'failed';
            $status['error']      = $error;
			$status['retryable']  = true;
            $status['updated_at'] = time();
        }

        return $status;
    }

    private function is_frontend_chat_request_processing_stale( $status ) {
        if (
            ! is_array( $status )
            || 'processing' !== (string) ( $status['status'] ?? '' )
        ) {
            return false;
        }

        $updated_at  = isset( $status['updated_at'] ) ? intval( $status['updated_at'] ) : 0;
        $stale_after = max( 75, (int) apply_filters( 'niwa_ai_frontend_chat_processing_stale_after', 120 ) );
        return $updated_at <= 0 || $updated_at + $stale_after <= time();
    }

    private function set_frontend_chat_request_status( $session, $request_id, $status, $data = array() ) {
        $key = $this->frontend_chat_request_cache_key( $session, $request_id );
        if ( '' === $key ) {
            return;
        }

        $record = array_merge(
            is_array( $data ) ? $data : array(),
            array(
                'status'     => sanitize_key( (string) $status ),
                'request_id' => $this->sanitize_frontend_request_id( $request_id ),
                'session'    => (string) $session,
                'updated_at' => time(),
            )
        );
        // The browser may navigate while a direct request is still running.
        // Keep the request-level status longer than the client recovery window
        // so the next page can resume by request_id without duplicating work.
        Niwa_AI_Durable_Cache::set( $key, $record, HOUR_IN_SECONDS );
    }

    private function build_frontend_chat_request_context_from_post( $session, $request_id = '', $request_received_at_ms = 0 ) {
        $context = array(
			'session'                => $session,
			'request_id'             => $this->sanitize_frontend_request_id( $request_id ),
			'request_received_at_ms' => max( 0, (float) $request_received_at_ms ),
        );

        if ( isset( $_POST['page_url'] ) ) {
            $context['page_url'] = esc_url_raw( wp_unslash( $_POST['page_url'] ) );
        }

        if ( isset( $_POST['visitor_email'] ) ) {
            $candidate = sanitize_email( wp_unslash( $_POST['visitor_email'] ) );
            if ( '' !== $candidate && is_email( $candidate ) ) {
                $context['visitor_email'] = $candidate;
            }
        }

        if ( isset( $_POST['pending_cart'] ) ) {
            $raw_pending_cart = wp_unslash( $_POST['pending_cart'] );
            if ( is_string( $raw_pending_cart ) ) {
                $decoded_pending_cart = json_decode( $raw_pending_cart, true );
                if ( is_array( $decoded_pending_cart ) ) {
                    $context['pending_cart'] = $decoded_pending_cart;
                }
            }
        }

        $context['cart_enabled'] = wp_doing_ajax() && isset( $_POST['action'] ) && 'niwa_ai_chat' === sanitize_key( wp_unslash( $_POST['action'] ) );

        return $this->normalize_frontend_chat_request_context( $context, $session );
    }

    private function normalize_frontend_chat_request_context( $context, $session ) {
        $context     = is_array( $context ) ? $context : array();
        $ui_language = ! empty( $context['ui_language'] ) ? strtolower( substr( (string) $context['ui_language'], 0, 2 ) ) : $this->get_frontend_ui_language();
        $page_url    = ! empty( $context['page_url'] ) ? esc_url_raw( (string) $context['page_url'] ) : home_url( '/' );

        $visitor_email = '';
        if ( ! empty( $context['visitor_email'] ) ) {
            $candidate = sanitize_email( (string) $context['visitor_email'] );
            if ( '' !== $candidate && is_email( $candidate ) ) {
                $visitor_email = $candidate;
            }
        }

        $pending_cart = array();
        if ( ! empty( $context['pending_cart'] ) && is_array( $context['pending_cart'] ) ) {
            $pending_cart = $this->sanitize_frontend_pending_cart_context( $context['pending_cart'] );
        }

        return array(
            'ui_language'           => $ui_language,
            'conversation_language' => ! empty( $context['conversation_language'] ) ? strtolower( substr( (string) $context['conversation_language'], 0, 2 ) ) : '',
            'session'               => $session,
            'request_id'            => ! empty( $context['request_id'] ) ? $this->sanitize_frontend_request_id( $context['request_id'] ) : '',
			'request_received_at_ms' => ! empty( $context['request_received_at_ms'] ) ? max( 0, (float) $context['request_received_at_ms'] ) : 0,
			'queued_at_ms'           => ! empty( $context['queued_at_ms'] ) ? max( 0, (float) $context['queued_at_ms'] ) : 0,
            'page_url'              => $page_url,
            'cart_enabled'          => ! empty( $context['cart_enabled'] ),
            'visitor_email'         => $visitor_email,
            'pending_cart'          => $pending_cart,
        );
    }

    private function run_frontend_chat_request( $message, $session, $history = array(), $include_full_history = null, $request_context = array() ) {
        $request_context = $this->normalize_frontend_chat_request_context( $request_context, $session );
		$request_id     = isset( $request_context['request_id'] ) ? (string) $request_context['request_id'] : '';
		$trace_meta     = array( 'success' => false );
		$trace_started  = $this->latency_tracker && method_exists( $this->latency_tracker, 'start_trace' )
			? $this->latency_tracker->start_trace(
				$request_id,
				array(
					'session_hash' => substr( hash( 'sha256', (string) $session ), 0, 16 ),
					'ui_language'  => isset( $request_context['ui_language'] ) ? (string) $request_context['ui_language'] : '',
				)
			)
			: false;
		if ( $trace_started ) {
			$received_at_ms = isset( $request_context['request_received_at_ms'] ) ? (float) $request_context['request_received_at_ms'] : 0;
			$queued_at_ms   = isset( $request_context['queued_at_ms'] ) ? (float) $request_context['queued_at_ms'] : 0;
			$processing_at_ms = microtime( true ) * 1000;
			if ( $received_at_ms > 0 ) {
				$preprocessing_end = $queued_at_ms > 0 ? $queued_at_ms : $processing_at_ms;
				$this->latency_tracker->add_stage( $request_id, 'request_preprocessing', max( 0, $preprocessing_end - $received_at_ms ) );
			}
			if ( $queued_at_ms > 0 ) {
				$this->latency_tracker->add_stage( $request_id, 'queue_wait', max( 0, $processing_at_ms - $queued_at_ms ) );
			}
		}

		try {

        $result = $this->rlm_engine->answer(
            $message,
            $session,
            $history,
            $request_context
        );
        if ( ! is_array( $result ) ) {
            $result = array(
                'success' => false,
                'error'   => 'Invalid response format from engine.',
            );
        }

        if ( ! empty( $result['error'] ) && ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) ) ) {
            error_log(
                'Niwa AI Chat Engine warning ' . wp_json_encode(
                    array(
                        'source' => isset( $result['source'] ) ? (string) $result['source'] : '',
                        'error'  => (string) $result['error'],
                    )
                )
            );
        }

        if ( empty( $result['success'] ) ) {
			$trace_meta['source'] = isset( $result['source'] ) ? (string) $result['source'] : '';
            return array(
				'success'    => false,
				'error'      => isset( $result['error'] ) ? (string) $result['error'] : 'Unknown error',
				'retryable'  => ! empty( $result['retryable'] ),
				'error_code' => isset( $result['error_code'] ) ? sanitize_key( (string) $result['error_code'] ) : '',
            );
        }

		$response_stage_started = $trace_started ? $this->latency_tracker->start_stage( $request_id ) : 0;
        $answer_text = isset( $result['answer'] ) ? (string) $result['answer'] : '';

        // Extract markdown page links [Label](url) → suggestion chips with page_link: codes.
        $link_data   = $this->extract_page_link_suggestions( $answer_text );
        $answer_text = $link_data['answer'];

        $payload = array(
            'answer'            => $answer_text,
            'source'            => isset( $result['source'] ) ? (string) $result['source'] : '',
            'confidence'        => isset( $result['confidence'] ) ? $result['confidence'] : '',
            'cards'             => isset( $result['cards'] ) && is_array( $result['cards'] ) ? $result['cards'] : array(),
            'suggestions'       => ! empty( $link_data['suggestions'] ) ? $link_data['suggestions'] : null,
            'suggestions_codes' => ! empty( $link_data['codes'] ) ? $link_data['codes'] : null,
        );

		// Product results already provide their own focused actions. Do not add
		// a separate page-link pill beneath the grid for the same answer.
		if ( ! empty( $payload['cards'] ) ) {
			$payload['suggestions']       = null;
			$payload['suggestions_codes'] = null;
		}

        if ( ! empty( $result['cart'] ) && is_array( $result['cart'] ) ) {
            $payload['cart'] = $result['cart'];
        }

        // Agent-driven browser navigation (navigate_to_page tool). The chat
        // client validates same-origin again before redirecting.
        if ( ! empty( $result['navigation'] ) && is_array( $result['navigation'] ) ) {
            $payload['navigation'] = $result['navigation'];
        }

        // Propagate lead-form activation flag from the RLM engine so the
        // frontend chat client can mount the form card/chat adapter.
        if ( ! empty( $result['lead_form_trigger'] ) ) {
            $payload['lead_form_trigger'] = true;
            if ( ! empty( $result['lead_form_trigger_reason'] ) ) {
                $payload['lead_form_trigger_reason'] = (string) $result['lead_form_trigger_reason'];
            }
        }

		// Propagate scheduler activation through the public chat response. The
		// frontend client listens for these fields and mounts the booking calendar.
		if ( ! empty( $result['scheduler_trigger'] ) ) {
			$payload['scheduler_trigger'] = true;
			if ( ! empty( $result['scheduler_trigger_reason'] ) ) {
				$payload['scheduler_trigger_reason'] = (string) $result['scheduler_trigger_reason'];
			}
			if ( ! empty( $result['scheduler_service_id'] ) ) {
				$payload['scheduler_service_id'] = sanitize_key( (string) $result['scheduler_service_id'] );
			}
			if ( ! empty( $result['scheduler_team_member_id'] ) ) {
				$payload['scheduler_team_member_id'] = sanitize_key( (string) $result['scheduler_team_member_id'] );
			}
		}

		$finalized_payload = $this->finalize_frontend_chat_payload( $session, $message, $payload, $include_full_history );
		if ( $trace_started ) {
			$this->latency_tracker->finish_stage(
				$request_id,
				'response_finalize',
				$response_stage_started,
				array( 'cards' => count( (array) $payload['cards'] ) )
			);
		}
		$result_source = isset( $result['source'] ) ? (string) $result['source'] : '';
		$trace_meta = array(
			'success'    => 'model_error' !== $result_source,
			'source'     => $result_source,
			'error_code' => isset( $result['error_code'] ) ? sanitize_key( (string) $result['error_code'] ) : '',
		);

        return array(
            'success' => true,
			'payload' => $finalized_payload,
        );
		} finally {
			if ( $trace_started ) {
				$this->latency_tracker->finish_trace( $request_id, $trace_meta );
			}
		}
    }

    private function ensure_frontend_opening_context_in_history( $session, $history ) {
        $history = is_array( $history ) ? array_values( $history ) : array();
        if ( ! empty( $history ) || ! $this->conversation_store ) {
            return $history;
        }

        $opening_message = $this->get_frontend_opening_context_message();
        if ( '' === $opening_message ) {
            return $history;
        }

        return $this->conversation_store->append( $session, 'bot', $opening_message );
    }

    private function get_frontend_opening_context_message() {
        $opening_message = '';
        if ( isset( $_POST['opening_message'] ) ) {
            $opening_message = sanitize_textarea_field( wp_unslash( $_POST['opening_message'] ) );
        }

        $opening_message = trim( wp_strip_all_tags( (string) $opening_message ) );
        if ( '' !== $opening_message ) {
            return $opening_message;
        }

        return $this->get_configured_frontend_greeting_text();
    }

    private function get_configured_frontend_greeting_text() {
        $general = function_exists( 'get_option' ) ? get_option( Niwa_AI_Constants::OPT_SETTINGS, array() ) : array();
        $ui      = function_exists( 'get_option' ) ? get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() ) : array();
        $opts    = array_merge(
            is_array( $general ) ? $general : array(),
            is_array( $ui ) ? $ui : array()
        );

        $custom_greeting = isset( $opts['greeting_text'] ) ? trim( wp_strip_all_tags( (string) $opts['greeting_text'] ) ) : '';
        if ( '' !== $custom_greeting ) {
            return sanitize_textarea_field( $custom_greeting );
        }

        return $this->get_default_frontend_greeting_text( isset( $opts['ui_language'] ) ? $opts['ui_language'] : 'en' );
    }

    private function get_default_frontend_greeting_text( $language = 'en' ) {
        return Niwa_AI_Constants::frontend_default_greeting( $language );
    }

    private function finalize_frontend_chat_payload( $session, $message, $payload, $include_full_history = null ) {
        $payload = is_array( $payload ) ? $payload : array();
        if ( null === $include_full_history ) {
            $include_full_history = $this->should_return_frontend_full_history();
        }

        $history_snapshot = array();
        if ( $this->conversation_store ) {
            $history_snapshot = $this->conversation_store->append( $session, 'user', $message );
            $history_snapshot = $this->conversation_store->append(
                $session,
                'bot',
                isset( $payload['answer'] ) ? (string) $payload['answer'] : '',
                isset( $payload['cards'] ) && is_array( $payload['cards'] ) ? $payload['cards'] : array()
            );
        }

        if ( ! $include_full_history ) {
            unset( $payload['history'] );
        } else {
            $payload['history'] = is_array( $history_snapshot ) ? $history_snapshot : array();
        }

        $payload['session'] = $session;

        // Permanently log the customer question for admin analytics.
        if ( $this->frontend_question_log && '' !== trim( (string) $message ) && $this->should_log_frontend_question( $include_full_history ) ) {
            try {
                $this->frontend_question_log->log_question(
                    $session,
                    $message,
                    isset( $payload['answer'] ) ? (string) $payload['answer'] : ''
                );
            } catch ( \Throwable $e ) {
                // Logging must never break the chat response.
                if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
                    error_log( 'Niwa AI question log error: ' . $e->getMessage() );
                }
            }
        }

        return $payload;
    }

    private function should_return_frontend_full_history() {
        $include_history = function_exists( 'current_user_can' ) && current_user_can( 'manage_options' );

        if ( function_exists( 'apply_filters' ) ) {
            $include_history = (bool) apply_filters( 'niwa_ai_frontend_chat_include_history', $include_history );
        }

        return (bool) $include_history;
    }

    private function sanitize_frontend_pending_cart_context( $pending_cart ) {
        $pending_cart = is_array( $pending_cart ) ? $pending_cart : array();
        $out          = array(
            'product_id'       => isset( $pending_cart['product_id'] ) ? absint( $pending_cart['product_id'] ) : 0,
            'product_query'    => isset( $pending_cart['product_query'] ) ? sanitize_text_field( (string) $pending_cart['product_query'] ) : '',
            'product_name'     => isset( $pending_cart['product_name'] ) ? sanitize_text_field( (string) $pending_cart['product_name'] ) : '',
            'selected'         => array(),
            'required_options' => array(),
            'all_options'      => array(),
        );

        foreach ( array( 'selected' ) as $map_key ) {
            if ( empty( $pending_cart[ $map_key ] ) || ! is_array( $pending_cart[ $map_key ] ) ) {
                continue;
            }
            foreach ( $pending_cart[ $map_key ] as $key => $value ) {
                if ( is_scalar( $value ) ) {
                    $out[ $map_key ][ sanitize_text_field( (string) $key ) ] = sanitize_text_field( (string) $value );
                }
            }
        }

        foreach ( array( 'required_options', 'all_options' ) as $list_key ) {
            if ( empty( $pending_cart[ $list_key ] ) || ! is_array( $pending_cart[ $list_key ] ) ) {
                continue;
            }
            foreach ( array_slice( $pending_cart[ $list_key ], 0, 8 ) as $option_group ) {
                if ( ! is_array( $option_group ) ) {
                    continue;
                }
                $clean_group = array(
                    'attribute' => isset( $option_group['attribute'] ) ? sanitize_text_field( (string) $option_group['attribute'] ) : '',
                    'label'     => isset( $option_group['label'] ) ? sanitize_text_field( (string) $option_group['label'] ) : '',
                    'options'   => array(),
                );
                foreach ( (array) ( $option_group['options'] ?? array() ) as $choice ) {
                    if ( ! is_array( $choice ) ) {
                        continue;
                    }
                    $clean_group['options'][] = array(
                        'value' => isset( $choice['value'] ) ? sanitize_text_field( (string) $choice['value'] ) : '',
                        'label' => isset( $choice['label'] ) ? sanitize_text_field( (string) $choice['label'] ) : '',
                    );
                    if ( count( $clean_group['options'] ) >= 20 ) {
                        break;
                    }
                }
                if ( '' !== $clean_group['attribute'] || '' !== $clean_group['label'] ) {
                    $out[ $list_key ][] = $clean_group;
                }
            }
        }

        return $out;
    }

    private function should_log_frontend_question( $include_full_history ) {
		$should_log = true;

        if ( function_exists( 'apply_filters' ) ) {
            $should_log = (bool) apply_filters( 'niwa_ai_frontend_chat_log_question', $should_log, $include_full_history );
        }

        return (bool) $should_log;
    }

    private function get_frontend_ui_language() {
        $language = '';

        if ( function_exists( 'get_option' ) ) {
            $ui_settings = get_option( Niwa_AI_Constants::OPT_UI_SETTINGS, array() );
            $language    = is_array( $ui_settings ) && isset( $ui_settings['ui_language'] )
                ? (string) $ui_settings['ui_language']
                : '';
        }

        $language = strtolower( trim( str_replace( '-', '_', $language ) ) );
        if ( '' === $language && function_exists( 'determine_locale' ) ) {
            $language = strtolower( trim( str_replace( '-', '_', (string) determine_locale() ) ) );
        }

        $parts    = preg_split( '/_+/', $language );
        $language = isset( $parts[0] ) ? preg_replace( '/[^a-z]/', '', (string) $parts[0] ) : '';
        if ( 'bs' === $language ) {
            return 'sr';
        }

        return '' !== $language ? substr( $language, 0, 2 ) : 'en';
    }

    /**
     * Extract markdown links [Label](url) from answer text.
     * Returns cleaned answer + suggestion arrays for page_link chips.
     */
    private function extract_page_link_suggestions( $answer ) {
        $suggestions = array();
        $codes       = array();

        if ( empty( $answer ) || ! is_string( $answer ) ) {
            return array(
                'answer'      => (string) $answer,
                'suggestions' => $suggestions,
                'codes'       => $codes,
            );
        }

        // Match markdown links: [Label](url)
        // The URL must start with http://, https://, or /
        $pattern = '/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^\s\)]+)\)/u';

        if ( preg_match_all( $pattern, $answer, $matches, PREG_SET_ORDER ) ) {
            foreach ( $matches as $m ) {
                $label = trim( $m[1] );
                $url   = trim( $m[2] );
                if ( '' !== $label && '' !== $url ) {
                    $suggestions[] = $label;
                    $codes[]       = 'page_link:' . $url;
                }
            }
        }

        if ( ! empty( $suggestions ) ) {
            // Strip the markdown links from the answer text.
            $cleaned = preg_replace( $pattern, '', $answer );
            // Clean up leftover whitespace (double spaces, trailing spaces before punctuation).
            $cleaned = preg_replace( '/[ \t]+/u', ' ', $cleaned );
            $cleaned = preg_replace( '/\n{3,}/', "\n\n", $cleaned );
            $cleaned = trim( $cleaned );
            $answer  = $cleaned;
        }

        return array(
            'answer'      => $answer,
            'suggestions' => $suggestions,
            'codes'       => $codes,
        );
    }

}
