<?php
/**
 * Wizard Schema Registry.
 *
 * Central registry that maps generator types to their wizard schemas
 * and exposes predefined option sets for the Prompt Builder wizard.
 *
 * @package AIPT\Api\PromptBuilder
 * @since   2.6.0
 */

namespace AIPT\Api\PromptBuilder;

use AIPT\Api\PromptBuilder\Schemas\DescriptionSchema;
use AIPT\Api\PromptBuilder\Schemas\StandardSchema;
use AIPT\Api\PromptBuilder\Schemas\TitleSchema;
use AIPT\Api\PromptBuilder\Schemas\FAQSchema;
use AIPT\Api\PromptBuilder\Schemas\SeoMetaSchema;
use AIPT\Api\PromptBuilder\Schemas\TaxonomySchema;
use AIPT\Api\PromptBuilder\OptionSets\Tones;
use AIPT\Api\PromptBuilder\OptionSets\AgeGroups;
use AIPT\Api\PromptBuilder\OptionSets\TargetAudiences;
use AIPT\Api\PromptBuilder\OptionSets\ContentGoals;
use AIPT\Api\PromptBuilder\OptionSets\WritingStyles;
use AIPT\Api\PromptBuilder\OptionSets\FormatStyles;

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

/**
 * Central registry for wizard schemas and option sets.
 *
 * Maps generator types to their schema classes and provides access to
 * predefined option sets used across all wizard steps.
 */
class WizardSchemaRegistry {

	/**
	 * Map of generator type strings to schema classes.
	 *
	 * @var array<string, string>
	 */
	private const SCHEMA_MAP = array(
		'description' => DescriptionSchema::class,
		'standard'    => StandardSchema::class,
		'title'       => TitleSchema::class,
		'faq'         => FAQSchema::class,
		'seo_meta'    => SeoMetaSchema::class,
		'taxonomy'    => TaxonomySchema::class,
	);

	/**
	 * Map of option set keys to option set classes.
	 *
	 * @var array<string, string>
	 */
	private const OPTION_SET_MAP = array(
		'tones'            => Tones::class,
		'age_groups'       => AgeGroups::class,
		'target_audiences' => TargetAudiences::class,
		'content_goals'    => ContentGoals::class,
		'writing_styles'   => WritingStyles::class,
		'format_styles'    => FormatStyles::class,
	);

	/**
	 * Get the wizard schema for a given generator type.
	 *
	 * @param string $generator_type One of: description, standard, title, faq, seo_meta, taxonomy.
	 * @return array<string, mixed> The schema array (see WizardSchema TypeScript interface).
	 * @throws \InvalidArgumentException If generator_type is not supported.
	 */
	public function get_schema( string $generator_type ): array {
		if ( ! isset( self::SCHEMA_MAP[ $generator_type ] ) ) {
			throw new \InvalidArgumentException(
				sprintf( 'Unsupported generator_type: %s', esc_html( $generator_type ) )
			);
		}
		$class = self::SCHEMA_MAP[ $generator_type ];
		return $class::get_schema();
	}

	/**
	 * Get all predefined option sets.
	 *
	 * @return array<string, array<int, array{value: string, label: string, description: string}>> Map of option_set_key to array of OptionSetItem.
	 */
	public function get_option_sets(): array {
		$sets = array();
		foreach ( self::OPTION_SET_MAP as $key => $class ) {
			$sets[ $key ] = $class::get();
		}
		return $sets;
	}

	/**
	 * Get a single option set by key.
	 *
	 * @param string $key Option set key (tones, age_groups, etc.).
	 * @return array<int, array{value: string, label: string, description: string}> Array of OptionSetItem.
	 * @throws \InvalidArgumentException If key is not recognized.
	 */
	public function getOptionSet( string $key ): array {
		if ( ! isset( self::OPTION_SET_MAP[ $key ] ) ) {
			throw new \InvalidArgumentException(
				sprintf( 'Unknown option set: %s', esc_html( $key ) )
			);
		}
		return self::OPTION_SET_MAP[ $key ]::get();
	}

