<?php
/**
 * UI field schemas for guided instance forms (server-authoritative metadata).
 *
 * @package SEOvaultSEO\Schema
 */

namespace SEOvaultSEO\Schema;

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

/**
 * Converts type class field definitions into safe guided-form controls.
 */
class SchemaGuidedFormSchema {

	/**
	 * Control types supported by the Phase 24 renderer.
	 *
	 * @var string[]
	 */
	const CONTROL_TYPES = array(
		'text',
		'textarea',
		'url',
		'datetime',
		'number',
		'enum',
		'media',
		'group',
		'list',
		'list_text',
		'hidden',
	);

	/**
	 * Field ids never shown as author-facing controls.
	 *
	 * @var string[]
	 */
	const HIDDEN_INTERNAL_IDS = array(
		'schema_name',
	);

	/**
	 * Build a UI schema for a guided/manual type.
	 *
	 * @param string $type Schema type.
	 * @return array{ok:bool,type:string,label:string,fields:array,errors:string[]}
	 */
	public static function build_for_type( $type ) {
		$type = (string) $type;
		if ( '' === $type || ! SchemaInstanceFieldSanitizer::is_guided_or_manual_product_type( $type ) ) {
			return array(
				'ok'     => false,
				'type'   => $type,
				'label'  => $type,
				'fields' => array(),
				'errors' => array( 'unsupported_type' ),
			);
		}

		$entry = SchemaTypePolicyRegistry::get_entry( $type );
		$label = is_array( $entry ) ? (string) ( $entry['label'] ?? $type ) : $type;
		$defs  = SchemaInstanceFieldSanitizer::get_field_definitions( $type );
		if ( empty( $defs ) ) {
			return array(
				'ok'     => false,
				'type'   => $type,
				'label'  => $label,
				'fields' => array(),
				'errors' => array( 'missing_field_definitions' ),
			);
		}

		$fields = self::map_definitions( $defs );
		$fields = self::apply_type_field_rules( $type, $fields );
		$help   = self::type_help( $type );

		return array(
			'ok'                    => true,
			'type'                  => $type,
			'label'                 => $label,
			'fields'                => $fields,
			'errors'                => array(),
			'help'                  => $help['help'],
			'picker_description'    => $help['picker_description'],
			'requires_visibility'   => is_array( $entry ) ? ! empty( $entry['requires_visible_content'] ) : true,
			'supported_controls'    => self::CONTROL_TYPES,
			'client_validity_note'  => 'advisory_only',
		);
	}

	/**
	 * Build schemas for all built-in guided/manual product types.
	 *
	 * @return array<string,array>
	 */
	public static function build_catalogue() {
		$types = array(
			'Event',
			'Recipe',
			'Course',
			'JobPosting',
			'SoftwareApplication',
			'Review',
			'Product',
		);
		$out   = array();
		foreach ( $types as $type ) {
			$built = self::build_for_type( $type );
			if ( ! empty( $built['ok'] ) ) {
				$out[ $type ] = $built;
			}
		}
		return $out;
	}

