<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Data_Objects\Chatbot_User;
use Limb_Chatbot\Includes\Data_Objects\Dataset;
use Limb_Chatbot\Includes\Data_Objects\File;
use Limb_Chatbot\Includes\Data_Objects\Lead_Field;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Limb_Chatbot;
use ReflectionClass;

/**
 * Class Helper
 *
 * Provides utility functions for string manipulation, file processing,
 * debugging, error formatting, and reflection-based operations.
 *
 * @package Limb_Chatbot\Includes\Services
 * @since 1.0.0
 */
class Helper {

	/**
	 * Converts an underscore string to CamelCase.
	 *
	 * @param  string  $string  The input string.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function underscore_to_camelcase( $string ) {
		$words = explode( '_', $string );

		return ucfirst( implode( '', array_map( 'ucfirst', $words ) ) );
	}

	public static function underscore_to_upper( $string ) {
		$words = explode( '_', $string );

		return strtoupper( implode( '_', array_map( 'ucfirst', $words ) ) );
	}

	/**
	 * Converts a camelCase string to underscore_case.
	 *
	 * @param  string  $string  The input string.
	 * @param  string  $separator  The separator character.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function camelcase_to_underscore( $string, $separator = '_' ) {
		// Use a regular expression to replace camel case with underscore
		$output = preg_replace_callback( '/([a-z])([A-Z])/', function ( $matches ) use ( $separator ) {
			return $matches[1] . $separator . strtolower( $matches[2] );
		}, $string );

		return strtolower( $output );
	}

	public static function text_to_underscore( $string ) {
		$array = explode( ' ', trim( $string ) );
		$array = array_map( function ( $item ) {
			return strtolower( $item );
		}, $array );

		return implode( '_', $array );
	}

	/**
	 * Converts underscore to hyphen.
	 *
	 * @param  string  $string  Input string.
	 * @param  bool  $lowercase  Whether to convert to lowercase.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function underscore_to_hyphen( $string, $lowercase = false ) {
		$string = str_replace( '_', '-', $string );

		return $lowercase ? strtolower( $string ) : $string;
	}

	/**
	 * Converts hyphen to underscore.
	 *
	 * @param  string  $string  Input string.
	 * @param  bool  $lowercase  Whether to convert to lowercase.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function hyphen_to_underscore( $string, $lowercase = false ) {
		$string = str_replace( '-', '_', $string );

		return $lowercase ? strtolower( $string ) : $string;
	}

	/**
	 * Converts a custom Exception or fallback into WP_Error.
	 *
	 * @param  mixed  $e  Exception or other error data.
	 *
	 * @return \WP_Error
	 * @since 1.0.0
	 *
	 */
	public static function get_wp_error( $e, $message = '' ) {
		if ( $e instanceof Exception ) {
			return new \WP_Error(
				$e->get_error_code(),
				self::linkify_urls_in_text( $e->getMessage() ),
				[
					'status'  => $e->get_http_status(),
					'details' => self::linkify_urls_in_value( $e->get_error_data() ),
				]
			);
		}
		$message = ! empty( $message ) ? $message : __( 'Technical error', 'limb-chatbot' );
		$message = self::linkify_urls_in_text( $message );

		return new \WP_Error( 'technical_error', $message, [ 'status' => 400 ] );
	}