	/**
	 * Get all supported generator types.
	 *
	 * @return string[] Array of generator type strings.
	 */
	public function getSupportedGeneratorTypes(): array {
		return array_keys( self::SCHEMA_MAP );
	}

	/**
	 * Validate wizard step values against a schema.
	 *
	 * $step_values must be pre-sanitized by the caller before passing here.
	 * Recommended sanitizers: absint() for integer fields, sanitize_text_field() for
	 * single-line strings, sanitize_textarea_field() for multi-line strings.
	 *
	 * Checks:
	 * - All `required: true` steps have non-empty values
	 * - `dynamic_array` types respect min_items / max_items
	 * - `text_input` with validation_rules (numeric, min, max) validated
	 *
	 * Note: single_select / multi_select option-set validation is handled at the
	 * auto-fill layer (filterValuesAgainstSchema) rather than here.
	 *
	 * @param string $generator_type Generator type.
	 * @param array  $step_values    Pre-sanitized wizard form values: [step_key => value_or_array].
	 * @return array<string, mixed> Shape: ['valid' => bool, 'errors' => array<string, string>].
	 * @throws \InvalidArgumentException If generator_type is not supported.
	 */
	public function validateStepValues( string $generator_type, array $step_values ): array {
		$schema = $this->get_schema( $generator_type );
		$errors = array();

		foreach ( $schema['steps'] as $step ) {

			if ( 'review' === $step['type'] ) {
				continue;
			}

			$key      = $step['key'];
			$value    = $step_values[ $key ] ?? null;
			$required = ! empty( $step['required'] );

			if ( $required ) {
				$is_empty = false;
				if ( null === $value || '' === $value ) {
					$is_empty = true;
				} elseif ( is_array( $value ) && 0 === count( $value ) ) {
					$is_empty = true;
				}
				if ( $is_empty ) {
					$errors[ $key ] = __( 'This field is required.', 'ai-product-tools' );
					continue;
				}
			}

			if ( null === $value || '' === $value || ( is_array( $value ) && 0 === count( $value ) ) ) {
				continue;
			}

			if ( 'dynamic_array' === $step['type'] ) {
				if ( ! is_array( $value ) ) {
					$errors[ $key ] = __( 'Expected an array of values.', 'ai-product-tools' );
					continue;
				}
			$non_empty = array_filter(
				$value,
				function( $v ) {
					return is_numeric( $v ) || ( is_string( $v ) && '' !== trim( $v ) );
				}
			);
				$count     = count( $non_empty );

				if ( isset( $step['min_items'] ) && $count < $step['min_items'] ) {
					$errors[ $key ] = sprintf(
						/* translators: %d: minimum items */
						__( 'At least %d items are required.', 'ai-product-tools' ),
						$step['min_items']
					);
					continue;
				}
				if ( isset( $step['max_items'] ) && $count > $step['max_items'] ) {
					$errors[ $key ] = sprintf(
						/* translators: %d: maximum items */
						__( 'Maximum %d items allowed.', 'ai-product-tools' ),
						$step['max_items']
					);
					continue;
				}
			}

			if ( 'text_input' === $step['type'] && ! empty( $step['validation_rules'] ) ) {
				$rules = $step['validation_rules'];
				if ( ! empty( $rules['numeric'] ) ) {
					if ( ! is_numeric( $value ) ) {
						$errors[ $key ] = __( 'Value must be numeric.', 'ai-product-tools' );
						continue;
					}
					$num = (float) $value;
					if ( isset( $rules['min'] ) && $num < $rules['min'] ) {
						$errors[ $key ] = sprintf(
							/* translators: %s: minimum value */
							__( 'Value must be at least %s.', 'ai-product-tools' ),
							$rules['min']
						);
						continue;
					}
					if ( isset( $rules['max'] ) && $num > $rules['max'] ) {
						$errors[ $key ] = sprintf(
							/* translators: %s: maximum value */
							__( 'Value must be at most %s.', 'ai-product-tools' ),
							$rules['max']
						);
						continue;
					}
				}
			}
		}

		return array(
			'valid'  => 0 === count( $errors ),
			'errors' => $errors,
		);
	}
}
