<?php
/**
 * MindWebConcierge Frontend Stream Handler
 *
 * @package MindWebConcierge
 */

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

add_action( 'wp_ajax_mindweco_send_message_stream', 'mindweco_frontend_send_message_stream' );
add_action( 'wp_ajax_nopriv_mindweco_send_message_stream', 'mindweco_frontend_send_message_stream' );
add_action( 'wp_ajax_mindweco_prepare_ask_socket', 'mindweco_prepare_ask_socket' );
add_action( 'wp_ajax_nopriv_mindweco_prepare_ask_socket', 'mindweco_prepare_ask_socket' );
add_action( 'wp_ajax_mindweco_save_socket_response', 'mindweco_save_socket_response' );
add_action( 'wp_ajax_nopriv_mindweco_save_socket_response', 'mindweco_save_socket_response' );

function mindweco_get_service_payload_for_ask() {
	$service_links = get_option( 'mindweco_service_links', array() );

	if ( ! is_array( $service_links ) ) {
		return array();
	}

	$services = array();
	foreach ( $service_links as $service ) {
		if ( empty( $service['url'] ) ) {
			continue;
		}

		$services[] = array(
			'name'        => isset( $service['title'] ) ? sanitize_text_field( $service['title'] ) : '',
			'description' => isset( $service['description'] ) ? sanitize_textarea_field( $service['description'] ) : '',
			'url'         => esc_url_raw( $service['url'] ),
		);
	}

	return $services;
}

function mindweco_normalize_language_code( $language ) {
	$language = sanitize_text_field( (string) $language );

	if ( '' === $language ) {
		return '';
	}

	$language = strtolower( $language );
	$parts    = preg_split( '/[-_]/', $language );

	return ! empty( $parts[0] ) ? $parts[0] : '';
}

function mindweco_get_ask_socket_url( $base_url, $token, $app_guid, $app_id, $conversation_id, $category_id, $agent_role, $agent_goal, $language ) {
	$socket_base = preg_replace( '/^https?:\/\//', 'wss://', untrailingslashit( esc_url_raw( $base_url ) ) );

	$query_args = array(
		'token'             => sanitize_text_field( $token ),
		'subscription_guid' => sanitize_text_field( $app_guid ),
		'app_id'            => absint( $app_id ),
		'conversation_id'   => sanitize_text_field( $conversation_id ),
		'category_id'       => absint( $category_id ),
		'agent_role'        => sanitize_text_field( $agent_role ),
		'agent_goal'        => sanitize_textarea_field( $agent_goal ),
		'language'          => mindweco_normalize_language_code( $language ),
	);

	$query_args = array_filter(
		$query_args,
		static function ( $value ) {
			return '' !== $value && null !== $value;
		}
	);

	return add_query_arg( $query_args, trailingslashit( $socket_base ) . 'ws/ask' );
}