	/**
	 * Convert plain URLs in a string to safe anchor tags.
	 *
	 * @param  string  $text
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public static function linkify_urls_in_text( string $text ): string {
		if ( '' === trim( $text ) ) {
			return $text;
		}

		$url_pattern = '~(?<!["\'>])(https?://[^\s<>"\']+)~i';

		return (string) preg_replace_callback(
			$url_pattern,
			static function ( array $matches ): string {
				$raw_match = $matches[1];
				$trailing  = '';

				// Move sentence punctuation outside the URL anchor.
				if ( preg_match( '/([.,!?;:]+)$/', $raw_match, $punctuation_match ) ) {
					$trailing  = $punctuation_match[1];
					$raw_match = substr( $raw_match, 0, - strlen( $trailing ) );
				}

				$url = esc_url_raw( $raw_match );
				if ( empty( $url ) ) {
					return $matches[1];
				}

				$label = esc_html( $url );

				return sprintf(
					'<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
					esc_url( $url ),
					$label
				) . $trailing;
			},
			$text
		);
	}

	/**
	 * Recursively linkify URLs in strings inside nested values.
	 *
	 * @param  mixed  $value
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public static function linkify_urls_in_value( $value ) {
		if ( is_string( $value ) ) {
			return self::linkify_urls_in_text( $value );
		}

		if ( is_array( $value ) ) {
			foreach ( $value as $key => $item ) {
				$value[ $key ] = self::linkify_urls_in_value( $item );
			}

			return $value;
		}

		if ( is_object( $value ) ) {
			$array_value = json_decode( wp_json_encode( $value, JSON_UNESCAPED_UNICODE ), true );
			if ( is_array( $array_value ) ) {
				return self::linkify_urls_in_value( $array_value );
			}
		}

		return $value;
	}

	/**
	 * Gets short class name.
	 *
	 * @param  string|object  $className  Class name or object.
	 * @param  bool  $lowercase  Whether to return in lowercase.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function get_class_shortname( $className, $lowercase = false ) {
		if ( is_object( $className ) ) {
			$reflection = new ReflectionClass( $className );
			$name       = $reflection->getShortName();
		} else {
			$className = is_object( $className ) ? get_class( $className ) : $className;
			$className = explode( '\\', $className );
			$name      = end( $className );
		}
		if ( $lowercase ) {
			$name = strtolower( $name );
		}

		return $name;
	}

	/**
	 * Gets the namespace from a fully qualified class name.
	 *
	 * @param  string|object  $class  The class.
	 *
	 * @return string|null
	 * @since 1.0.0
	 *
	 */
	public static function get_namespace_name( $class ): ?string {
		try {
			return ( new ReflectionClass( $class ) )->getNamespaceName();
		} catch ( \Exception $e ) {
			self::log( $e, __METHOD__ );

			return null;
		}
	}

	/**
	 * Logs plugin debug output if debug mode is enabled.
	 *
	 * @param  mixed  $data  The data to log.
	 * @param  string  $key  Optional label.
	 *
	 * @return void
	 * @since 1.0.0
	 *
	 */
	public static function log( $data, $key = 'log' ): void {
		if ( Limb_Chatbot()->get_debug() ) {
			$key = (string) $key;
			if ( $data instanceof Exception ) {
				$data_to_log = $data->get_error_data();
			} else {
				$data_to_log = $data;
			}
			if ( $data instanceof \Exception ) {
				$message = $data->getMessage() . ':  ';
			}
			error_log( Limb_Chatbot()->get_plugin_name() . '-debug: [' . $key . ']: ' . ( $message ?? '' ) . print_r( $data_to_log, true ) );
		}
	}

	/**
	 * Generates a UUID.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function get_uuid() {
		return wp_generate_uuid4();
	}

	/**
	 * Determine if the site is running in local or live mode.
	 *
	 * @since 1.0.9
	 * @return string 'local' or 'live'
	 */
	public static function get_site_mode() {
		$site_url = home_url();

		// Check for common local development indicators
		$local_indicators = array(
			'.local',
			'.localhost',
			'localhost',
			'127.0.0.1',
			'::1',
		);

		foreach ( $local_indicators as $indicator ) {
			if ( stripos( $site_url, $indicator ) !== false ) {
				return 'local';
			}
		}

		return 'live';
	}

	/**
	 * Generates a random string using WordPress password generator.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function get_random_string() {
		return wp_generate_password( 10, false );
	}

	/**
	 * Tries to decode a string as JSON, or return original value.
	 *
	 * @param  string  $string  Input string.
	 * @param  bool  $assoc  Whether to return associative array.
	 *
	 * @return mixed
	 * @since 1.0.0
	 *
	 */
	public static function maybe_json_decode( $string, $assoc = true ) {
		if ( ! is_string( $string ) ) {
			return $string;
		}
		$decoded = json_decode( $string, $assoc );
		if ( json_last_error() == JSON_ERROR_NONE ) {
			return $decoded;
		} else {
			return $string;
		}
	}

