<?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 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;
			}

			$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;
			}

			register_post_meta(
				$post_type,
				$meta_key,
				[
					'single'            => true,
					'type'              => $this->restType( $type, $field ),
					'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' => static function ( mixed $value ) use ( $field, $group ): mixed {
						$fields = new Fields();

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

	/**
	 * @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;
			}

			register_term_meta(
				$taxonomy,
				$meta_key,
				[
					'single'            => true,
					'type'              => $this->restType( $type, $field ),
					'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' => static function ( mixed $value ) use ( $field, $group ): mixed {
						$fields = new Fields();

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

	/**
	 * Whether a stored field type may appear on the public REST meta object.
	 *
	 * Password hashes are never registered: core REST reads skip auth_callback.
	 */
	public static function exposesTypeInRest( string $type ): bool {
		if ( ! Config::storesValue( $type ) ) {
			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 ): callable {
		return static function ( mixed $value ) use ( $field ): bool {
			$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.', 'wp-powersuite' ),
						$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.', 'wp-powersuite' ),
						$label
					)
				);
			}

			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, [ 'checkbox', 'gallery' ], true ) || ( ! empty( $field['multiple'] ) && in_array( $type, [ 'select', 'post_object', 'term', 'user' ], true ) ) ) {
			return 'array';
		}

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

		return 'string';
	}

	/**
	 * @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' ) {
			return [
				'type'  => 'array',
				'items' => [
					'type' => in_array( $type, [ 'post_object', 'term', 'user', 'gallery' ], true ) ? 'integer' : 'string',
				],
			];
		}

		if ( $rest_type === 'object' ) {
			return [
				'type'                 => 'object',
				'additionalProperties' => true,
			];
		}

		return [
			'type' => $rest_type,
		];
	}
}