	/**
	 * Author-facing help copy for guided types.
	 *
	 * @param string $type Schema type.
	 * @return array{help:string,picker_description:string}
	 */
	public static function type_help( $type ) {
		$type = (string) $type;
		$catalog = array(
			'Event' => array(
				'help'               => __( 'Use Event only when this page is about a real upcoming or ongoing event. Add a start date and either a place or an online location. Incomplete drafts stay unfinished and do not change published schema.', 'seovault-ai' ),
				'picker_description' => __( 'Add Event details such as name, start date, and location. Drafts save without publishing until the server confirms readiness.', 'seovault-ai' ),
			),
			'Course' => array(
				'help'               => __( 'Use Course only when this page represents a real course. Include the course name, description, and provider. Incomplete drafts stay unfinished and do not change published schema.', 'seovault-ai' ),
				'picker_description' => __( 'Add Course details such as name, description, and provider. Drafts save without publishing until the server confirms readiness.', 'seovault-ai' ),
			),
			'JobPosting' => array(
				'help'               => __( 'Use Job posting only for a real open role on this page. Include title, description, date posted, hiring organization, and either a location or remote mode. Expired jobs are withheld.', 'seovault-ai' ),
				'picker_description' => __( 'Add Job posting details such as title, description, hiring organization, and location or remote. Drafts save without publishing until the server confirms readiness.', 'seovault-ai' ),
			),
			'Recipe' => array(
				'help'               => __( 'Use Recipe only when this page is a real recipe. Include the recipe name, an image of the dish, and at least one real instruction. Do not add empty nutrition facts. Incomplete drafts stay unfinished and do not change published schema.', 'seovault-ai' ),
				'picker_description' => __( 'Add Recipe details such as name, image, and instructions. Drafts save without publishing until the server confirms readiness.', 'seovault-ai' ),
			),
			'SoftwareApplication' => array(
				'help'               => __( 'Use Software application only when this page represents real software. Include the name, a real offer price (0 for free), and currency. Optional aggregate ratings must be genuine. Incomplete drafts stay unfinished and do not change published schema.', 'seovault-ai' ),
				'picker_description' => __( 'Add Software application details such as name, offer price, and currency. Drafts save without publishing until the server confirms readiness.', 'seovault-ai' ),
			),
			'Review' => array(
				'help'               => __( 'Use Review only for a visible, independent review on this page. Include the reviewed item, author, rating within bounds, and review text. Self-serving reviews of your own business or product are blocked.', 'seovault-ai' ),
				'picker_description' => __( 'Add Review details such as item reviewed, author, rating, and review text. Drafts save without publishing until the server confirms readiness.', 'seovault-ai' ),
			),
		);

		if ( isset( $catalog[ $type ] ) ) {
			return $catalog[ $type ];
		}

		$entry = SchemaTypePolicyRegistry::get_entry( $type );
		$label = is_array( $entry ) ? (string) ( $entry['label'] ?? $type ) : $type;

		return array(
			'help'               => sprintf(
				/* translators: %s: schema type label. */
				__( 'Add %s only when this page really represents that content. Incomplete drafts are saved but not published until readiness passes on the server.', 'seovault-ai' ),
				$label
			),
			'picker_description' => sprintf(
				/* translators: %s: schema type label. */
				__( 'Opens the %s guided form. Incomplete drafts are saved but not published until readiness passes on the server.', 'seovault-ai' ),
				$label
			),
		);
	}

	/**
	 * Attach conditional visibility rules for type-specific UX.
	 *
	 * @param string                         $type   Schema type.
	 * @param array<int,array<string,mixed>> $fields Mapped fields.
	 * @return array<int,array<string,mixed>>
	 */
	private static function apply_type_field_rules( $type, array $fields ) {
		if ( 'Event' !== (string) $type ) {
			return $fields;
		}

		foreach ( $fields as &$field ) {
			if ( ! is_array( $field ) || 'location' !== (string) ( $field['id'] ?? '' ) || empty( $field['fields'] ) || ! is_array( $field['fields'] ) ) {
				continue;
			}
			foreach ( $field['fields'] as &$child ) {
				if ( ! is_array( $child ) || empty( $child['id'] ) ) {
					continue;
				}
				$id = (string) $child['id'];
				if ( in_array( $id, array( 'name', 'address' ), true ) ) {
					$child['visible_when'] = array(
						'path' => '@type',
						'in'   => array( '', 'Place' ),
					);
				} elseif ( 'url' === $id ) {
					$child['visible_when'] = array(
						'path' => '@type',
						'in'   => array( 'VirtualLocation' ),
					);
				}
			}
			unset( $child );
		}
		unset( $field );

		return $fields;
	}