	/**
	 * Validates a URL with protocol and filter.
	 *
	 * @param  string  $url  URL to validate.
	 *
	 * @return bool
	 * @since 1.0.0
	 *
	 */
	public static function is_valid_url( $url ) {
		if ( ! str_starts_with( $url, 'http://' ) && ! str_starts_with( $url, 'https://' ) ) {
			return false;
		}
		if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Validates a base64 image string with file signature.
	 *
	 * @param  string  $base64  The base64-encoded image.
	 *
	 * @return bool
	 * @since 1.0.0
	 *
	 */
	public static function is_valid_base64_image( $base64 ) {
		// Check if the string starts with the correct data URL format
		if ( preg_match( '/^data:image\/(\w+);base64,/', $base64, $matches ) ) {
			// Extract the actual base64 data part
			$data = substr( $base64, strpos( $base64, ',' ) + 1 );
			// Check if the remaining string has valid base64 characters and optional padding
			if ( preg_match( '/^[A-Za-z0-9+\/]*={0,2}$/', $data ) ) {
				// Decode a portion to check for image signatures
				$decodedData = base64_decode( $data, true );
				if ( $decodedData === false ) {
					return false; // Decoding failed
				}
				// Check image type based on signature
				$imageType = strtolower( $matches[1] );
				switch ( $imageType ) {
					case 'png':
						return str_starts_with( $decodedData, "\x89PNG\r\n\x1a\n" );
					case 'jpeg':
						return str_starts_with( $decodedData, "\xFF\xD8\xFF" ); // JPEG signature
					case 'gif':
						return str_starts_with( $decodedData, "GIF89a" ) || str_starts_with( $decodedData, "GIF87a" ); // GIF signature
					// Add more image formats as needed
					default:
						return false; // Unsupported image type
				}
			}
		}

		return false; // Invalid format
	}

	/**
	 * Decodes a file response and returns as base64 image.
	 *
	 * @param  mixed  $response  The HTTP response.
	 *
	 * @return string|false
	 * @since 1.0.0
	 *
	 */
	public static function decode_file_from_rest_response( $response ) {
		$body      = wp_remote_retrieve_body( $response );
		$mime_type = wp_remote_retrieve_header( $response, 'content-type' );
		if ( ! empty( $mime_type ) && ! is_wp_error( $mime_type ) && $body ) {
			$base64_image = base64_encode( $body );

			return 'data:' . $mime_type . ';base64,' . $base64_image;
		}

		return false;
	}

	/**
	 * Checks if a string is probably JSON.
	 *
	 * @param  string  $string  Input string.
	 *
	 * @return bool
	 * @since 1.0.0
	 *
	 */
	public static function is_probable_json( $string ) {
		$string = trim( $string );
		if ( empty( $string ) ) {
			return false;
		}
		// Quick check: does it start with { or [ and end with } or ]?
		if ( ! ( ( ( $string[0] === '{' || $string[0] === '[' )
		           && ( $string[ strlen( $string ) - 1 ] === '}' || $string[ strlen( $string ) - 1 ] === ']' ) ) ) ) {
			return false;
		}
		$bracketCount = 0;
		$inQuotes     = false;
		$escaped      = false;
		for ( $i = 0; $i < strlen( $string ); $i ++ ) {
			$char = $string[ $i ];
			if ( $char === '"' && ! $escaped ) {
				// Toggle quotes, unless it's an escaped quote
				$inQuotes = ! $inQuotes;
			}
			// Manage escape sequences so we don't get tripped by escaped quotes
			$escaped = ( $char === '\\' ) && ! $escaped;
			// Only process brackets if not inside quotes
			if ( ! $inQuotes ) {
				if ( $char === '{' || $char === '[' ) {
					$bracketCount ++;
				} elseif ( $char === '}' || $char === ']' ) {
					$bracketCount --;
					// If at any point we have more closing brackets, it's invalid
					if ( $bracketCount < 0 ) {
						return false;
					}
				}
			}
		}

		// Finally, all brackets should be balanced (i.e., count should be zero)
		return $bracketCount === 0 && ! $inQuotes;
	}

	/**
	 * Downloads image by URL and returns base64-encoded image data URI.
	 *
	 * @param  string  $url  The image URL.
	 *
	 * @return string|null
	 * @since 1.0.0
	 *
	 */
	public static function convert_uri_to_base64( $url ) {
		if ( ! empty( $url ) && filter_var( $url, FILTER_VALIDATE_URL ) ) {
			$response  = wp_remote_get( $url );
			$body      = wp_remote_retrieve_body( $response );
			$mime_type = wp_remote_retrieve_header( $response, 'content-type' );
			if ( ! empty( $mime_type ) && ! is_wp_error( $mime_type ) && $body ) {
				$base64_image = base64_encode( $body );

				return 'data:' . $mime_type . ';base64,' . $base64_image;
			}
		}

		return null;
	}

	/**
	 * File move
	 *
	 * @param string $from From
	 * @param string $to To
	 *
	 * @return bool
	 */
	public static function file_move( $from, $to ) {
		try {
			global $wp_filesystem;

			if ( ! function_exists( 'WP_Filesystem' ) ) {
				// Instead of direct include, use WordPress's proper loading
				if ( ! function_exists( 'get_filesystem_method' ) ) {
					return false; // WordPress not fully loaded
				}
			}

			WP_Filesystem();

			if ( $wp_filesystem->exists( $from ) ) {
				return $wp_filesystem->move( $from, $to );
			}

			return false;
		} catch ( \Exception $e ) {
			return false;
		}
	}

	/**
	 * Extracts MIME type from base64 image string.
	 *
	 * @param  string  $base64_data  Base64 input.
	 *
	 * @return string|null
	 * @since 1.0.0
	 *
	 */
	public static function get_mime_type_from_base64( $base64_data ) {
		if ( preg_match( '/^data:(\w+\/\w+);base64,/', $base64_data, $type ) ) {
			return $type[1]; // MIME type from the base64 header
		}

		return null;
	}

	/**
	 * Checks if MIME type is an image type.
	 *
	 * @param  string  $mime_type  MIME type.
	 *
	 * @return bool
	 * @since 1.0.0
	 *
	 */
	public static function is_image_mime_type( string $mime_type ) {
		return in_array( $mime_type, [
			'image/jpeg',
			'image/png',
			'image/gif',
			'image/webp',
			'image/svg+xml',
			'image/bmp',
			'image/tiff',
			'image/x-icon'
		], true );
	}

	/**
	 * Gets the value of a protected/private property from an object.
	 *
	 * @param  object  $instance  The object instance.
	 * @param  string  $property_name  Property name.
	 *
	 * @return mixed|null
	 * @since 1.0.0
	 *
	 */
	public static function get_instance_property_value( $instance, string $property_name ) {
		try {
			$reflection = new ReflectionClass( $instance );
			$property   = $reflection->getProperty( $property_name );
			$property->setAccessible( true );

			return $property->getValue( $instance );
		} catch ( \Exception $e ) {
			return null;
		}
	}

	/**
	 * Casts value to given type name.
	 *
	 * @param  string  $type  Type name.
	 * @param  mixed  $value  Value to cast.
	 *
	 * @return mixed
	 * @since 1.0.0
	 *
	 */
	public static function cast_value( string $type, $value ) {
		switch ( $type ) {
			case 'int':
			case 'integer':
				return (int) $value;
			case 'float':
				return (float) $value;
			case 'string':
				return (string) $value;
			case 'bool':
			case 'boolean':
				return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
			case 'array':
				return (array) $value;
			case 'object':
				return (object) $value;
			case 'Limb_Chatbot\Includes\Services\Widget_Collection':
			case 'Limb_Chatbot\Includes\Services\Collection':
				return $value;
			default:
				return null;
			// throw new Exception("Unsupported type: {$type}");
		}
	}

	/**
	 * Loads plugin configuration from /configs/{file}.php and nested keys.
	 *
	 * @param  string  $config  Dot-style config path (e.g. `file/key1/key2`).
	 *
	 * @return mixed|null
	 * @since 1.0.0
	 *
	 */
	public static function config( string $config ) {
		$config = explode( '/', trim( $config ) );
		if ( empty( $config[0] ) ) {
			return null;
		}
		$file_path = rtrim( Limb_Chatbot::get_instance()->get_plugin_dir_path(), '/' ) . "/configs/{$config[0]}.php";
		if ( ! file_exists( $file_path ) ) {
			return null;
		}
		$res = include $file_path;
		foreach ( array_slice( $config, 1 ) as $key ) {
			if ( ! is_array( $res ) || ! array_key_exists( $key, $res ) ) {
				return null;
			}
			$res = $res[ $key ];
		}

		return $res;
	}

	/**
	 * Generates a random string with alphanumeric characters.
	 *
	 * @param  int  $length  String length.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function random_string( $length = 10 ) {
		return wp_generate_password( $length, false, false );
	}

	/**
	 * Gets normalized type name for a value.
	 *
	 * @param  mixed  $value  The value to evaluate.
	 *
	 * @return string
	 * @since 1.0.0
	 *
	 */
	public static function gettype( $value ): string {
		if ( is_int( $value ) ) {
			return 'int';
		}
		if ( is_bool( $value ) ) {
			return 'bool';
		}
		if ( is_float( $value ) ) {
			return 'float';
		}
		if ( is_string( $value ) ) {
			return 'string';
		}
		if ( is_array( $value ) ) {
			return 'array';
		}
		if ( is_object( $value ) ) {
			return 'object';
		}

		return gettype( $value );
	}

	/**
	 * Checks if array is associative.
	 *
	 * @param  array  $arr  Input array.
	 *
	 * @return bool
	 * @since 1.0.0
	 *
	 */
	public static function is_assoc( array $arr ): bool {
		return array_keys( $arr ) !== range( 0, count( $arr ) - 1 );
	}

	/**
	 * Gets the full upload path for a given relative file path.
	 *
	 * @param  string  $file_path  Relative file path.
	 *
	 * @return string|null
	 * @since 1.0.0
	 *
	 */
	public static function get_wp_uploaded_file_dir( $file_path ): ?string {
		return trailingslashit( wp_upload_dir()['basedir'] ) . $file_path ?? null;
	}

	/**
	 * @param $file
	 * @param  string  $to
	 *
	 * @return bool
	 */
	public static function wp_handle_limb_upload( $file, string $to ): bool {
		// Check if we're in a REST API context and load required functions
		if ( ! function_exists( 'wp_handle_upload' ) ) {
			if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
				// This is acceptable for REST API context as per WordPress guidelines
				require_once( ABSPATH . 'wp-admin/includes/file.php' );
			} else {
				return false;
			}
		}

		$upload = wp_handle_upload( $file, [ 'test_form' => false ] );
		if ( ! empty( $upload['file'] ) ) {
			return self::file_move( $upload['file'], $to );
		} else {
			return false;
		}
	}

