<?php
/**
 * CPT Builder taxonomy validation and sanitization.
 *
 * @package CptBuilder
 * @since 1.1.0
 */

namespace CptBuilder;

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

/**
 * Validation and sanitization helper for taxonomy definitions.
 */
final class TaxonomyValidator {

	private TaxonomyStorage $storage;

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

	/**
	 * Normalize a raw taxonomy key to allowed characters.
	 *
	 * @param string $key Raw key.
	 */
	public function sanitizeKey( string $key ): string {
		$key = strtolower( trim( $key ) );
		$key = preg_replace( '/[^a-z0-9_]/', '', $key );

		return substr( (string) $key, 0, Config::MAX_TAXONOMY_KEY_LENGTH );
	}

	/**
	 * Validate a taxonomy key against format and collision rules.
	 *
	 * @param string      $key          Sanitized key.
	 * @param bool        $is_edit      Whether editing an existing definition.
	 * @param string|null $original_key Original key when editing.
	 * @return string|null Error message, or null when valid.
	 */
	public function validateKey( string $key, bool $is_edit, ?string $original_key = null ): ?string {
		if ( $key === '' ) {
			return __( 'The taxonomy key is required.', 'wp-powersuite' );
		}

		if ( ! preg_match( '/^[a-z][a-z0-9_]*$/', $key ) ) {
			return __( 'The taxonomy key must start with a letter and use only lowercase letters, numbers, and underscores.', 'wp-powersuite' );
		}

		if ( strlen( $key ) > Config::MAX_TAXONOMY_KEY_LENGTH ) {
			return sprintf(
				/* translators: %d: maximum number of characters. */
				__( 'The taxonomy key cannot be longer than %d characters.', 'wp-powersuite' ),
				Config::MAX_TAXONOMY_KEY_LENGTH
			);
		}

		if ( in_array( $key, Config::reservedTaxonomies(), true ) ) {
			return __( 'That taxonomy key is reserved by WordPress and cannot be used.', 'wp-powersuite' );
		}

		if ( $is_edit && $original_key !== null && $key !== $original_key ) {
			return __( 'The taxonomy key cannot be changed after creation.', 'wp-powersuite' );
		}

		if ( ! $is_edit && $this->storage->exists( $key ) ) {
			return __( 'A taxonomy with this key already exists.', 'wp-powersuite' );
		}

		if ( ! $is_edit && taxonomy_exists( $key ) ) {
			return __( 'A taxonomy with this key is already registered by another plugin or theme.', 'wp-powersuite' );
		}

		return null;
	}

	/**
	 * Build a complete, sanitized taxonomy definition from raw input.
	 *
	 * @param array<string, mixed>      $input    Raw (already unslashed) input.
	 * @param array<string, mixed>|null $existing Existing definition when editing.
	 * @return array<string, mixed>
	 */
	public function buildDefinition( array $input, ?array $existing = null ): array {
		$now        = time();
		$definition = Config::defaultTaxonomyDefinition();

		if ( is_array( $existing ) ) {
			$definition = array_merge( $definition, $existing );
		}

		$definition['key']          = $this->sanitizeKey( (string) ( $input['key'] ?? ( $existing['key'] ?? '' ) ) );
		$definition['enabled']      = ! empty( $input['enabled'] );
		$definition['object_types'] = $this->sanitizeObjectTypes( isset( $input['object_types'] ) && is_array( $input['object_types'] ) ? $input['object_types'] : [] );
		$definition['labels']       = $this->sanitizeLabels( isset( $input['labels'] ) && is_array( $input['labels'] ) ? $input['labels'] : [] );
		$definition['args']         = $this->sanitizeArgs( isset( $input['args'] ) && is_array( $input['args'] ) ? $input['args'] : [] );
		$definition['schema']       = Config::SCHEMA_VERSION;
		$definition['created_at']   = (int) ( $existing['created_at'] ?? 0 ) ?: $now;
		$definition['updated_at']   = $now;

		return $definition;
	}