	/**
	 * Map server definitions to UI controls.
	 *
	 * @param array<int,array<string,mixed>> $definitions Field defs.
	 * @return array<int,array<string,mixed>>
	 */
	private static function map_definitions( array $definitions ) {
		$fields = array();
		foreach ( $definitions as $field ) {
			if ( ! is_array( $field ) || empty( $field['id'] ) ) {
				continue;
			}
			$mapped = self::map_field( $field );
			if ( null !== $mapped ) {
				$fields[] = $mapped;
			}
		}
		return $fields;
	}

	/**
	 * Map one definition.
	 *
	 * @param array<string,mixed> $field Def.
	 * @return array<string,mixed>|null
	 */
	private static function map_field( array $field ) {
		$id = (string) $field['id'];
		if ( in_array( $id, self::HIDDEN_INTERNAL_IDS, true ) ) {
			return null;
		}

		$type    = (string) ( $field['type'] ?? 'Text' );
		$control = self::control_for_type( $type, $field );
		$label   = isset( $field['label'] ) ? (string) $field['label'] : $id;

		$mapped = array(
			'id'          => $id,
			'label'       => $label,
			'description' => isset( $field['desc'] ) ? (string) $field['desc'] : ( isset( $field['description'] ) ? (string) $field['description'] : '' ),
			'control'     => $control,
			'required'    => ! empty( $field['required'] ),
			'default'     => array_key_exists( 'std', $field ) ? $field['std'] : '',
		);

		if ( 'hidden' === $control ) {
			$mapped['label'] = $label;
		}

		if ( 'enum' === $control ) {
			$mapped['options'] = self::normalize_options( isset( $field['options'] ) ? $field['options'] : array() );
		}

		if ( 'group' === $control || 'list' === $control ) {
			$nested = isset( $field['fields'] ) && is_array( $field['fields'] ) ? $field['fields'] : array();
			$mapped['fields'] = self::map_definitions( $nested );
		}

		if ( 'list_text' === $control ) {
			$mapped['item_label'] = $label;
		}

		if ( 'datetime' === $control && is_string( $mapped['default'] ) ) {
			$mapped['default'] = SchemaSanitizer::to_datetime_local_value( $mapped['default'] );
		}

		return $mapped;
	}

	/**
	 * Resolve UI control for a definition type.
	 *
	 * @param string              $type  Definition type.
	 * @param array<string,mixed> $field Full def.
	 * @return string
	 */
	private static function control_for_type( $type, array $field ) {
		$id = (string) ( $field['id'] ?? '' );

		if ( 'Hidden' === $type || '@id' === $id ) {
			return 'hidden';
		}
		if ( '@type' === $id && 'Enum' !== $type ) {
			return 'hidden';
		}
		if ( 'Group' === $type ) {
			return ! empty( $field['cloneable'] ) ? 'list' : 'group';
		}
		if ( 'DateTime' === $type ) {
			return 'datetime';
		}
		if ( 'Image' === $type || 'Media' === $type ) {
			return 'media';
		}
		if ( 'Number' === $type ) {
			return 'number';
		}
		if ( 'Enum' === $type ) {
			return 'enum';
		}
		if ( 'URL' === $type || 'Url' === $type || 'url' === strtolower( $id ) ) {
			return 'url';
		}
		if ( 'Textarea' === $type ) {
			return 'textarea';
		}

		// Common string-list fields (e.g. recipeIngredient) use a repeatable text list.
		if ( in_array( $id, array( 'recipeIngredient', 'recipeInstructions' ), true ) ) {
			return 'list_text';
		}

		return 'text';
	}

	/**
	 * Normalize enum options to value/label pairs.
	 *
	 * @param mixed $options Raw options.
	 * @return array<int,array{value:string,label:string}>
	 */
	private static function normalize_options( $options ) {
		if ( ! is_array( $options ) ) {
			return array();
		}
		$out = array();
		foreach ( $options as $key => $label ) {
			if ( is_int( $key ) ) {
				$value = (string) $label;
				$out[] = array(
					'value' => $value,
					'label' => $value,
				);
			} else {
				$out[] = array(
					'value' => (string) $key,
					'label' => (string) $label,
				);
			}
		}
		return $out;
	}
}