	public static function resolve_source_object( $source_type, $source, ?Dataset $dataset = null ) {
		switch ( $source_type ) {
			case 'post':
			case 'cpt':
				return get_post( $source );
			case 'term':
				return get_term( $source );
			case 'text':
			case 'q_a':
			case 'url':
				return $source;
			case 'file':
				return File::find_by_uuid( $source );
			default:
				return null;
		}
	}



	/**
	 * Check if value matches expected type.
	 *
	 * @param  mixed   $value Value to check
	 * @param  string  $type  Expected type
	 *
	 * @return bool True if type matches
	 * @since 1.0.0
	 */
	public static function check_type( $value, string $type ): bool {
		switch ( $type ) {
			case 'string':
				return is_string( $value );
			case 'integer':
				return is_int( $value ) || ( is_string( $value ) && is_numeric( $value ) );
			case 'number':
			case 'float':
			case 'double':
			case 'numeric':
				return is_numeric( $value );
			case 'boolean':
				return is_bool( $value ) || in_array( $value, [ '0', '1', 0, 1 ], true );
			case 'array':
				return is_array( $value );
			case 'null':
				return null === $value;
			default:
				return true;
		}
	}

	public static function is_local() {
		if ( function_exists( 'wp_get_environment_type' ) ) {
			$env = wp_get_environment_type();

			if ( $env === 'local' || $env === 'development' ) {
				return true;
			}
		}

		// 2) Environment variables (used by Bedrock, Docker, CI setups …)
		$envVars = [
			getenv( 'WP_ENV' ),
			getenv( 'APP_ENV' ),
			getenv( 'ENVIRONMENT' ),
			getenv( 'WP_ENVIRONMENT_TYPE' ),
		];

		foreach ( $envVars as $var ) {
			if ( in_array( $var, [ 'local', 'development', 'dev' ], true ) ) {
				return true;
			}
		}

		// 3) Hostname/IP based heuristics (fallback only)
		$host   = $_SERVER['HTTP_HOST'] ?? '';
		$remote = $_SERVER['REMOTE_ADDR'] ?? '';

		$local_ips  = [ '127.0.0.1', '::1' ];
		$local_tlds = [ '.test', '.local', '.localhost', '.loc' ];

		if ( in_array( $remote, $local_ips, true ) ) {
			return true;
		}

		foreach ( $local_tlds as $tld ) {
			if ( str_ends_with( $host, $tld ) ) {
				return true;
			}
		}

		return false;
	}