	/**
	 * Sanitize the list of object types (post type keys) to attach to.
	 *
	 * @param array<int, mixed> $object_types Raw list.
	 * @return array<int, string>
	 */
	public function sanitizeObjectTypes( array $object_types ): array {
		$clean = [];
		foreach ( $object_types as $object_type ) {
			$object_type = sanitize_key( (string) $object_type );
			if ( $object_type !== '' ) {
				$clean[] = $object_type;
			}
		}

		return array_values( array_unique( $clean ) );
	}

	/**
	 * Sanitize taxonomy label values.
	 *
	 * @param array<string, mixed> $labels Raw labels.
	 * @return array<string, string>
	 */
	public function sanitizeLabels( array $labels ): array {
		$clean = [];
		foreach ( Config::allTaxonomyLabelKeys() as $label_key ) {
			if ( ! isset( $labels[ $label_key ] ) ) {
				continue;
			}
			$value = sanitize_text_field( (string) $labels[ $label_key ] );
			if ( $value !== '' ) {
				$clean[ $label_key ] = $value;
			}
		}

		return $clean;
	}

	/**
	 * Sanitize and cast register_taxonomy() arguments.
	 *
	 * @param array<string, mixed> $args Raw args.
	 * @return array<string, mixed>
	 */
	public function sanitizeArgs( array $args ): array {
		$defaults = Config::defaultTaxonomyArgs();
		$clean    = $defaults;

		foreach ( Config::taxonomyBooleanArgKeys() as $bool_key ) {
			$clean[ $bool_key ] = ! empty( $args[ $bool_key ] );
		}

		$clean['description']     = isset( $args['description'] ) ? sanitize_textarea_field( (string) $args['description'] ) : '';
		$clean['rest_base']       = isset( $args['rest_base'] ) ? sanitize_title( (string) $args['rest_base'] ) : '';
		$clean['rewrite_slug']    = isset( $args['rewrite_slug'] ) ? sanitize_title( (string) $args['rewrite_slug'] ) : '';
		$clean['query_var_slug']  = isset( $args['query_var_slug'] ) ? sanitize_title( (string) $args['query_var_slug'] ) : '';

		$namespace = strtolower( trim( (string) ( $args['rest_namespace'] ?? '' ) ) );
		$namespace = preg_replace( '#[^a-z0-9_/-]#', '', $namespace );
		$namespace = preg_replace( '#/+#', '/', (string) $namespace );
		$clean['rest_namespace'] = trim( (string) $namespace, '/' );

		return $clean;
	}

	/**
	 * Validate a fully built taxonomy definition.
	 *
	 * @param array<string, mixed> $definition   Built definition.
	 * @param bool                 $is_edit      Whether editing.
	 * @param string|null          $original_key Original key when editing.
	 * @return array<int, string>
	 */
	public function validateDefinition( array $definition, bool $is_edit, ?string $original_key = null ): array {
		$errors = [];

		$key_error = $this->validateKey( (string) ( $definition['key'] ?? '' ), $is_edit, $original_key );
		if ( $key_error !== null ) {
			$errors[] = $key_error;
		}

		$labels = isset( $definition['labels'] ) && is_array( $definition['labels'] ) ? $definition['labels'] : [];
		if ( empty( $labels['singular_name'] ) ) {
			$errors[] = __( 'The singular label is required.', 'wp-powersuite' );
		}
		if ( empty( $labels['name'] ) ) {
			$errors[] = __( 'The plural label is required.', 'wp-powersuite' );
		}

		$args = isset( $definition['args'] ) && is_array( $definition['args'] ) ? $definition['args'] : [];
		foreach ( [ 'rewrite_slug', 'query_var_slug' ] as $slug_arg ) {
			$slug = (string) ( $args[ $slug_arg ] ?? '' );
			if ( $slug !== '' && in_array( $slug, Config::reservedSlugs(), true ) ) {
				$errors[] = sprintf(
					/* translators: %s: slug value. */
					__( 'The slug "%s" is reserved and would conflict with WordPress.', 'wp-powersuite' ),
					$slug
				);
			}
		}

		return $errors;
	}
}
