<?php
/**
 * Field Registry — single source of truth (wraps Schema).
 *
 * One field definition drives defaults, sanitization, validation,
 * labels, descriptions, and rendering payloads.
 *
 * @package Kutby
 * @since   2.5.0
 */

declare(strict_types=1);

namespace Kutby\ThemeOptions;

use Kutby\Sanitizers\Sanitizer as CoreSanitizer;

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

/**
 * Class FieldRegistry
 *
 * Thin facade over Schema so callers have a clear registry API.
 * Schema remains the implementation to preserve BC.
 */
final class FieldRegistry {

	/**
	 * @return array<string, array<string, array<string, mixed>>>
	 */
	public static function all(): array {
		return Schema::all();
	}

	/**
	 * @return array<string, array<string, mixed>>
	 */
	public static function defaults(): array {
		return Schema::defaults();
	}

	/**
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @return string
	 */
	public static function type( string $group, string $key ): string {
		return Schema::type( $group, $key );
	}

	/**
	 * @param string               $group  Group.
	 * @param array<string, mixed> $values Values.
	 * @return array<int, array<string, mixed>>
	 */
	public static function fields_for( string $group, array $values ): array {
		return Schema::fields_for( $group, $values );
	}

	/**
	 * Get one field definition.
	 *
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @return array<string, mixed>|null
	 */
	public static function get( string $group, string $key ): ?array {
		$all = Schema::all();
		return $all[ $group ][ $key ] ?? null;
	}

	/**
	 * Field label (Arabic).
	 *
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @return string
	 */
	public static function label( string $group, string $key ): string {
		$field = self::get( $group, $key );
		return (string) ( $field['label'] ?? $field['title'] ?? $key );
	}

	/**
	 * Field description / hint.
	 *
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @return string
	 */
	public static function description( string $group, string $key ): string {
		$field = self::get( $group, $key );
		return (string) ( $field['description'] ?? $field['hint'] ?? '' );
	}

	/**
	 * Default value for one field.
	 *
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @return mixed
	 */
	public static function default( string $group, string $key ): mixed {
		$field = self::get( $group, $key );
		return $field['default'] ?? null;
	}

	/**
	 * Sanitize one field value using schema type.
	 *
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @param mixed  $value Raw value.
	 * @return mixed
	 */
	public static function sanitize( string $group, string $key, mixed $value ): mixed {
		$type    = self::type( $group, $key );
		$default = self::default( $group, $key );

		return match ( $type ) {
			'toggle', 'checkbox' => CoreSanitizer::bool( $value ),
			'number'             => is_float( $default ) ? CoreSanitizer::float( $value ) : CoreSanitizer::int( $value ),
			'color'              => CoreSanitizer::color( (string) $value, (string) $default ),
			'url'                => CoreSanitizer::url( (string) $value ),
			'media'              => CoreSanitizer::attachment_id( $value ),
			'textarea'           => CoreSanitizer::textarea( (string) $value ),
			default              => CoreSanitizer::text( (string) $value ),
		};
	}

	/**
	 * Validate one field against schema rules.
	 *
	 * Supported rules: required, numeric, integer, boolean, email, url, enum, min, max, callback.
	 *
	 * @param string $group Group.
	 * @param string $key   Key.
	 * @param mixed  $value Value.
	 * @return true|string True or Arabic error message.
	 */
	public static function validate( string $group, string $key, mixed $value ): true|string {
		$field = self::get( $group, $key );
		if ( null === $field ) {
			return 'حقل غير معروف.';
		}

		$label = self::label( $group, $key );
		$rules = self::rules_for( $field );

		foreach ( $rules as $rule ) {
			$result = self::apply_rule( $rule, $value, $field, $label );
			if ( true !== $result ) {
				return $result;
			}
		}

		return true;
	}

	/**
	 * Validate one or more option groups. Returns Arabic error messages (empty = valid).
	 *
	 * @param array<string, mixed> $options Full options payload (pre-sanitize).
	 * @param array<int, string>   $groups  Group keys to validate (empty = all schema groups).
	 * @return array<int, string>
	 */
	public static function validate_groups( array $options, array $groups = array() ): array {
		$schema = self::all();
		$errors = array();

		if ( empty( $groups ) ) {
			$groups = array_keys( $schema );
		}

		foreach ( $groups as $group ) {
			$group = sanitize_key( (string) $group );
			if ( empty( $schema[ $group ] ) || ! is_array( $schema[ $group ] ) ) {
				continue;
			}

			$values = isset( $options[ $group ] ) && is_array( $options[ $group ] ) ? $options[ $group ] : array();

			foreach ( $schema[ $group ] as $key => $_def ) {
				$value  = $values[ $key ] ?? self::default( $group, (string) $key );
				$result = self::validate( $group, (string) $key, $value );
				if ( true !== $result ) {
					$errors[] = $result;
				}
			}
		}

		return $errors;
	}