	public static function generate_unique_id(int $length = 10): string
	{
		// Clamp length between 1 and 10
		$length = max(1, min(10, $length));

		$characters = '0123456789abcdefghijklmnopqrstuvwxyz';
		$maxIndex   = strlen($characters) - 1;
		$result     = '';

		for ($i = 0; $i < $length; $i++) {
			$result .= $characters[random_int(0, $maxIndex)];
		}

		return $result;
	}

	public static function get_supported_native_fields( string $post_type ): array {
		$supported = [ 'post_date' ];

		if ( post_type_supports( $post_type, 'title' ) ) {
			$supported[] = 'post_title';
		}

		if ( post_type_supports( $post_type, 'editor' ) ) {
			$supported[] = 'post_content';
		}

		if ( post_type_supports( $post_type, 'excerpt' ) ) {
			$supported[] = 'post_excerpt';
		}

		if ( post_type_supports( $post_type, 'author' ) ) {
			$supported[] = 'author_name';
		}

		$post_type_object = get_post_type_object( $post_type );
		if ( $post_type_object && ( $post_type_object->public || $post_type_object->publicly_queryable ) ) {
			$supported[] = 'permalink';
		}

		if ( post_type_supports( $post_type, 'thumbnail' ) ) {
			$supported[] = 'featured_image';
		}

		return array_values( array_unique( $supported ) );
	}

