<?php
/**
 * Custom Fields — block editor field schema and placement rules.
 *
 * @package CustomFields
 * @since 1.4.0
 */

namespace CustomFields;

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

/**
 * Builds editor-ready field group schemas and resolves where fields render.
 */
final class EditorSchema {

	/**
	 * Field types rendered in the block editor document sidebar.
	 *
	 * @return array<int, string>
	 */
	public static function sidebarTypes(): array {
		return [
			'text',
			'textarea',
			'number',
			'range',
			'email',
			'phone',
			'url',
			'date',
			'time',
			'datetime',
			'color',
			'toggle',
			'select',
			'radio',
			'checkbox',
			'message',
			'image',
			'file',
			'link',
			'gallery',
			'oembed',
			'post_object',
			'term',
			'user',
		];
	}

	/**
	 * Nested field types that always require a meta box surface.
	 *
	 * @return array<int, string>
	 */
	public static function metaBoxTypes(): array {
		return Config::nestedTypes();
	}

	/**
	 * Stored presentation position for a group.
	 *
	 * @param array<string, mixed> $group Group definition.
	 */
	public static function groupPosition( array $group ): string {
		$position = sanitize_key( (string) ( $group['position'] ?? 'normal' ) );

		return array_key_exists( $position, Config::positions() ) ? $position : 'normal';
	}

	/**
	 * Whether a group uses the block editor document sidebar (Side position only).
	 *
	 * @param array<string, mixed> $group Group definition.
	 */
	public static function groupUsesGutenbergSidebar( array $group ): bool {
		return self::groupPosition( $group ) === 'side';
	}

	/**
	 * Whether the block editor is active for a post type.
	 *
	 * @param string $post_type Post type key.
	 */
	public static function usesBlockEditor( string $post_type ): bool {
		return function_exists( 'use_block_editor_for_post_type' ) && use_block_editor_for_post_type( $post_type );
	}

	/**
	 * Whether a field must use the classic meta box (nested or multi relational).
	 *
	 * @param array<string, mixed> $field Field definition.
	 */
	public static function fieldUsesMetaBox( array $field ): bool {
		$type = (string) ( $field['type'] ?? 'text' );
		if ( in_array( $type, self::metaBoxTypes(), true ) ) {
			return true;
		}

		if ( $type === 'wysiwyg' || $type === 'password' ) {
			return true;
		}

		return in_array( $type, [ 'post_object', 'term', 'user' ], true ) && ! empty( $field['multiple'] );
	}