	/**
	 * Persist via Manager after validate → sanitize (Manager) → cache → events.
	 *
	 * Canonical save entry for Admin forms and AJAX.
	 *
	 * @param array<string, mixed> $options Options (merged, pre-sanitize).
	 * @param array<int, string>   $groups  Optional group filter for validation.
	 * @param array<string, mixed> $context Extra event context (e.g. via=ajax).
	 * @return true|array<int, string> True on success, error list on failure.
	 */
	public static function store( array $options, array $groups = array(), array $context = array() ): true|array {
		$errors = self::validate_groups( $options, $groups );
		if ( ! empty( $errors ) ) {
			return $errors;
		}

		Manager::save( $options, $context );
		return true;
	}

	/**
	 * Build ordered validation rules for a field definition.
	 *
	 * @param array<string, mixed> $field Field.
	 * @return array<int, array<string, mixed>>
	 */
	private static function rules_for( array $field ): array {
		$rules = array();
		$type  = (string) ( $field['type'] ?? 'text' );

		if ( ! empty( $field['required'] ) ) {
			$rules[] = array( 'name' => 'required' );
		}

		// Explicit validate / rules keys (string, list, or map).
		$explicit = $field['validate'] ?? $field['rules'] ?? null;
		if ( is_string( $explicit ) && '' !== $explicit ) {
			$rules[] = array( 'name' => $explicit );
		} elseif ( is_array( $explicit ) ) {
			foreach ( $explicit as $k => $v ) {
				if ( is_int( $k ) && is_string( $v ) ) {
					$rules[] = array( 'name' => $v );
				} elseif ( is_string( $k ) ) {
					$rules[] = array( 'name' => $k, 'param' => $v );
				}
			}
		}

		// Type-driven rules (no duplication when already explicit).
		$names = array_column( $rules, 'name' );

		switch ( $type ) {
			case 'number':
				if ( ! in_array( 'numeric', $names, true ) ) {
					$rules[] = array( 'name' => 'numeric' );
				}
				$default = $field['default'] ?? null;
				if ( is_int( $default ) && ! in_array( 'integer', $names, true ) ) {
					$rules[] = array( 'name' => 'integer' );
				}
				break;
			case 'url':
				if ( ! in_array( 'url', $names, true ) ) {
					$rules[] = array( 'name' => 'url' );
				}
				break;
			case 'email':
				if ( ! in_array( 'email', $names, true ) ) {
					$rules[] = array( 'name' => 'email' );
				}
				break;
			case 'toggle':
			case 'checkbox':
				if ( ! in_array( 'boolean', $names, true ) ) {
					$rules[] = array( 'name' => 'boolean' );
				}
				break;
			case 'select':
				if ( ! empty( $field['options'] ) && is_array( $field['options'] ) && ! in_array( 'enum', $names, true ) ) {
					$rules[] = array( 'name' => 'enum' );
				}
				break;
			case 'media':
				if ( ! in_array( 'integer', $names, true ) ) {
					$rules[] = array( 'name' => 'integer' );
				}
				break;
		}

		if ( isset( $field['min'] ) && ! in_array( 'min', $names, true ) ) {
			$rules[] = array( 'name' => 'min', 'param' => $field['min'] );
		}
		if ( isset( $field['max'] ) && ! in_array( 'max', $names, true ) ) {
			$rules[] = array( 'name' => 'max', 'param' => $field['max'] );
		}

		if ( isset( $field['options'] ) && is_array( $field['options'] ) && ! in_array( 'enum', $names, true ) && 'select' !== $type ) {
			$rules[] = array( 'name' => 'enum' );
		}

		$callback = $field['callback'] ?? $field['validate_callback'] ?? null;
		if ( is_callable( $callback ) ) {
			$rules[] = array( 'name' => 'callback', 'param' => $callback );
		}

		return $rules;
	}