	public static function validate_value_type( $value, string $data_type ) {
		switch ( $data_type ) {
			case Lead_Field::DATA_TYPE_EMAIL:
				$sanitized = sanitize_email( $value );

				return ! empty( $sanitized ) ? $sanitized : null;
			case Lead_Field::DATA_TYPE_NUMBER:
				if ( ! is_numeric( $value ) ) {
					return null;
				}

				return (float) $value;
			case Lead_Field::DATA_TYPE_PHONE:
				$sanitized = sanitize_text_field( $value );
				// Basic phone validation: at least 7 digits
				$digits = preg_replace( '/\D/', '', $sanitized );

				return strlen( $digits ) >= 7 ? $sanitized : null;
			case Lead_Field::DATA_TYPE_DATE:
				// Validate date format (YYYY-MM-DD or any MySQL datetime format)
				$timestamp = strtotime( $value );

				return $timestamp ? gmdate( 'Y-m-d H:i:s', $timestamp ) : null;
			case Lead_Field::DATA_TYPE_ATTACHMENT:
				if ( is_array( $value ) ) {
					$normalized = array_values( array_filter( array_map( static function ( $item ) {
						if ( ! is_array( $item ) ) {
							return null;
						}

						return [
							'title'    => isset( $item['title'] ) ? sanitize_text_field( (string) $item['title'] ) : '',
							'mime_type'=> isset( $item['mime_type'] ) ? sanitize_mime_type( (string) $item['mime_type'] ) : '',
							'value'    => isset( $item['value'] ) ? esc_url_raw( (string) $item['value'] ) : '',
							'media_id' => isset( $item['media_id'] ) ? absint( $item['media_id'] ) : 0,
						];
					}, $value ) ) );

					return ! empty( $normalized ) ? wp_json_encode( $normalized ) : null;
				}
				if ( is_string( $value ) ) {
					$decoded = json_decode( $value, true );
					if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
						return self::validate_value_type( $decoded, Lead_Field::DATA_TYPE_ATTACHMENT );
					}
				}

				return null;
			case Lead_Field::DATA_TYPE_TEXT:
			default:
				return sanitize_text_field( $value );
		}
	}

	/**
	 * Sanitizes an SVG string, allowing only safe elements and attributes.
	 *
	 * @param  string  $svg  Raw SVG markup.
	 *
	 * @return string
	 */
	public static function sanitize_svg( string $svg ): string {
		return wp_kses( $svg, [
			'svg'            => [ 'xmlns' => true, 'viewbox' => true, 'viewBox' => true, 'width' => true, 'height' => true, 'fill' => true, 'stroke' => true, 'class' => true, 'style' => true, 'aria-hidden' => true, 'role' => true ],
			'path'           => [ 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'fill-rule' => true, 'clip-rule' => true, 'class' => true, 'style' => true ],
			'circle'         => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true, 'style' => true ],
			'rect'           => [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true, 'style' => true ],
			'line'           => [ 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'class' => true, 'style' => true ],
			'polyline'       => [ 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'class' => true, 'style' => true ],
			'polygon'        => [ 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true, 'style' => true ],
			'g'              => [ 'fill' => true, 'stroke' => true, 'transform' => true, 'class' => true, 'style' => true, 'opacity' => true ],
			'defs'           => [],
			'title'          => [],
			'desc'           => [],
			'use'            => [ 'href' => true, 'xlink:href' => true, 'x' => true, 'y' => true, 'width' => true, 'height' => true ],
			'symbol'         => [ 'id' => true, 'viewbox' => true, 'viewBox' => true, 'width' => true, 'height' => true ],
			'clippath'       => [ 'id' => true ],
			'mask'           => [ 'id' => true ],
			'stop'           => [ 'offset' => true, 'stop-color' => true, 'stop-opacity' => true, 'style' => true ],
			'lineargradient' => [ 'id' => true, 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'gradientunits' => true, 'gradientUnits' => true ],
			'radialgradient' => [ 'id' => true, 'cx' => true, 'cy' => true, 'r' => true, 'fx' => true, 'fy' => true, 'gradientunits' => true, 'gradientUnits' => true ],
		] );
	}

	public static function make_custom_user_from_raw_array( $user_id, array $row ) {
		return Chatbot_User::make(
			[
				'id'          => $user_id,
				'uuid'        => $row['participant_user_uuid'] ?? '',
				'wp_user_id'  => isset( $row['participant_user_wp_user_id'] ) ? (int) $row['participant_user_wp_user_id'] : null,
				'device_uuid' => $row['participant_user_device_uuid'] ?? null,
				'ip'          => $row['participant_user_ip'] ?? null,
				'type'        => isset( $row['participant_user_type'] ) ? (int) $row['participant_user_type'] : null,
				'status'      => isset( $row['participant_user_status'] ) ? (int) $row['participant_user_status'] : null,
				'created_at'  => $row['participant_user_created_at'] ?? null,
				'updated_at'  => $row['participant_user_updated_at'] ?? null,
			]
		);
	}
}