	/**
	 * Whether a side-position group can be fully handled in the document sidebar.
	 *
	 * @param array<string, mixed> $group Group definition.
	 */
	public static function groupIsSidebarOnly( array $group ): bool {
		if ( ! self::groupUsesGutenbergSidebar( $group ) ) {
			return false;
		}

		foreach ( (array) ( $group['fields'] ?? [] ) as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			if ( self::fieldUsesMetaBox( $field ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Whether a group contains fields that must use the classic meta box.
	 *
	 * @param array<string, mixed> $group Group definition.
	 */
	public static function groupHasComplexFields( array $group ): bool {
		foreach ( (array) ( $group['fields'] ?? [] ) as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			if ( self::fieldUsesMetaBox( $field ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Fields that should render in classic meta boxes for a group.
	 *
	 * @param array<string, mixed> $group     Group definition.
	 * @param string               $post_type Post type key.
	 * @return array<int, array<string, mixed>>
	 */
	public static function fieldsForMetaBox( array $group, string $post_type ): array {
		$fields = isset( $group['fields'] ) && is_array( $group['fields'] ) ? $group['fields'] : [];
		if ( $fields === [] ) {
			return [];
		}

		if ( ! self::usesBlockEditor( $post_type ) ) {
			return $fields;
		}

		// Normal, after_title, and advanced always use meta boxes in the block editor.
		if ( ! self::groupUsesGutenbergSidebar( $group ) ) {
			return $fields;
		}

		// Side position: simple fields use the document sidebar via REST.
		if ( self::groupIsSidebarOnly( $group ) ) {
			return [];
		}

		$meta_box_fields = [];
		foreach ( $fields as $field ) {
			if ( is_array( $field ) && self::fieldUsesMetaBox( $field ) ) {
				$meta_box_fields[] = $field;
			}
		}

		return $meta_box_fields;
	}

	/**
	 * Build schema for matching groups that use the block editor sidebar.
	 *
	 * @param int    $post_id   Post ID (0 for new posts).
	 * @param string $post_type Post type key.
	 * @return array{postType: string, groups: array<int, array<string, mixed>>}
	 */
	public static function forPost( int $post_id, string $post_type ): array {
		if ( ! self::usesBlockEditor( $post_type ) ) {
			return [
				'postType' => $post_type,
				'groups'   => [],
			];
		}

		$locations = new Locations( new Storage() );
		$groups    = $locations->groupsForPost( $post_id, $post_type );
		$payload   = [];

		foreach ( $groups as $group_key => $group ) {
			if ( ! is_array( $group ) ) {
				continue;
			}

			$serialized = self::serializeGroup( (string) $group_key, $group, $post_id );
			if ( $serialized !== null ) {
				$payload[] = $serialized;
			}
		}

		return [
			'postType' => $post_type,
			'groups'   => $payload,
		];
	}

	/**
	 * @param array<string, mixed> $group Group definition.
	 * @return array<string, mixed>|null
	 */
	private static function serializeGroup( string $group_key, array $group, int $post_id ): ?array {
		if ( ! self::groupUsesGutenbergSidebar( $group ) ) {
			return null;
		}

		$fields = [];

		foreach ( (array) ( $group['fields'] ?? [] ) as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}

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

			if ( ! in_array( $type, self::sidebarTypes(), true ) && ! Config::storesValue( $type ) ) {
				if ( $type !== 'message' ) {
					continue;
				}
			}

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

			$fields[] = self::serializeField( $field, $meta_key, $post_id );
		}

		if ( $fields === [] ) {
			return null;
		}

		$field_key_map = [];
		foreach ( $fields as $field ) {
			if ( (string) ( $field['key'] ?? '' ) !== '' ) {
				$field_key_map[ (string) $field['key'] ] = (string) ( $field['metaKey'] ?? '' );
			}
		}

		return [
			'key'         => $group_key,
			'title'       => (string) ( $group['title'] ?? $group_key ),
			'position'    => 'side',
			'sidebarOnly' => self::groupIsSidebarOnly( $group ),
			'fieldKeyMap' => $field_key_map,
			'fields'      => $fields,
		];
	}

	/**
	 * Whether any matching group renders fields in meta boxes.
	 */
	public static function postTypeUsesMetaBoxFields( int $post_id, string $post_type ): bool {
		$locations = new Locations( new Storage() );
		foreach ( $locations->groupsForPost( $post_id, $post_type ) as $group ) {
			if ( is_array( $group ) && self::fieldsForMetaBox( $group, $post_type ) !== [] ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Whether any matching group uses the block editor document sidebar.
	 */
	public static function postTypeUsesGutenbergSidebar( int $post_id, string $post_type ): bool {
		if ( ! self::usesBlockEditor( $post_type ) ) {
			return false;
		}

		$locations = new Locations( new Storage() );
		foreach ( $locations->groupsForPost( $post_id, $post_type ) as $group ) {
			if ( ! is_array( $group ) || ! self::groupUsesGutenbergSidebar( $group ) ) {
				continue;
			}

			foreach ( (array) ( $group['fields'] ?? [] ) as $field ) {
				if ( is_array( $field ) && ! self::fieldUsesMetaBox( $field ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * @deprecated 1.10.2 Use postTypeUsesMetaBoxFields().
	 */
	public static function postTypeHasComplexFields( int $post_id, string $post_type ): bool {
		return self::postTypeUsesMetaBoxFields( $post_id, $post_type );
	}

	/**
	 * @param array<string, mixed> $field     Field definition.
	 * @return array<string, mixed>
	 */
	private static function serializeField( array $field, string $meta_key, int $post_id ): array {
		$type = (string) ( $field['type'] ?? 'text' );

		$item = [
			'key'          => (string) ( $field['key'] ?? '' ),
			'label'        => (string) ( $field['label'] ?? '' ),
			'name'         => (string) ( $field['name'] ?? '' ),
			'type'         => $type,
			'metaKey'      => $meta_key,
			'required'     => ! empty( $field['required'] ),
			'instructions' => (string) ( $field['instructions'] ?? '' ),
			'placeholder'  => (string) ( $field['placeholder'] ?? '' ),
			'default'      => $field['default'] ?? '',
			'multiple'     => ! empty( $field['multiple'] ),
			'choices'      => self::serializeChoices( (string) ( $field['choices'] ?? '' ) ),
			'min'          => (string) ( $field['min'] ?? '' ),
			'max'          => (string) ( $field['max'] ?? '' ),
			'step'         => (string) ( $field['step'] ?? '' ),
			'rows'         => max( 2, min( 20, (int) ( $field['rows'] ?? 4 ) ) ),
			'message'      => (string) ( $field['message_content'] ?? '' ),
			'conditional'  => ! empty( $field['conditional']['enabled'] ) ? ( $field['conditional'] ?? [] ) : [],
			'options'      => self::serializeRelationalOptions( $field ),
		];

		if ( $type === 'post_object' ) {
			$post_types = isset( $field['post_types'] ) && is_array( $field['post_types'] )
				? array_values( array_map( 'strval', $field['post_types'] ) )
				: [];
			$item['postTypes'] = $post_types;
		}

		if ( $type === 'term' ) {
			$item['taxonomy'] = sanitize_key( (string) ( $field['taxonomy'] ?? '' ) );
		}

		if ( Config::storesValue( $type ) && $meta_key !== '' && $post_id > 0 ) {
			$item['value'] = get_post_meta( $post_id, $meta_key, true );
		} elseif ( $type === 'link' ) {
			$item['value'] = [ 'url' => '', 'title' => '', 'target' => '' ];
		} elseif ( $type === 'toggle' ) {
			$item['value'] = '';
		} elseif ( in_array( $type, [ 'checkbox', 'gallery' ], true ) || ( ! empty( $field['multiple'] ) && $type === 'select' ) ) {
			$item['value'] = [];
		} else {
			$item['value'] = $field['default'] ?? '';
		}

		return $item;
	}

	/**
	 * @return array<int, array{value: string, label: string}>
	 */
	private static function serializeChoices( string $choices ): array {
		$map = Validator::parseChoices( $choices );
		$out = [];
		foreach ( $map as $value => $label ) {
			$out[] = [
				'value' => (string) $value,
				'label' => (string) $label,
			];
		}

		return $out;
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @return array<int, array{value: string, label: string}>
	 */
	private static function serializeRelationalOptions( array $field ): array {
		$type = (string) ( $field['type'] ?? 'text' );
		$out  = [];

		if ( $type === 'post_object' ) {
			$post_types = isset( $field['post_types'] ) && is_array( $field['post_types'] ) ? array_map( 'strval', $field['post_types'] ) : [];
			$query      = [
				'post_type'              => $post_types !== [] ? $post_types : 'any',
				'post_status'            => 'publish',
				'posts_per_page'         => 100,
				'orderby'                => 'title',
				'order'                  => 'ASC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			];
			$posts = get_posts( $query );
			foreach ( $posts as $post ) {
				if ( ! $post instanceof \WP_Post ) {
					continue;
				}
				$out[] = [
					'value' => (string) $post->ID,
					'label' => $post->post_title . ' (' . $post->post_type . ')',
				];
			}

			return $out;
		}

		if ( $type === 'term' ) {
			$taxonomy = sanitize_key( (string) ( $field['taxonomy'] ?? '' ) );
			if ( $taxonomy === '' || ! taxonomy_exists( $taxonomy ) ) {
				return [];
			}
			$terms = get_terms(
				[
					'taxonomy'   => $taxonomy,
					'hide_empty' => false,
					'number'     => 200,
				]
			);
			if ( ! is_array( $terms ) ) {
				return [];
			}
			foreach ( $terms as $term ) {
				if ( ! $term instanceof \WP_Term ) {
					continue;
				}
				$out[] = [
					'value' => (string) $term->term_id,
					'label' => $term->name,
				];
			}

			return $out;
		}

		if ( $type === 'user' ) {
			foreach ( get_users( [ 'number' => 200, 'orderby' => 'display_name' ] ) as $user ) {
				if ( ! $user instanceof \WP_User ) {
					continue;
				}
				$out[] = [
					'value' => (string) $user->ID,
					'label' => $user->display_name . ' (' . $user->user_login . ')',
				];
			}
		}

		return $out;
	}
}