	/**
	 * Apply a single validation rule.
	 *
	 * @param array<string, mixed> $rule  Rule.
	 * @param mixed                $value Value.
	 * @param array<string, mixed> $field Field def.
	 * @param string               $label Label.
	 * @return true|string
	 */
	private static function apply_rule( array $rule, mixed $value, array $field, string $label ): true|string {
		$name  = (string) ( $rule['name'] ?? '' );
		$param = $rule['param'] ?? null;
		$empty = self::is_empty( $value );

		// Skip non-required rules when empty.
		if ( $empty && 'required' !== $name && 'boolean' !== $name ) {
			return true;
		}

		return match ( $name ) {
			'required' => $empty ? sprintf( 'حقل "%s" مطلوب.', $label ) : true,
			'numeric'  => ( is_numeric( $value ) || is_int( $value ) || is_float( $value ) )
				? true
				: sprintf( 'يجب أن يكون حقل "%s" رقمًا.', $label ),
			'integer'  => self::is_integer_value( $value )
				? true
				: sprintf( 'يجب أن يكون حقل "%s" عددًا صحيحًا.', $label ),
			'boolean'  => self::is_boolean_value( $value )
				? true
				: sprintf( 'قيمة حقل "%s" غير صالحة.', $label ),
			'email'    => ( is_email( (string) $value ) )
				? true
				: sprintf( 'البريد الإلكتروني في "%s" غير صالح.', $label ),
			'url'      => self::is_valid_url_value( (string) $value )
				? true
				: sprintf( 'الرابط في "%s" غير صالح.', $label ),
			'enum'     => self::in_enum( $value, $field )
				? true
				: sprintf( 'قيمة غير مسموحة في "%s".', $label ),
			'min'      => ( is_numeric( $value ) && (float) $value >= (float) $param )
				? true
				: sprintf( 'الحد الأدنى لحقل "%s" هو %s.', $label, (string) $param ),
			'max'      => ( is_numeric( $value ) && (float) $value <= (float) $param )
				? true
				: sprintf( 'الحد الأقصى لحقل "%s" هو %s.', $label, (string) $param ),
			'callback' => self::run_callback( $param, $value, $field, $label ),
			default    => true,
		};
	}

	/**
	 * Accept absolute URLs, site-relative paths, and in-page anchors.
	 *
	 * Theme CTAs often use values like `#kutby-grades-title` which
	 * FILTER_VALIDATE_URL rejects.
	 *
	 * @param string $value Raw URL.
	 * @return bool
	 */
	private static function is_valid_url_value( string $value ): bool {
		$value = trim( $value );
		if ( '' === $value ) {
			return true;
		}

		// In-page fragment (e.g. #section-id).
		if ( 1 === preg_match( '/^#[A-Za-z0-9_.:%-]+$/', $value ) ) {
			return true;
		}

		// Site-relative path or query.
		if ( 1 === preg_match( '/^[\/?][^\s]*$/', $value ) ) {
			return true;
		}

		return false !== filter_var( $value, FILTER_VALIDATE_URL );
	}

	/**
	 * @param mixed $value Value.
	 * @return bool
	 */
	private static function is_empty( mixed $value ): bool {
		if ( null === $value ) {
			return true;
		}
		if ( is_string( $value ) && '' === trim( $value ) ) {
			return true;
		}
		if ( is_array( $value ) && array() === $value ) {
			return true;
		}

		return false;
	}

	/**
	 * @param mixed $value Value.
	 * @return bool
	 */
	private static function is_integer_value( mixed $value ): bool {
		if ( is_int( $value ) ) {
			return true;
		}
		if ( is_string( $value ) && '' !== $value && ctype_digit( ltrim( $value, '-+' ) ) ) {
			return true;
		}
		if ( is_float( $value ) && floor( $value ) === $value ) {
			return true;
		}

		return false;
	}

	/**
	 * @param mixed $value Value.
	 * @return bool
	 */
	private static function is_boolean_value( mixed $value ): bool {
		if ( is_bool( $value ) ) {
			return true;
		}
		if ( in_array( $value, array( 0, 1, '0', '1', 'true', 'false', 'on', 'off' ), true ) ) {
			return true;
		}

		return false;
	}

	/**
	 * @param mixed                $value Value.
	 * @param array<string, mixed> $field Field.
	 * @return bool
	 */
	private static function in_enum( mixed $value, array $field ): bool {
		$options = $field['options'] ?? null;
		if ( ! is_array( $options ) ) {
			return true;
		}

		return array_key_exists( (string) $value, $options );
	}

	/**
	 * @param mixed                $callback Callback.
	 * @param mixed                $value    Value.
	 * @param array<string, mixed> $field    Field.
	 * @param string               $label    Label.
	 * @return true|string
	 */
	private static function run_callback( mixed $callback, mixed $value, array $field, string $label ): true|string {
		if ( ! is_callable( $callback ) ) {
			return true;
		}

		$result = $callback( $value, $field, $label );
		if ( true === $result || null === $result ) {
			return true;
		}
		if ( is_string( $result ) && '' !== $result ) {
			return $result;
		}

		return sprintf( 'قيمة حقل "%s" غير صالحة.', $label );
	}
}
