<?php
/**
 * Custom Fields — REST API meta registration.
 *
 * @package CustomFields
 * @since 1.4.0
 */

namespace CustomFields;

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

/**
 * Registers stored field meta keys with the REST API.
 */
final class RestMeta {

	private const MAX_MULTI_VALUE_ITEMS = 500;

	private Storage $storage;

	public function __construct( Storage $storage ) {
		$this->storage = $storage;
	}

	/**
	 * Hook registration.
	 */
	public function register(): void {
		add_action( 'init', [ $this, 'registerAll' ], 25 );
	}

	/**
	 * Register post and term meta for every enabled field group.
	 */
	public function registerAll(): void {
		foreach ( $this->storage->all() as $group_key => $group ) {
			if ( ! is_array( $group ) || empty( $group['enabled'] ) ) {
				continue;
			}
			if ( empty( $group['show_in_rest'] ) ) {
				continue;
			}

			$rules  = LocationRules::get( $group );
			$screen = (string) ( $rules['screen'] ?? 'post' );
			$fields = isset( $group['fields'] ) && is_array( $group['fields'] ) ? $group['fields'] : [];

			if ( $screen === 'taxonomy' ) {
				foreach ( LocationTargets::taxonomiesFromRules( $rules ) as $taxonomy ) {
					$this->registerTermFields( $taxonomy, (string) $group_key, $fields, $group );
				}
				continue;
			}

			foreach ( LocationTargets::postTypesFromRules( $rules ) as $post_type ) {
				$this->registerPostFields( $post_type, (string) $group_key, $fields, $group );
			}
		}
	}