function mindweco_get_or_create_conversation_for_ask( $session_id ) {
	global $wpdb;

	if ( empty( $session_id ) ) {
		return '';
	}

	$conversation = $wpdb->get_row(
		$wpdb->prepare(
			"SELECT * FROM {$wpdb->prefix}mindweco_conversations
			WHERE cookie_id = %s AND is_cleared = 0
			ORDER BY created_at DESC LIMIT 1",
			$session_id
		)
	);

	if ( $conversation ) {
		return $conversation->id;
	}

	$conversation_id = wp_generate_uuid4();
	$wpdb->insert(
		"{$wpdb->prefix}mindweco_conversations",
		array(
			'id'                => $conversation_id,
			'cookie_id'         => $session_id,
			'ip_address'        => mindweco_get_client_ipv4(),
			'screen_resolution' => isset( $_POST['screen_resolution'] ) ? sanitize_text_field( wp_unslash( $_POST['screen_resolution'] ) ) : '',
			'user_agent'        => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
			'language_accepted' => mindweco_normalize_language_code( isset( $_POST['language_accepted'] ) ? wp_unslash( $_POST['language_accepted'] ) : '' ),
			'referrer'          => isset( $_SERVER['HTTP_REFERER'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
			'timezone'          => isset( $_POST['timezone'] ) ? sanitize_text_field( wp_unslash( $_POST['timezone'] ) ) : '',
			'platform'          => isset( $_POST['platform'] ) ? sanitize_text_field( wp_unslash( $_POST['platform'] ) ) : '',
			'created_at'        => current_time( 'mysql', true ),
		),
		array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
	);

	return $conversation_id;
}

function mindweco_prepare_ask_socket() {
	global $wpdb;

	$nonce_value = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
	if ( ! wp_verify_nonce( $nonce_value, 'mindweco_frontend_nonce' ) ) {
		wp_send_json_error( array( 'message' => 'Security check failed' ) );
	}

	$options   = get_option( 'mindweco_plugin_options', array() );
	$base_url  = isset( $options['base_url'] ) ? $options['base_url'] : '';
	$token     = mindweco_decrypt_api_key( get_option( 'mindweco_plugin_access_token', '' ) );
	$category  = get_option( 'mindweco_plugin_category', null );
	$app_guid  = get_option( 'mindweco_plugin_guid', '' );
	$top_k     = absint( get_option( 'mindweco_ask_setting', 6 ) );

	if ( ! $category || empty( $category['id'] ) || empty( $base_url ) || empty( $token ) ) {
		wp_send_json_error( array( 'message' => 'MindWebConcierge configuration missing' ) );
	}

	$question              = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
	$post_id               = absint( isset( $_POST['post_id'] ) ? $_POST['post_id'] : 0 );
	$ask_only_current_page = ! empty( $_POST['ask_only_current_page'] );
	$preopen_socket        = ! empty( $_POST['preopen_socket'] );
	$session_id            = Mindweco_Session::get_frontend_session_id();

	if ( empty( $session_id ) ) {
		wp_send_json_error( array( 'message' => esc_html__( 'Chat session could not be initialized.', 'mind-web-concierge' ) ) );
		return;
	}

	$category_id           = absint( $category['id'] );
	$notebooks             = mindweco_get_notebooks_for_page( $post_id );
	$has_notebooks         = ! empty( $notebooks );

	if ( $has_notebooks && $ask_only_current_page ) {
		$category_ids = $notebooks;
	} elseif ( $has_notebooks && ! $ask_only_current_page ) {
		$category_ids = array_merge( array( $category_id ), $notebooks );
	} elseif ( ! $has_notebooks && $ask_only_current_page ) {
		$category_ids = array();
	} else {
		$category_ids = array( $category_id );
	}

	$conversation_id = mindweco_get_or_create_conversation_for_ask( $session_id );
	if ( empty( $conversation_id ) ) {
		wp_send_json_error( array( 'message' => esc_html__( 'Chat session could not be initialized.', 'mind-web-concierge' ) ) );
		return;
	}
	$question_page_based = ! empty( $category_ids ) ? wp_json_encode( $category_ids ) : '[]';
	$question_id = 0;

	if ( ! $preopen_socket || '' !== $question ) {
		$wpdb->insert(
			"{$wpdb->prefix}mindweco_questions",
			array(
				'conversation_id'     => $conversation_id,
				'question_text'       => $question,
				'page_id'             => $post_id,
				'question_page_based' => $question_page_based,
				'created_at'          => current_time( 'mysql', true ),
			),
			array( '%s', '%s', '%d', '%s', '%s' )
		);
		$question_id = (int) $wpdb->insert_id;
	}

	$indexing_options = get_option( 'mindweco_plugin_indexing', array() );
	if ( is_string( $indexing_options ) ) {
		$indexing_options = maybe_unserialize( $indexing_options );
	}
	$indexing_options = is_array( $indexing_options ) ? $indexing_options : array();
	$app_id           = isset( $indexing_options['id'] ) ? absint( $indexing_options['id'] ) : $category_id;
	$agent_role       = get_option( 'mindweco_plugin_agent_role', get_option( 'agent_role', '' ) );
	$agent_goal       = get_option( 'mindweco_plugin_agent_goal', get_option( 'agent_goal', '' ) );
	$language         = mindweco_normalize_language_code( isset( $_POST['language_accepted'] ) ? wp_unslash( $_POST['language_accepted'] ) : '' );

	wp_send_json_success(
		array(
			'url'             => mindweco_get_ask_socket_url( $base_url, $token, $app_guid, $app_id, $conversation_id, $category_id, $agent_role, $agent_goal, $language ),
			'question_id'     => $question_id,
			'conversation_id' => $conversation_id,
			'payload'         => array(
				'question'           => $question,
				'top_k'              => $top_k,
				'category_id'        => $category_id,
				'category_ids'       => array_map( 'absint', $category_ids ),
				'distance'           => 1,
				'agent_role'         => $agent_role,
				'agent_goal'         => $agent_goal,
				'services'           => mindweco_get_service_payload_for_ask(),
				'preopen_socket'     => $preopen_socket ? 1 : 0,
			),
		)
	);
}

function mindweco_save_socket_response() {
	global $wpdb;

	$nonce_value = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
	if ( ! wp_verify_nonce( $nonce_value, 'mindweco_frontend_nonce' ) ) {
		wp_send_json_error( array( 'message' => 'Security check failed' ) );
	}

	$question_id = absint( isset( $_POST['question_id'] ) ? $_POST['question_id'] : 0 );
	$response    = isset( $_POST['response'] ) ? sanitize_textarea_field( wp_unslash( $_POST['response'] ) ) : '';
	$answer_status = isset( $_POST['answer_status'] ) ? intval( sanitize_text_field( wp_unslash( $_POST['answer_status'] ) ) ) : 0;
	$answer_status_source = isset( $_POST['answer_status_source'] ) ? sanitize_text_field( wp_unslash( $_POST['answer_status_source'] ) ) : 'default(0)';

	if ( $answer_status < 0 ) {
		$answer_status = 0;
	}

	if ( ! $question_id || '' === $response ) {
		wp_send_json_error( array( 'message' => 'Missing response data' ) );
	}

	if ( function_exists( 'mindweco_ensure_responses_answer_status_column' ) ) {
		mindweco_ensure_responses_answer_status_column();
	}

	$wpdb->insert(
		"{$wpdb->prefix}mindweco_responses",
		array(
			'question_id'   => $question_id,
			'response_text' => $response,
			'answer_status' => $answer_status,
			'created_at'    => current_time( 'mysql', true ),
		),
		array( '%d', '%s', '%d', '%s' )
	);

	wp_send_json_success();
}

/**
 * Extract the first complete JSON object from a plain or SSE-wrapped stream tail.
 *
 * @param string $text Raw response text.
 * @return string JSON object string, or an empty string.
 */
function mindweco_extract_json_object_from_stream_text( $text ) {
	$text = trim( (string) $text );

	if ( '' === $text ) {
		return '';
	}

	if ( preg_match( '/^data:\s*/m', $text ) ) {
		$lines = preg_split( '/\R/', $text );
		$parts = array();

		foreach ( $lines as $line ) {
			$line = trim( $line );

			if ( '' === $line || 'data: [DONE]' === $line ) {
				continue;
			}

			if ( 0 === strpos( $line, 'data:' ) ) {
				$line = trim( substr( $line, 5 ) );
			}

			$parts[] = $line;
		}

		$text = trim( implode( '', $parts ) );
	}

	$start = strpos( $text, '{' );
	if ( false === $start ) {
		return '';
	}

	$depth     = 0;
	$in_string = false;
	$escaped   = false;
	$length    = strlen( $text );

	for ( $i = $start; $i < $length; $i++ ) {
		$char = $text[ $i ];

		if ( $in_string ) {
			if ( $escaped ) {
				$escaped = false;
			} elseif ( '\\' === $char ) {
				$escaped = true;
			} elseif ( '"' === $char ) {
				$in_string = false;
			}

			continue;
		}

		if ( '"' === $char ) {
			$in_string = true;
		} elseif ( '{' === $char ) {
			$depth++;
		} elseif ( '}' === $char ) {
			$depth--;

			if ( 0 === $depth ) {
				return substr( $text, $start, $i - $start + 1 );
			}
		}
	}

	return substr( $text, $start );
}

/**
 * Extract all complete JSON objects from a plain or SSE-wrapped stream tail.
 *
 * @param string $text Raw response text.
 * @return array JSON object strings.
 */
function mindweco_extract_json_objects_from_stream_text( $text ) {
	$text = trim( (string) $text );

	if ( '' === $text ) {
		return array();
	}

	if ( preg_match( '/^data:\s*/m', $text ) ) {
		$lines = preg_split( '/\R/', $text );
		$parts = array();

		foreach ( $lines as $line ) {
			$line = trim( $line );

			if ( '' === $line || 'data: [DONE]' === $line ) {
				continue;
			}

			if ( 0 === strpos( $line, 'data:' ) ) {
				$line = trim( substr( $line, 5 ) );
			}

			$parts[] = $line;
		}

		$text = trim( implode( "\n", $parts ) );
	}

	$objects = array();
	$offset  = 0;
	$length  = strlen( $text );

	while ( $offset < $length ) {
		$start = strpos( $text, '{', $offset );

		if ( false === $start ) {
			break;
		}

		$depth     = 0;
		$in_string = false;
		$escaped   = false;

		for ( $i = $start; $i < $length; $i++ ) {
			$char = $text[ $i ];

			if ( $in_string ) {
				if ( $escaped ) {
					$escaped = false;
				} elseif ( '\\' === $char ) {
					$escaped = true;
				} elseif ( '"' === $char ) {
					$in_string = false;
				}

				continue;
			}

			if ( '"' === $char ) {
				$in_string = true;
			} elseif ( '{' === $char ) {
				$depth++;
			} elseif ( '}' === $char ) {
				$depth--;

				if ( 0 === $depth ) {
					$objects[] = substr( $text, $start, $i - $start + 1 );
					$offset    = $i + 1;
					continue 2;
				}
			}
		}

		break;
	}

	return $objects;
}

function mindweco_frontend_send_message_stream() {
	global $wpdb;

	header( 'X-Requested-With: XMLHttpRequest' );

	$nonce_value  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
	$nonce_verify = wp_verify_nonce( $nonce_value, 'mindweco_frontend_nonce' );

	if ( ! $nonce_verify ) {
		wp_send_json_error( array( 'message' => 'Security check failed' ) );
		return;
	}

	$session_id = Mindweco_Session::get_frontend_session_id();

	if ( empty( $session_id ) ) {
		wp_send_json_error( array( 'message' => esc_html__( 'Chat session could not be initialized.', 'mind-web-concierge' ) ) );
		return;
	}

	$conversation = $wpdb->get_row(
		$wpdb->prepare(
			"SELECT * FROM {$wpdb->prefix}mindweco_conversations
			WHERE cookie_id = %s AND is_cleared = 0
			ORDER BY created_at DESC LIMIT 1",
			$session_id
    	)
	);

	if ( ! $conversation ) {
		$conversation_id = wp_generate_uuid4();
		$wpdb->insert(
			"{$wpdb->prefix}mindweco_conversations",
			array(
				'id'                => $conversation_id,
				'cookie_id'         => $session_id,
				'ip_address'        => mindweco_get_client_ipv4(),
				'screen_resolution' => isset( $_POST['screen_resolution'] ) ? sanitize_text_field( wp_unslash( $_POST['screen_resolution'] ) ) : '',
				'user_agent'        => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '',
				'language_accepted' => mindweco_normalize_language_code( isset( $_POST['language_accepted'] ) ? wp_unslash( $_POST['language_accepted'] ) : '' ),
				'referrer'          => isset( $_SERVER['HTTP_REFERER'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '',
				'timezone'          => isset( $_POST['timezone'] ) ? sanitize_text_field( wp_unslash( $_POST['timezone'] ) ) : '',
				'platform'          => isset( $_POST['platform'] ) ? sanitize_text_field( wp_unslash( $_POST['platform'] ) ) : '',
				'created_at' => current_time( 'mysql', true ),
			),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
		);
	} else {
		$conversation_id = $conversation->id;

		if ( 1 === (int) $conversation->is_cleared ) {
			$wpdb->update(
				"{$wpdb->prefix}mindweco_conversations",
				array( 'is_cleared' => 0 ),
				array( 'id' => $conversation_id ),
				array( '%d' ),
				array( '%s' )
			);
		}
	}

	$options   = get_option( 'mindweco_plugin_options', array() );
	$base_url  = isset( $options['base_url'] ) ? $options['base_url'] : '';
	$token = mindweco_decrypt_api_key( get_option( 'mindweco_plugin_access_token', '' ) );
	$category  = get_option( 'mindweco_plugin_category', null );
	$app_guid  = get_option( 'mindweco_plugin_guid', '' );
	$top_k     = intval( get_option( 'mindweco_ask_setting', 20 ) );

	if ( ! $category || empty( $category['id'] ) || empty( $base_url ) || empty( $token ) ) {
		wp_send_json_error( array( 'message' => 'MindWebConcierge configuration missing' ) );
		return;
	}

	$question              = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
	$post_id               = absint( isset( $_POST['post_id'] ) ? $_POST['post_id'] : 0 );
	$ask_only_current_page = ! empty( $_POST['ask_only_current_page'] );

	$table_items = $wpdb->prefix . 'mindweco_indexed_items';
	$category_id = intval( $category['id'] );

	$item_id = $wpdb->get_var(
		$wpdb->prepare(
			"SELECT item_id FROM {$table_items} WHERE post_id = %d LIMIT 1",
			$post_id
		)
	);
	$item_id = intval( $item_id );

	$notebooks     = mindweco_get_notebooks_for_page( $post_id );
	$has_notebooks = ! empty( $notebooks );

	if ( $has_notebooks && $ask_only_current_page ) {
		$items_ids    = array( $item_id );
		$category_ids = $notebooks;
	} elseif ( $has_notebooks && ! $ask_only_current_page ) {
		$items_ids    = array();
		$category_ids = array_merge( array( $category_id ), $notebooks );
	} elseif ( ! $has_notebooks && $ask_only_current_page ) {
		$items_ids    = array( $item_id );
		$category_ids = array();
	} else {
		$items_ids    = array();
		$category_ids = array( $category_id );
	}

	$question_page_based = ! empty( $category_ids ) ? wp_json_encode( $category_ids ) : '[]';

	$wpdb->insert(
		"{$wpdb->prefix}mindweco_questions",
		array(
			'conversation_id'     => $conversation_id,
			'question_text'       => $question,
			'page_id'             => $post_id,
			'question_page_based' => $question_page_based,
			'created_at'          => current_time( 'mysql', true ),
		),
		array( '%s', '%s', '%d', '%s', '%s' )
	);

	$question_id = $wpdb->insert_id;

	while ( ob_get_level() ) {
		ob_end_clean();
	}

	if ( function_exists( 'set_time_limit' ) ) {
		set_time_limit( 0 );
	}

	if ( function_exists( 'apache_setenv' ) ) {
		apache_setenv( 'no-gzip', '1' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv
	}

	if ( function_exists( 'ini_set' ) ) {
		ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.Risky
		ini_set( 'output_buffering', '0' );           // phpcs:ignore WordPress.PHP.IniSet.Risky
		ini_set( 'implicit_flush', '1' );             // phpcs:ignore WordPress.PHP.IniSet.Risky
	}

	header( 'Content-Type: text/plain; charset=utf-8' );
	header( 'Cache-Control: no-cache, no-store, must-revalidate' );
	header( 'Pragma: no-cache' );
	header( 'Expires: 0' );
	header( 'Connection: keep-alive' );
	header( 'X-Accel-Buffering: no' );

	flush();

	$full_response = '';
	$json_started  = false;
	$json_buffer   = '';

	$json_markers = array(
		'{"usage"',
		'{ "usage"',
		'{"type": "references"',
		'{"type":"references"',
		'{ "type": "references"',
		'{"answer"',
		'{ "answer"',
		'{"answer_status"',
		'{ "answer_status"',
		'{"sources"',
		'{ "sources"',
		'{"followups"',
		'{ "followups"',
		'{"follow_up',
		'{ "follow_up',
	);

	$stream_callback = function ( $ch, $data ) use ( &$full_response, &$json_started, &$json_buffer, $json_markers ) {

		if ( $json_started ) {
			$json_buffer .= $data;
			return strlen( $data );
		}

		$found_marker  = false;
		$marker_pos    = false;
		$found_marker_str = '';

		foreach ( $json_markers as $marker ) {
			$pos = strpos( $data, $marker );
			if ( false !== $pos ) {
				if ( false === $marker_pos || $pos < $marker_pos ) {
					$marker_pos       = $pos;
					$found_marker_str = $marker;
					$found_marker     = true;
				}
			}
		}

		if ( $found_marker ) {
			$json_started = true;

			$text_only = substr( $data, 0, $marker_pos );
			if ( '' !== $text_only ) {
				// This is a raw streaming endpoint that outputs plain text chunks directly
				// to the browser using text/plain Content-Type. Standard WordPress escaping
				// functions (esc_html, esc_attr, etc.) cannot be applied here because:
				// 1. The output is streamed incrementally, not rendered as HTML.
				// 2. Escaping would corrupt the content (e.g., converting & to &amp;).
				// 3. The nonce has been verified above (wp_verify_nonce).
				// 4. The output is sanitized and escaped by the JavaScript layer on render.
				echo $text_only; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw streaming plain-text chunk; nonce-verified, JS-escaped on render.
				if ( ob_get_level() ) {
					ob_flush();
				}
				flush();
				$full_response .= $text_only;
			}

			$json_buffer = substr( $data, $marker_pos );
			return strlen( $data );
		}

		// Same as above — raw streamed chunk from external API response.
		// Output is plain text, not HTML. Escaping is intentionally deferred
		// to the JavaScript rendering layer (parseMarkdown + DOMPurify or equivalent).
		echo $data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw streaming plain-text chunk; nonce-verified, JS-escaped on render.
		if ( ob_get_level() ) {
			ob_flush();
		}
		flush();
		$full_response .= $data;

		return strlen( $data );
	};

	$response = mindweco_ask_page_based_ref(
		$base_url,
		$token,
		$question,
		$category_id,
		$category_ids,
		$top_k,
		$app_guid,
		$stream_callback,
		$items_ids,
		$conversation_id
	);
	if ( ! $response['success'] && 401 === $response['http_code'] ) {
		$new_token = mindweco_plugin_refresh_token();
		if ( $new_token ) {
			$full_response = '';
			$json_started  = false;
			$json_buffer   = '';
			$response      = mindweco_ask_page_based_ref(
				$base_url,
				$new_token,
				$question,
				$category_id,
				$category_ids,
				$top_k,
				$app_guid,
				$stream_callback,
				$items_ids,
				$conversation_id
			);
		}
	}

	if ( ! $response['success'] && 500 === $response['http_code'] ) {
		wp_send_json_success( array( 'silent' => true ) );
	}


	$answer_status = 0;
	$answer_status_source = 'default(0)';
	$metadata_json  = mindweco_extract_json_object_from_stream_text( $json_buffer );
	$metadata_jsons = mindweco_extract_json_objects_from_stream_text( $json_buffer );

	if ( empty( $metadata_json ) ) {
		foreach ( $json_markers as $marker ) {
			$metadata_pos = strpos( $full_response, $marker );
			if ( false !== $metadata_pos ) {
				$metadata_json = mindweco_extract_json_object_from_stream_text( substr( $full_response, $metadata_pos ) );
				$metadata_jsons = mindweco_extract_json_objects_from_stream_text( substr( $full_response, $metadata_pos ) );
				break;
			}
		}
	}

	if ( ! empty( $metadata_json ) ) {
		if ( empty( $metadata_jsons ) ) {
			$metadata_jsons = array( $metadata_json );
		}

		$decoded = array();
		foreach ( $metadata_jsons as $metadata_part ) {
			$part_decoded = json_decode( $metadata_part, true );

			if ( json_last_error() !== JSON_ERROR_NONE ) {
				continue;
			}

			if ( is_array( $part_decoded ) ) {
				$decoded = array_merge( $decoded, $part_decoded );
			}
		}

		$answer_status = isset( $decoded['answer_status'] ) ? intval( $decoded['answer_status'] ) : 0;
		if ( isset( $decoded['answer_status'] ) ) {
			$answer_status_source = 'answer_complete.answer_status';
		}
		$sources       = isset( $decoded['sources'] ) ? $decoded['sources'] : null;

		if ( isset( $decoded['answer'] ) && is_string( $decoded['answer'] ) && '' === trim( $full_response ) ) {
			$full_response = $decoded['answer'];
			echo $decoded['answer']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Raw streaming plain-text chunk; nonce-verified, JS-escaped on render.
			if ( ob_get_level() ) {
				ob_flush();
			}
			flush();
		}

		$followups     = array();
		foreach ( array( 'followups', 'follow_ups', 'followup_questions', 'follow_up_questions', 'suggested_questions' ) as $followups_key ) {
			if ( isset( $decoded[ $followups_key ] ) && is_array( $decoded[ $followups_key ] ) ) {
				$followups = $decoded[ $followups_key ];
				break;
			}
		}

		$followups = array_filter(
			array_map(
				static function ( $followup ) {
					if ( is_string( $followup ) ) {
						return sanitize_text_field( $followup );
					}

					if ( is_array( $followup ) ) {
						foreach ( array( 'question', 'text', 'title', 'label' ) as $key ) {
							if ( isset( $followup[ $key ] ) && is_string( $followup[ $key ] ) ) {
								return sanitize_text_field( $followup[ $key ] );
							}
						}
					}

					return '';
				},
				$followups
			)
		);
		if ( $sources && is_array( $sources ) ) {
			$sources_map = array();
			foreach ( $sources as $source ) {
				if ( isset( $source['source_id'], $source['url'] ) ) {
					$sources_map[ $source['source_id'] ] = array(
						'url'              => ( is_string( $source['url'] ) && 'string' !== $source['url'] ) ? esc_url_raw( $source['url'] ) : null,
						'title'            => isset( $source['title'] ) ? $source['title'] : null,
						'indexing_item_id' => isset( $source['indexing_item_id'] ) ? $source['indexing_item_id'] : ( isset( $source['item_id'] ) ? $source['item_id'] : null ),
					);
				}
			}
			echo "\n[SOURCES]" . wp_json_encode( $sources_map ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-encoded array; plain-text stream output, not HTML context.
			if ( ob_get_level() ) {
				ob_flush();
			}
			flush();
		}

		if ( ! empty( $followups ) ) {
			echo "\n[FOLLOWUPS]" . wp_json_encode( array_values( $followups ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-encoded array; plain-text stream output, not HTML context.
			if ( ob_get_level() ) {
				ob_flush();
			}
			flush();
		}
	}

	if ( ! empty( $full_response ) || ! empty( $json_buffer ) ) {
		$clean_response = $full_response;

		$clean_metadata_pos = false;
		foreach ( $json_markers as $marker ) {
			$marker_pos = strpos( $clean_response, $marker );
			if ( false !== $marker_pos && ( false === $clean_metadata_pos || $marker_pos < $clean_metadata_pos ) ) {
				$clean_metadata_pos = $marker_pos;
			}
		}

		if ( false !== $clean_metadata_pos ) {
			$clean_response = substr( $clean_response, 0, $clean_metadata_pos );
		}
		
		$clean_response = trim( $clean_response );

		if ( function_exists( 'mindweco_ensure_responses_answer_status_column' ) ) {
			mindweco_ensure_responses_answer_status_column();
		}

		$wpdb->insert(
			"{$wpdb->prefix}mindweco_responses",
			array(
				'question_id'   => $question_id,
				'response_text' => $clean_response,
				'answer_status' => $answer_status,
				'created_at'    => current_time('mysql', true),			
			),
			array( '%d', '%s', '%d', '%s' )
		);
	}

	wp_die();
}


/**
 * Returns the client IPv4 address.
 *
 * @return string IPv4 address or empty string.
 */
function mindweco_get_client_ipv4() {
	$keys = array( 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR' );

	foreach ( $keys as $key ) {
		if ( empty( $_SERVER[ $key ] ) ) {
			continue;
		}

		$raw_ips = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
		$ips     = explode( ',', $raw_ips );

		foreach ( $ips as $ip ) {
			$ip = trim( $ip );

			if ( '::1' === $ip ) {
				return '127.0.0.1';
			}

			if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
				return $ip;
			}
		}
	}

	return '';
}


/**
 * Returns the notebook IDs configured for a given post/page.
 *
 * @param int $post_id The WordPress post ID.
 * @return array Array of notebook IDs, or empty array if none found.
 */
function mindweco_get_notebooks_for_page( $post_id ) {
	$page_based_notebooks = get_option( 'mindweco_page_based_notebooks', array() );

	if ( empty( $page_based_notebooks ) || ! is_array( $page_based_notebooks ) ) {
		return array();
	}

	$page_title = get_the_title( $post_id );

	if ( empty( $page_title ) ) {
		return array();
	}

	foreach ( $page_based_notebooks as $item ) {
		if ( ! isset( $item['title'] ) || ! isset( $item['notebooks'] ) ) {
			continue;
		}

		if ( 0 === strcasecmp( $item['title'], $page_title ) ) {
			$notebooks = $item['notebooks'];

			if ( ! is_array( $notebooks ) ) {
				return array();
			}

			return $notebooks;
		}
	}

	return array();
}
