<?php
/**
 * Custom Fields — format stored values for display and integrations.
 *
 * @package CustomFields
 * @since 1.4.0
 */

namespace CustomFields;

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

/**
 * Resolves and formats field values for templates, shortcodes, and builders.
 */
final class FieldOutput {

	private const MAX_MULTI_VALUE_IDS = 500;

	/**
	 * Field types that must never be rendered via shortcodes/tags/builders.
	 *
	 * @return array<int, string>
	 */
	public static function sensitiveTypes(): array {
		return [ 'password' ];
	}

	/**
	 * Field types excluded from builder/tag pickers.
	 *
	 * @return array<int, string>
	 */
	public static function integratableTypes(): array {
		return array_values(
			array_diff(
				array_keys( Config::fieldTypes() ),
				array_merge( Config::displayOnlyTypes(), Config::nestedTypes(), self::sensitiveTypes() )
			)
		);
	}

	/**
	 * Whether the current viewer may read custom-field values for a post.
	 */
	public static function viewerCanReadPost( int $post_id ): bool {
		if ( $post_id <= 0 ) {
			return false;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post ) {
			return false;
		}

		if ( ! current_user_can( 'read_post', $post_id ) ) {
			return false;
		}

		// Password-protected posts: require unlock cookie, or edit access.
		if ( post_password_required( $post ) && ! current_user_can( 'edit_post', $post_id ) ) {
			return false;
		}

		return true;
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 */
	public static function fieldIsPubliclyRenderable( array $field ): bool {
		$type = (string) ( $field['type'] ?? 'text' );

		return in_array( $type, self::integratableTypes(), true );
	}

	/**
	 * All integratable fields from enabled groups.
	 *
	 * @return array<int, array<string, string>>
	 */
	public static function integratableFields(): array {
		$storage = new Storage();
		$fields  = [];

		foreach ( $storage->all() as $group_key => $group ) {
			if ( ! is_array( $group ) || empty( $group['enabled'] ) ) {
				continue;
			}

			$group_key   = (string) $group_key;
			$group_title = (string) ( $group['title'] ?? $group_key );
			$list        = isset( $group['fields'] ) && is_array( $group['fields'] ) ? $group['fields'] : [];

			foreach ( $list as $field ) {
				if ( ! is_array( $field ) ) {
					continue;
				}

				$type = (string) ( $field['type'] ?? 'text' );
				if ( ! in_array( $type, self::integratableTypes(), true ) ) {
					continue;
				}

				$name = (string) ( $field['name'] ?? '' );
				if ( $name === '' ) {
					continue;
				}

				$meta_key = MetaKeys::resolve( $group_key, $field );
				if ( $meta_key === '' ) {
					continue;
				}

				$fields[] = [
					'group_key'   => $group_key,
					'group_label' => $group_title,
					'field_name'  => $name,
					'field_label' => (string) ( $field['label'] ?? $name ),
					'meta_key'    => $meta_key,
					'type'        => $type,
					'selection'   => $group_key . ':' . $name,
				];
			}
		}

		return $fields;
	}

	/**
	 * Locate a field definition by group key and short field name.
	 *
	 * @return array{group: array<string, mixed>, field: array<string, mixed>}|null
	 */
	public static function locateField( string $group_key, string $field_name ): ?array {
		$storage = new Storage();
		$group   = $storage->get( $group_key );
		if ( ! is_array( $group ) ) {
			return null;
		}

		foreach ( (array) ( $group['fields'] ?? [] ) as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			if ( (string) ( $field['name'] ?? '' ) === $field_name || (string) ( $field['key'] ?? '' ) === $field_name ) {
				return [
					'group' => $group,
					'field' => $field,
				];
			}
		}

		return null;
	}

	/**
	 * Get a human-friendly display value for a post field.
	 *
	 * @param int|null $post_id Post ID (defaults to current post).
	 */
	public static function getDisplay( string $group_key, string $field_name, ?int $post_id = null, mixed $default = '' ): mixed {
		$post_id = $post_id ?? (int) get_the_ID();
		if ( $post_id <= 0 || ! self::viewerCanReadPost( $post_id ) ) {
			return $default;
		}

		$located = self::locateField( $group_key, $field_name );
		if ( $located === null ) {
			return $default;
		}

		$field = $located['field'];
		if ( ! self::fieldIsPubliclyRenderable( $field ) ) {
			return $default;
		}

		$meta = MetaKeys::resolve( $group_key, $field );
		if ( $meta === '' ) {
			return $default;
		}

		$value = get_post_meta( $post_id, $meta, true );
		if ( $value === '' || $value === null || $value === [] ) {
			return $default;
		}

		return self::formatValue( $field, $value, 'display' );
	}

	/**
	 * Render a field value for shortcodes, blocks, and page builders.
	 *
	 * @param array<string, mixed> $args format: display|raw|html|url|text, default
	 */
	public static function render( string $group_key, string $field_name, ?int $post_id = null, array $args = [] ): string {
		$post_id = $post_id ?? (int) get_the_ID();
		if ( $post_id <= 0 || ! self::viewerCanReadPost( $post_id ) ) {
			return '';
		}

		$located = self::locateField( $group_key, $field_name );
		if ( $located === null ) {
			return '';
		}

		$format  = sanitize_key( (string) ( $args['format'] ?? 'display' ) );
		$default = $args['default'] ?? '';
		$field   = $located['field'];
		if ( ! self::fieldIsPubliclyRenderable( $field ) ) {
			return '';
		}

		$meta = MetaKeys::resolve( $group_key, $field );
		if ( $meta === '' ) {
			return '';
		}

		$raw = get_post_meta( $post_id, $meta, true );
		if ( $raw === '' || $raw === null || $raw === [] ) {
			if ( $default === '' || $default === null ) {
				return '';
			}
			$raw = $default;
		}

		if ( $format === 'raw' ) {
			if ( is_array( $raw ) ) {
				return esc_html( wp_json_encode( $raw ) );
			}

			return esc_html( (string) $raw );
		}

		$type  = (string) ( $field['type'] ?? 'text' );
		$value = self::formatValue( $field, $raw, $format === 'display' ? 'display' : $format );

		if ( $format === 'display' ) {
			if ( in_array( $type, [ 'wysiwyg', 'oembed' ], true ) ) {
				$format = 'html';
			} elseif ( in_array( $type, [ 'image', 'file', 'gallery' ], true ) ) {
				$format = 'url';
			} elseif ( $type === 'link' ) {
				$link = is_array( $value ) ? $value : [];
				$href = (string) ( $link['url'] ?? '' );
				$text = (string) ( $link['title'] ?? $href );
				if ( $href === '' ) {
					return '';
				}
				$target = (string) ( $link['target'] ?? '' ) === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';

				return '<a href="' . esc_url( $href ) . '"' . $target . '>' . esc_html( $text !== '' ? $text : $href ) . '</a>';
			} elseif ( $type === 'textarea' && in_array( sanitize_key( (string) ( $field['new_lines'] ?? '' ) ), [ 'br', 'wpautop' ], true ) ) {
				$format = 'html';
			} elseif ( in_array( $type, [ 'page_link', 'google_map' ], true ) ) {
				$format = 'html';
			} elseif ( $type === 'icon_picker' ) {
				$format = 'html';
			} elseif ( in_array( $type, [ 'post_object', 'relationship', 'term', 'user' ], true ) ) {
				$format = 'text';
			} else {
				$format = 'text';
			}
		}

		$value = self::stringifyForRender( $value );

		switch ( $format ) {
			case 'html':
				if ( $type === 'oembed' ) {
					return wp_kses( (string) $value, self::oembedAllowedHtml() );
				}
				return wp_kses_post( (string) $value );
			case 'url':
				return esc_url( (string) $value );
			case 'text':
			default:
				return esc_html( (string) $value );
		}
	}

	/**
	 * Render by stored meta key (when group/field names are unknown).
	 *
	 * @param array<string, mixed> $args
	 */
	public static function renderByMetaKey( string $meta_key, ?int $post_id = null, array $args = [] ): string {
		$post_id = $post_id ?? (int) get_the_ID();
		if ( $post_id <= 0 || $meta_key === '' ) {
			return '';
		}

		// Only registered integratable CF fields — never arbitrary post meta.
		foreach ( self::integratableFields() as $field ) {
			if ( (string) ( $field['meta_key'] ?? '' ) === $meta_key ) {
				return self::render(
					(string) $field['group_key'],
					(string) $field['field_name'],
					$post_id,
					$args
				);
			}
		}

		return '';
	}

	/**
	 * @param array<string, mixed> $field  Field definition.
	 * @param mixed                $value  Stored value.
	 * @param string               $format display|url|text
	 * @return mixed
	 */
	public static function formatValue( array $field, mixed $value, string $format = 'display' ): mixed {
		$type = (string) ( $field['type'] ?? 'text' );
		$return_format = sanitize_key( (string) ( $field['return'] ?? '' ) );

		if ( $value === '' || $value === null || $value === [] ) {
			return $format === 'url' ? '' : $value;
		}

		switch ( $type ) {
			case 'password':
				return $format === 'url' ? '' : '';

			case 'textarea':
				if ( $format === 'url' ) {
					return '';
				}
				if ( $format === 'text' ) {
					return (string) $value;
				}
				$new_lines = sanitize_key( (string) ( $field['new_lines'] ?? '' ) );
				if ( $new_lines === 'wpautop' ) {
					return wpautop( (string) $value );
				}
				if ( $new_lines === 'br' ) {
					return nl2br( esc_html( (string) $value ) );
				}

				return $value;

			case 'date':
			case 'time':
			case 'datetime':
				return self::formatTemporalValue( $field, $value, $type, $format );

			case 'oembed':
				return self::formatOembed( $field, $value, $format );

			case 'toggle':
				$on_text  = (string) ( $field['toggle_on_text'] ?? '' );
				$off_text = (string) ( $field['toggle_off_text'] ?? '' );
				$on_text  = $on_text !== '' ? $on_text : __( 'Yes', 'wppowersuite-lite' );
				$off_text = $off_text !== '' ? $off_text : __( 'No', 'wppowersuite-lite' );
				return (string) $value === '1' ? $on_text : $off_text;

			case 'color':
				if ( $return_format === 'array' ) {
					return self::formatColorArray( (string) $value );
				}

				return (string) $value;

			case 'image':
			case 'file':
				$id = (int) $value;
				if ( $id <= 0 ) {
					return $format === 'url' ? '' : '—';
				}
				if ( $return_format === 'id' ) {
					return $id;
				}
				if ( $return_format === 'array' ) {
					return self::formatAttachmentArray( $id, $type );
				}
				if ( $format === 'url' || $return_format === 'url' ) {
					return $type === 'file' ? self::attachmentUrl( $id ) : wps_cf_get_image_url( $id );
				}
				$title = get_the_title( $id );

				return $title !== '' ? $title : (string) $id;

			case 'gallery':
				if ( ! is_array( $value ) ) {
					$value = array_filter( array_map( 'absint', explode( ',', (string) $value ) ) );
				}
				$value = self::limitIds( $value );
				if ( $format === 'url' ) {
					$first = (int) ( $value[0] ?? 0 );

					return $first > 0 ? wps_cf_get_image_url( $first ) : '';
				}
				if ( $return_format === 'id' ) {
					return array_values( array_map( 'absint', $value ) );
				}
				if ( $return_format === 'url' ) {
					return array_values(
						array_filter(
							array_map(
								static fn( mixed $id ): string => wps_cf_get_image_url( $id ),
								$value
							)
						)
					);
				}
				if ( $return_format === 'array' ) {
					return array_values(
						array_filter(
							array_map(
								static fn( mixed $id ): array => self::formatAttachmentArray( (int) $id ),
								$value
							)
						)
					);
				}

				return (string) count( $value );

			case 'link':
				if ( ! is_array( $value ) ) {
					return $format === 'url' ? '' : $value;
				}
				if ( $format === 'url' || $return_format === 'url' ) {
					return (string) ( $value['url'] ?? '' );
				}

				return $value;

			case 'post_object':
			case 'relationship':
				return $return_format === 'id' ? self::formatReadablePostIdList( $value ) : self::formatObjectList( $value, 'post', true );

			case 'page_link':
				return self::formatPageLink( $field, $value, $format );

			case 'google_map':
				return self::formatGoogleMap( $value, $format );

			case 'icon_picker':
				$icon   = is_string( $value ) ? trim( $value ) : '';
				$is_url = $icon !== '' && preg_match( '#^https?://#i', $icon ) === 1 && esc_url_raw( $icon ) !== '';
				if ( ! $is_url ) {
					$icon = sanitize_key( $icon );
				}
				if ( $icon === '' ) {
					return '';
				}
				if ( $return_format === 'array' ) {
					return [
						'type'  => $is_url ? 'url' : ( strpos( $icon, 'dashicons-' ) === 0 ? 'dashicon' : 'string' ),
						'value' => $icon,
					];
				}

				if ( $format !== 'display' ) {
					return $icon;
				}

				return $is_url
					? '<img class="wpps-cf-icon-output" src="' . esc_url( $icon ) . '" alt="" width="20" height="20">'
					: '<span class="dashicons ' . esc_attr( $icon ) . '" aria-hidden="true"></span><span class="screen-reader-text">' . esc_html( $icon ) . '</span>';

			case 'group':
				return is_array( $value ) ? wp_json_encode( $value ) : (string) $value;

			case 'term':
				return $return_format === 'object' ? self::formatObjectList( $value, 'term', true ) : self::formatIdList( $value );

			case 'user':
				if ( $return_format === 'id' ) {
					return self::formatIdList( $value );
				}
				return self::formatObjectList( $value, 'user', $return_format !== 'array' );

			case 'nav_menu':
				return self::formatNavMenu( $field, $value, $return_format );

			case 'radio':
			case 'checkbox':
			case 'select':
			case 'button_group':
				return self::formatChoiceValue( $field, $value, $return_format );

			default:
				return $value;
		}
	}

	private static function stringifyForRender( mixed $value ): string {
		if ( is_scalar( $value ) || $value === null ) {
			return (string) $value;
		}

		if ( is_array( $value ) ) {
			$parts = [];
			foreach ( $value as $item ) {
				$parts[] = self::stringifyForRender( $item );
			}
			return implode( ', ', array_filter( $parts, static fn( string $part ): bool => $part !== '' ) );
		}

		if ( $value instanceof \WP_Post ) {
			return get_the_title( $value ) ?: (string) $value->ID;
		}

		if ( $value instanceof \WP_Term ) {
			return $value->name;
		}

		if ( $value instanceof \WP_User ) {
			return $value->display_name;
		}

		return '';
	}

	private static function formatAttachmentArray( int $id, string $type = 'image' ): array {
		if ( $id <= 0 ) {
			return [];
		}

		$file     = (string) get_attached_file( $id );
		$url      = self::attachmentUrl( $id );
		$mime     = (string) get_post_mime_type( $id );
		$mime_parts = explode( '/', $mime, 2 );
		$metadata = wp_get_attachment_metadata( $id );
		$sizes    = [];
		if ( is_array( $metadata ) && isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
			foreach ( $metadata['sizes'] as $size => $size_data ) {
				if ( ! is_array( $size_data ) ) {
					continue;
				}
				$image = wp_get_attachment_image_src( $id, (string) $size );
				$sizes[ (string) $size ] = [
					'url'       => is_array( $image ) ? (string) $image[0] : '',
					'width'     => absint( $size_data['width'] ?? 0 ),
					'height'    => absint( $size_data['height'] ?? 0 ),
					'mime-type' => (string) ( $size_data['mime-type'] ?? '' ),
					'filesize'  => absint( $size_data['filesize'] ?? 0 ),
				];
			}
		}

		return [
			'ID'          => $id,
			'id'          => $id,
			'url'         => $url,
			'title'       => get_the_title( $id ),
			'filename'    => wp_basename( $file ),
			'filesize'    => $file !== '' && file_exists( $file ) ? (int) filesize( $file ) : 0,
			'mime_type'   => $mime,
			'type'        => (string) ( $mime_parts[0] ?? $type ),
			'subtype'     => (string) ( $mime_parts[1] ?? '' ),
			'alt'         => (string) get_post_meta( $id, '_wp_attachment_image_alt', true ),
			'caption'     => (string) wp_get_attachment_caption( $id ),
			'description' => (string) get_post_field( 'post_content', $id ),
			'width'       => is_array( $metadata ) ? absint( $metadata['width'] ?? 0 ) : 0,
			'height'      => is_array( $metadata ) ? absint( $metadata['height'] ?? 0 ) : 0,
			'sizes'       => $sizes,
		];
	}

	/**
	 * Format a stored nav menu term ID.
	 *
	 * @param array<string, mixed> $field         Field definition.
	 * @param mixed                $value         Stored menu term ID.
	 * @param string               $return_format id|object|menu.
	 */
	private static function formatNavMenu( array $field, mixed $value, string $return_format ): mixed {
		$menu_id = absint( $value );
		if ( $menu_id <= 0 || ! is_nav_menu( $menu_id ) ) {
			return '';
		}

		if ( $return_format === 'object' ) {
			$menu = wp_get_nav_menu_object( $menu_id );
			return $menu instanceof \WP_Term ? $menu : '';
		}

		if ( $return_format === 'menu' ) {
			$container = sanitize_key( (string) ( $field['nav_menu_container'] ?? 'div' ) );
			if ( ! in_array( $container, [ 'div', 'nav' ], true ) ) {
				$container = false;
			}
			return wp_nav_menu(
				[
					'menu'       => $menu_id,
					'container'  => $container,
					'echo'       => false,
					'fallback_cb' => false,
				]
			) ?: '';
		}

		return $menu_id;
	}

	private static function attachmentUrl( int $id ): string {
		if ( $id <= 0 ) {
			return '';
		}

		$url = wp_get_attachment_url( $id );

		return is_string( $url ) ? $url : '';
	}

	private static function formatIdList( mixed $value ): int|array {
		$ids = is_array( $value ) ? self::limitIds( $value ) : [ absint( $value ) ];
		if ( is_array( $value ) ) {
			return $ids;
		}

		return (int) ( $ids[0] ?? 0 );
	}

	private static function formatReadablePostIdList( mixed $value ): int|array {
		$ids = is_array( $value ) ? self::limitIds( $value ) : [ absint( $value ) ];
		$ids = array_values(
			array_filter(
				$ids,
				static fn( int $id ): bool => $id > 0 && current_user_can( 'read_post', $id )
			)
		);

		return is_array( $value ) ? $ids : (int) ( $ids[0] ?? 0 );
	}

	/**
	 * @param mixed  $value Stored ID or list of IDs.
	 * @param string $kind  post|term|user
	 */
	private static function formatObjectList( mixed $value, string $kind, bool $objects = false ): mixed {
		$ids = is_array( $value ) ? self::limitIds( $value ) : [ absint( $value ) ];
		if ( $ids === [] ) {
			return '';
		}

		$out = [];
		foreach ( $ids as $id ) {
			if ( $kind === 'post' ) {
				if ( ! current_user_can( 'read_post', $id ) ) {
					continue;
				}
				$post = get_post( $id );
				$out[] = $objects && $post instanceof \WP_Post ? $post : ( get_the_title( $id ) ?: (string) $id );
			} elseif ( $kind === 'term' ) {
				$term = get_term( $id );
				$out[] = $objects && $term instanceof \WP_Term && ! is_wp_error( $term ) ? $term : ( ( $term instanceof \WP_Term && ! is_wp_error( $term ) ) ? $term->name : (string) $id );
			} else {
				$user = get_userdata( $id );
				if ( $objects && $user instanceof \WP_User ) {
					$out[] = $user;
				} elseif ( $user instanceof \WP_User ) {
					$out[] = self::formatPublicUserArray( $user );
				} else {
					$out[] = (string) $id;
				}
			}
		}

		if ( ! is_array( $value ) ) {
			return $out[0] ?? '';
		}

		return $out;
	}

	/**
	 * @param array<int, mixed> $ids Raw IDs.
	 * @return array<int, int>
	 */
	private static function limitIds( array $ids ): array {
		return array_slice(
			array_values(
				array_unique(
					array_filter( array_map( 'absint', $ids ) )
				)
			),
			0,
			self::MAX_MULTI_VALUE_IDS
		);
	}

	private static function formatPublicUserArray( \WP_User $user ): array {
		return [
			'ID'           => (int) $user->ID,
			'id'           => (int) $user->ID,
			'display_name' => (string) $user->display_name,
		];
	}

	private static function formatChoiceValue( array $field, mixed $value, string $return_format ): mixed {
		$choices = Validator::parseChoices( (string) ( $field['choices'] ?? '' ) );
		$is_list = is_array( $value );
		$values  = $is_list ? array_values( array_map( 'strval', $value ) ) : [ (string) $value ];
		$out     = [];

		foreach ( $values as $item ) {
			$label = (string) ( $choices[ $item ] ?? $item );
			if ( $return_format === 'label' ) {
				$out[] = $label;
			} elseif ( $return_format === 'array' ) {
				$out[] = [
					'value' => $item,
					'label' => $label,
				];
			} else {
				$out[] = $item;
			}
		}

		return $is_list ? $out : ( $out[0] ?? '' );
	}

	private static function formatTemporalValue( array $field, mixed $value, string $type, string $format ): string {
		$text = trim( (string) $value );
		if ( $text === '' ) {
			return '';
		}

		$output_format = $format === 'display'
			? (string) ( $field['display_format'] ?? '' )
			: (string) ( $field['return'] ?? '' );

		if ( $type === 'date' ) {
			$date = self::parseDateValue( $field, $text );
			if ( $date instanceof \DateTimeImmutable ) {
				return $output_format !== '' ? $date->format( $output_format ) : $text;
			}
		} elseif ( $type === 'time' ) {
			$normalized = strlen( $text ) === 5 ? $text . ':00' : $text;
			$date       = self::parseExactDateTime( '!H:i:s', $normalized, 'H:i:s' );
			if ( $date instanceof \DateTimeImmutable ) {
				return $output_format !== '' ? $date->format( $output_format ) : $text;
			}
		} elseif ( $type === 'datetime' ) {
			$normalized = str_replace( 'T', ' ', $text );
			$normalized = strlen( $normalized ) === 16 ? $normalized . ':00' : $normalized;
			$date       = self::parseExactDateTime( 'Y-m-d H:i:s', $normalized, 'Y-m-d H:i:s' );
			if ( $date instanceof \DateTimeImmutable ) {
				return $output_format !== '' ? $date->format( $output_format ) : $text;
			}
		}

		return $text;
	}

	/**
	 * Format an oEmbed URL for public display.
	 *
	 * @param array<string, mixed> $field Field definition.
	 * @param mixed                $value Stored URL.
	 * @param string               $format display|html|url|text.
	 */
	private static function formatOembed( array $field, mixed $value, string $format ): string {
		$url = esc_url_raw( trim( (string) $value ) );
		if ( $url === '' || preg_match( '#^https?://#i', $url ) !== 1 ) {
			return '';
		}

		if ( $format === 'url' || $format === 'text' ) {
			return $url;
		}

		$args = [];
		$width = absint( $field['oembed_width'] ?? 0 );
		$height = absint( $field['oembed_height'] ?? 0 );
		if ( $width > 0 ) {
			$args['width'] = $width;
		}
		if ( $height > 0 ) {
			$args['height'] = $height;
		}

		$html = false;
		if ( function_exists( '_wp_oembed_get_object' ) && function_exists( 'wp_oembed_get' ) ) {
			$oembed  = _wp_oembed_get_object();
			$provider = $oembed instanceof \WP_oEmbed ? $oembed->get_provider( $url, [ 'discover' => false ] ) : false;
			if ( is_string( $provider ) && $provider !== '' ) {
				$html = wp_oembed_get( $url, $args );
			}
		}
		if ( is_string( $html ) && $html !== '' ) {
			return $html;
		}

		return '<a href="' . esc_url( $url ) . '">' . esc_html( $url ) . '</a>';
	}

	/**
	 * Allowed oEmbed markup for public rendering.
	 *
	 * @return array<string, array<string, bool>>
	 */
	private static function oembedAllowedHtml(): array {
		$allowed = wp_kses_allowed_html( 'post' );
		$allowed['iframe'] = [
			'src'             => true,
			'title'           => true,
			'width'           => true,
			'height'          => true,
			'frameborder'     => true,
			'allow'           => true,
			'allowfullscreen' => true,
			'loading'         => true,
			'referrerpolicy'  => true,
			'class'           => true,
		];

		return $allowed;
	}

	private static function parseDateValue( array $field, string $text ): ?\DateTimeImmutable {
		$formats = [];
		$save_format = trim( (string) ( $field['save_format'] ?? '' ) );
		if ( $save_format !== '' ) {
			$formats[] = $save_format;
		}
		$formats[] = 'Y-m-d';

		foreach ( array_values( array_unique( $formats ) ) as $format ) {
			$date  = \DateTimeImmutable::createFromFormat( '!' . $format, $text );
			$parse = \DateTimeImmutable::getLastErrors();
			if (
				$date instanceof \DateTimeImmutable
				&& $date->format( $format ) === $text
				&& ( $parse === false || ( $parse['warning_count'] === 0 && $parse['error_count'] === 0 ) )
			) {
				return $date;
			}
		}

		return null;
	}

	private static function parseExactDateTime( string $format, string $text, string $roundtrip_format ): ?\DateTimeImmutable {
		$date  = \DateTimeImmutable::createFromFormat( $format, $text );
		$parse = \DateTimeImmutable::getLastErrors();
		if (
			$date instanceof \DateTimeImmutable
			&& $date->format( $roundtrip_format ) === $text
			&& ( $parse === false || ( $parse['warning_count'] === 0 && $parse['error_count'] === 0 ) )
		) {
			return $date;
		}

		return null;
	}

	private static function formatColorArray( string $value ): array {
		if ( preg_match( '/^rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),([01](?:\.\d+)?)\)$/', $value, $matches ) ) {
			return [
				'red'   => (int) $matches[1],
				'green' => (int) $matches[2],
				'blue'  => (int) $matches[3],
				'alpha' => (float) $matches[4],
				'value' => $value,
			];
		}

		return [
			'value' => $value,
		];
	}

	/**
	 * @param mixed  $value  Stored post ID or list of IDs.
	 * @param string $format display|url|text.
	 */
	private static function formatPageLink( array $field, mixed $value, string $format ): mixed {
		$values = is_array( $value ) ? $value : [ $value ];
		$values = array_values( array_filter( array_map( static fn( mixed $item ): string => trim( (string) $item ), $values ) ) );
		if ( $values === [] ) {
			return '';
		}
		$return_format = sanitize_key( (string) ( $field['return'] ?? 'url' ) );

		if ( $return_format === 'id' ) {
			$out = [];
			foreach ( $values as $item ) {
				if ( str_starts_with( $item, 'archive:' ) ) {
					$out[] = self::pageLinkArchiveUrl( $item );
					continue;
				}
				$post_id = absint( $item );
				if ( $post_id > 0 && current_user_can( 'read_post', $post_id ) ) {
					$out[] = $post_id;
				}
			}
			$out = array_values( array_filter( $out ) );
			return is_array( $value ) ? $out : ( $out[0] ?? '' );
		}

		$links = [];
		foreach ( $values as $item ) {
			$is_archive = str_starts_with( $item, 'archive:' );
			if ( ! $is_archive && ! current_user_can( 'read_post', absint( $item ) ) ) {
				continue;
			}
			$url = $is_archive ? self::pageLinkArchiveUrl( $item ) : get_permalink( absint( $item ) );
			if ( ! is_string( $url ) || $url === '' ) {
				continue;
			}
			if ( $format === 'url' ) {
				return $url;
			}

			$title   = $is_archive ? self::pageLinkArchiveLabel( $item ) : get_the_title( absint( $item ) );
			$links[] = $format === 'display'
				? '<a href="' . esc_url( $url ) . '">' . esc_html( $title !== '' ? $title : $url ) . '</a>'
				: ( $title !== '' ? $title : $url );
		}

		return implode( ', ', $links );
	}

	private static function pageLinkArchiveUrl( string $token ): string {
		if ( $token === 'archive:home' ) {
			return home_url( '/' );
		}
		if ( ! preg_match( '/^archive:post_type:([a-z0-9_-]+)$/', $token, $matches ) ) {
			return '';
		}
		$url = get_post_type_archive_link( $matches[1] );

		return is_string( $url ) ? $url : '';
	}

	private static function pageLinkArchiveLabel( string $token ): string {
		if ( $token === 'archive:home' ) {
			return __( 'Home', 'wppowersuite-lite' );
		}
		if ( ! preg_match( '/^archive:post_type:([a-z0-9_-]+)$/', $token, $matches ) ) {
			return '';
		}
		$object = get_post_type_object( $matches[1] );

		return $object ? sprintf(
			/* translators: %s: post type label. */
			__( '%s archive', 'wppowersuite-lite' ),
			(string) $object->labels->name
		) : '';
	}

	/**
	 * @param mixed  $value  Stored map value.
	 * @param string $format display|url|text.
	 */
	private static function formatGoogleMap( mixed $value, string $format ): string {
		if ( ! is_array( $value ) ) {
			return '';
		}

		$address = trim( (string) ( $value['address'] ?? '' ) );
		$lat     = trim( (string) ( $value['lat'] ?? '' ) );
		$lng     = trim( (string) ( $value['lng'] ?? '' ) );
		if ( $address === '' && ( $lat === '' || $lng === '' ) ) {
			return '';
		}

		$query = $lat !== '' && $lng !== '' ? $lat . ',' . $lng : $address;
		$url   = 'https://www.google.com/maps/search/?api=1&query=' . rawurlencode( $query );
		if ( $format === 'url' ) {
			return $url;
		}

		$label = $address !== '' ? $address : $query;
		if ( $format === 'display' ) {
			return '<a href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $label ) . '</a>';
		}

		return $label;
	}
}