	/**
	 * @param array<int, array<string, mixed>> $fields Field list.
	 */
	private function registerPostFields( string $post_type, string $group_key, array $fields, array $group ): void {
		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

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

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

			$rest_type = $this->restType( $type, $field );
			register_post_meta(
				$post_type,
				$meta_key,
				[
					'single'            => true,
					'type'              => $rest_type,
					'show_in_rest'      => [
						'schema' => $this->restSchema( $type, $field ),
					],
					'auth_callback'     => static function ( bool $allowed, string $meta_key_arg, int $object_id ) use ( $post_type ): bool {
						unset( $allowed, $meta_key_arg );

						if ( ! current_user_can( 'edit_post', $object_id ) ) {
							return false;
						}

						return get_post_type( $object_id ) === $post_type;
					},
					'sanitize_callback' => function ( mixed $value ) use ( $field, $group, $rest_type ): mixed {
						$fields = new Fields();

						return $this->normalizeRestValue( $rest_type, $fields->sanitizeValue( $field, $value, $group ) );
					},
					'validate_callback' => $this->validateCallback( $field, $group ),
				]
			);
		}
	}

	/**
	 * @param array<int, array<string, mixed>> $fields Field list.
	 */
	private function registerTermFields( string $taxonomy, string $group_key, array $fields, array $group ): void {
		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

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

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

			$rest_type = $this->restType( $type, $field );
			register_term_meta(
				$taxonomy,
				$meta_key,
				[
					'single'            => true,
					'type'              => $rest_type,
					'show_in_rest'      => [
						'schema' => $this->restSchema( $type, $field ),
					],
					'auth_callback'     => static function ( bool $allowed, string $meta_key_arg, int $object_id ) use ( $taxonomy ): bool {
						unset( $allowed, $meta_key_arg );

						$term = get_term( $object_id, $taxonomy );

						return $term instanceof \WP_Term && ! is_wp_error( $term ) && current_user_can( 'edit_term', $object_id );
					},
					'sanitize_callback' => function ( mixed $value ) use ( $field, $group, $rest_type ): mixed {
						$fields = new Fields();

						return $this->normalizeRestValue( $rest_type, $fields->sanitizeValue( $field, $value, $group ) );
					},
					'validate_callback' => $this->validateCallback( $field, $group ),
				]
			);
		}
	}

	/**
	 * Whether a stored field type may appear on the public REST meta object.
	 *
	 * Password hashes and builder-only containers are never registered:
	 * core REST reads skip auth_callback and nested schemas need bespoke handling.
	 */
	public static function exposesTypeInRest( string $type ): bool {
		if ( ! Config::storesValue( $type ) ) {
			return false;
		}

		if ( in_array( $type, Config::nestedTypes(), true ) ) {
			return false;
		}

		return ! in_array( $type, FieldOutput::sensitiveTypes(), true );
	}

	/**
	 * REST validation for required fields and optional regex patterns.
	 *
	 * @param array<string, mixed> $field Field definition.
	 */
	private function validateCallback( array $field, array $group ): callable {
		return static function ( mixed $value ) use ( $field, $group ): bool|\WP_Error {
			$fields = new Fields();
			$label  = (string) ( $field['label'] ?? $field['name'] ?? '' );

			if ( ! empty( $field['required'] ) && $fields->isEmpty( $value ) ) {
				return new \WP_Error(
					'wpps_cf_required',
					sprintf(
						/* translators: %s: field label. */
						__( '“%s” is required.', 'wppowersuite-lite' ),
						$label
					)
				);
			}

			if ( ! $fields->isEmpty( $value ) && ! $fields->matchesPattern( $field, $value ) ) {
				return new \WP_Error(
					'wpps_cf_invalid',
					sprintf(
						/* translators: %s: field label. */
						__( '“%s” has an invalid format.', 'wppowersuite-lite' ),
						$label
					)
				);
			}

			$value_errors = $fields->validateValue( $field, $value, $group );
			if ( $value_errors !== [] ) {
				return new \WP_Error(
					'wpps_cf_invalid',
					implode( ' ', $value_errors )
				);
			}

			return true;
		};
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 */
	private function restType( string $type, array $field ): string {
		if ( in_array( $type, [ 'number', 'range' ], true ) ) {
			return 'number';
		}

		if ( in_array( $type, [ 'post_object', 'term', 'user', 'nav_menu', 'image', 'file' ], true ) && ! $this->fieldStoresMultiple( $type, $field ) ) {
			return 'integer';
		}

		if ( $this->fieldStoresMultiple( $type, $field ) ) {
			return 'array';
		}

		if ( in_array( $type, [ 'repeater', 'flexible_content' ], true ) ) {
			return 'array';
		}

		if ( in_array( $type, [ 'link', 'google_map', 'group', 'clone' ], true ) ) {
			return 'object';
		}

		return 'string';
	}

	/**
	 * Whether a term field stores multiple term IDs.
	 *
	 * @param array<string, mixed> $field Field definition.
	 */
	private function termAllowsMultiple( array $field ): bool {
		$field_type = sanitize_key( (string) ( $field['taxonomy_field_type'] ?? 'checkbox' ) );

		return in_array( $field_type, [ 'multi_select', 'checkbox' ], true );
	}

	/**
	 * Whether a field stores an array value.
	 *
	 * @param array<string, mixed> $field Field definition.
	 */
	private function fieldStoresMultiple( string $type, array $field ): bool {
		return in_array( $type, [ 'checkbox', 'gallery', 'relationship' ], true )
			|| ( ! empty( $field['multiple'] ) && in_array( $type, [ 'select', 'post_object', 'relationship', 'page_link', 'term', 'user' ], true ) )
			|| ( $type === 'term' && $this->termAllowsMultiple( $field ) );
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @return array<string, mixed>
	 */
	private function restSchema( string $type, array $field ): array {
		$rest_type = $this->restType( $type, $field );

		if ( $rest_type === 'array' ) {
			$item_schema = [
				'type' => in_array( $type, [ 'post_object', 'relationship', 'term', 'user', 'gallery' ], true ) ? 'integer' : 'string',
			];
			$enum        = $this->choiceValuesForSchema( $type, $field );
			if ( $enum !== [] ) {
				$item_schema['enum'] = $enum;
			}

			return [
				'type'     => 'array',
				'maxItems' => self::MAX_MULTI_VALUE_ITEMS,
				'items'    => $item_schema,
			];
		}

		if ( $rest_type === 'object' ) {
			if ( $type === 'link' ) {
				return [
					'type'                 => 'object',
					'additionalProperties' => false,
					'properties'           => [
						'url'    => [
							'type'   => 'string',
							'format' => 'uri',
						],
						'title'  => [
							'type' => 'string',
						],
						'target' => [
							'type' => 'string',
							'enum' => [ '', '_blank' ],
						],
					],
				];
			}

			if ( $type === 'google_map' ) {
				return [
					'type'                 => 'object',
					'additionalProperties' => false,
					'properties'           => [
						'address' => [
							'type' => 'string',
						],
						'lat'     => [
							'type' => 'string',
						],
						'lng'     => [
							'type' => 'string',
						],
						'zoom'    => [
							'type' => 'integer',
						],
					],
				];
			}

			return [
				'type'                 => 'object',
				'additionalProperties' => true,
			];
		}

		$schema = [
			'type' => $rest_type,
		];

		if ( $type === 'email' ) {
			$schema['format'] = 'email';
		} elseif ( in_array( $type, [ 'url', 'oembed' ], true ) ) {
			$schema['format'] = 'uri';
		} elseif ( $type === 'date' ) {
			if ( trim( (string) ( $field['save_format'] ?? '' ) ) === '' ) {
				$schema['pattern'] = '^\\d{4}-\\d{2}-\\d{2}$';
			}
		} elseif ( $type === 'datetime' ) {
			$schema['pattern'] = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2})?$';
		} elseif ( $type === 'time' ) {
			$schema['pattern'] = '^\\d{2}:\\d{2}(:\\d{2})?$';
		}

		if ( $rest_type === 'string' && Config::typeSupports( $type, 'maxlength' ) ) {
			$maxlength = absint( $field['maxlength'] ?? 0 );
			if ( $maxlength > 0 ) {
				$schema['maxLength'] = $maxlength;
			}
		}

		$enum = $this->choiceValuesForSchema( $type, $field );
		if ( $rest_type === 'string' && $enum !== [] ) {
			if ( ! empty( $field['allow_null'] ) ) {
				array_unshift( $enum, '' );
			}
			$schema['enum'] = array_values( array_unique( $enum ) );
		}

		return $schema;
	}

	/**
	 * Configured choice values that REST clients may validate locally.
	 *
	 * @param array<string, mixed> $field Field definition.
	 * @return array<int, string>
	 */
	private function choiceValuesForSchema( string $type, array $field ): array {
		if ( ! in_array( $type, Config::choiceTypes(), true ) ) {
			return [];
		}

		if ( $type === 'select' && ! empty( $field['select_create_options'] ) ) {
			return [];
		}

		if ( in_array( $type, [ 'radio', 'checkbox' ], true ) && ! empty( $field['allow_custom'] ) ) {
			return [];
		}

		$choices = Validator::parseChoices( (string) ( $field['choices'] ?? '' ) );

		return array_values( array_map( 'strval', array_keys( $choices ) ) );
	}

	private function normalizeRestValue( string $rest_type, mixed $value ): mixed {
		if ( $rest_type === 'integer' && $value === '' ) {
			return 0;
		}

		return $value;
	}
}
