<?php
/**
 * Variable source registry.
 *
 * @package 100Prints
 */

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

class Hundred_Prints_Variable_Source_Registry {

	/**
	 * Registered source objects keyed by source key.
	 *
	 * @var array<string,Hundred_Prints_Variable_Source_Interface>
	 */
	private $sources = [];

	/**
	 * Alias map from legacy keys to canonical keys.
	 *
	 * @var array<string,string>
	 */
	private $aliases = [];

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->register_builtin_sources();

		/**
		 * Register first-class variable source objects.
		 *
		 * @param Hundred_Prints_Variable_Source_Registry $registry Registry instance.
		 */
		do_action( '100prints_register_variable_sources', $this );
	}

	/**
	 * Register a source object.
	 *
	 * @param Hundred_Prints_Variable_Source_Interface $source  Source object.
	 * @param array                                    $aliases Optional alias keys.
	 * @return self
	 */
	public function register( Hundred_Prints_Variable_Source_Interface $source, array $aliases = [] ) {
		$key = sanitize_key( $source->get_key() );
		if ( '' === $key ) {
			return $this;
		}

		$this->sources[ $key ] = $source;

		foreach ( $aliases as $alias ) {
			$alias = sanitize_key( $alias );
			if ( '' !== $alias ) {
				$this->aliases[ $alias ] = $key;
			}
		}

		return $this;
	}

	/**
	 * Get all canonical source objects.
	 *
	 * @return array
	 */
	public function get_sources() {
		return $this->sources;
	}

	/**
	 * Get a source object by canonical key or alias.
	 *
	 * @param string $source_key Source key.
	 * @return Hundred_Prints_Variable_Source_Interface|null
	 */
	public function get_source( $source_key ) {
		$source_key = sanitize_key( $source_key );
		$source_key = isset( $this->aliases[ $source_key ] ) ? $this->aliases[ $source_key ] : $source_key;

		return isset( $this->sources[ $source_key ] ) ? $this->sources[ $source_key ] : null;
	}

	/**
	 * Get editor-facing source metadata.
	 *
	 * @return array
	 */
	public function get_editor_definitions() {
		$definitions = [];

		foreach ( $this->sources as $source ) {
			$definitions[] = [
				'label'  => $source->get_label(),
				'value'  => $source->get_key(),
				'fields' => $source->get_fields(),
				'editorData' => method_exists( $source, 'get_editor_data' ) ? $source->get_editor_data() : [],
			];
		}

		return $definitions;
	}

	/**
	 * Sanitize one source-specific mapping rule.
	 *
	 * @param array $rule Raw mapping rule.
	 * @return array
	 */
	public function sanitize_rule( array $rule ) {
		$rule = $this->normalize_legacy_rule( $rule );
		$source_key = isset( $rule['source'] ) ? sanitize_key( $rule['source'] ) : 'manual';
		$source = $this->get_source( $source_key );

		if ( $source ) {
			return $source->sanitize_rule( $rule );
		}

		$sanitized = [
			'source' => $source_key,
		];

		foreach ( $rule as $key => $value ) {
			$key = sanitize_key( $key );
			if ( '' === $key || 'source' === $key ) {
				continue;
			}

			$sanitized[ $key ] = is_array( $value ) ? map_deep( $value, 'sanitize_text_field' ) : sanitize_text_field( wp_unslash( $value ) );
		}

		return $sanitized;
	}

	/**
	 * Resolve one mapping rule.
	 *
	 * @param array $rule    Mapping rule.
	 * @param array $context Runtime context.
	 * @return mixed
	 */
	public function resolve_rule( array $rule, array $context ) {
		$rule = $this->normalize_legacy_rule( $rule );
		$source_key = isset( $rule['source'] ) ? sanitize_key( $rule['source'] ) : 'manual';
		$source = $this->get_source( $source_key );

		if ( ! $source ) {
			return '';
		}

		return $source->resolve( $source->sanitize_rule( $rule ), $context );
	}

	/**
	 * Build shortcode/default mappings as the registry's opinion, not shortcode logic.
	 *
	 * @param array $variables Template variable names.
	 * @return array
	 */
	public function get_default_variable_map( array $variables ) {
		$variable_map = [];

		foreach ( $variables as $variable ) {
			$variable = sanitize_text_field( $variable );
			if ( '' === $variable ) {
				continue;
			}

			$variable_map[] = $this->get_default_rule_for_variable( $variable );
		}

		return $variable_map;
	}

	/**
	 * Sanitize a variable mapping field value based on schema.
	 *
	 * @param array $field Field schema.
	 * @param mixed $value Raw value.
	 * @return mixed
	 */
	public static function sanitize_field_value( array $field, $value ) {
		$field_type = isset( $field['type'] ) ? sanitize_key( $field['type'] ) : 'text';

		if ( 'manual' === $field_type ) {
			return is_array( $value ) ? map_deep( $value, 'sanitize_text_field' ) : [];
		}

		if ( is_array( $value ) ) {
			return map_deep( $value, 'sanitize_text_field' );
		}

		$value = wp_unslash( $value );

		switch ( $field_type ) {
			case 'select':
				$allowed_values = [];
				if ( ! empty( $field['options'] ) && is_array( $field['options'] ) ) {
					foreach ( $field['options'] as $option ) {
						if ( is_array( $option ) && isset( $option['value'] ) ) {
							$allowed_values[] = sanitize_key( $option['value'] );
						}
					}
				}

				$value = sanitize_key( $value );
				return in_array( $value, $allowed_values, true ) ? $value : ( isset( $field['default'] ) ? $field['default'] : '' );
			case 'url':
				return esc_url_raw( trim( sanitize_text_field( $value ) ) );
			case 'number':
				return is_numeric( $value ) ? 0 + $value : ( isset( $field['default'] ) ? $field['default'] : 0 );
			case 'checkbox':
				return ! empty( $value );
			case 'textarea':
				return sanitize_textarea_field( $value );
			case 'text':
			default:
				return sanitize_text_field( $value );
		}
	}

	/**
	 * Register core-owned sources.
	 */
	private function register_builtin_sources() {
		$this->register( new Hundred_Prints_Variable_Source_Current_User() );
		$this->register( new Hundred_Prints_Variable_Source_Post_Data() );
		$this->register( new Hundred_Prints_Variable_Source_Site_Info(), [ 'site_settings' ] );
		$this->register( new Hundred_Prints_Variable_Source_System(), [ 'system_date', 'system_time' ] );
		$this->register( new Hundred_Prints_Variable_Source_Query_Param() );
		$this->register( new Hundred_Prints_Variable_Source_Request_Context() );
		$this->register( new Hundred_Prints_Variable_Source_Static() );
		$this->register( new Hundred_Prints_Variable_Source_Form() );
	}

	/**
	 * Convert older saved mapping keys into current source object rules.
	 *
	 * @param array $rule Mapping rule.
	 * @return array
	 */
	private function normalize_legacy_rule( array $rule ) {
		$source_key = isset( $rule['source'] ) ? sanitize_key( $rule['source'] ) : 'manual';

		$legacy_rules = [
			'current_user_display_name' => [ 'source' => 'current_user', 'userField' => 'display_name' ],
			'current_user_email'        => [ 'source' => 'current_user', 'userField' => 'user_email' ],
			'current_user_id'           => [ 'source' => 'current_user', 'userField' => 'user_id' ],
			'site_name'                 => [ 'source' => 'site_info', 'siteField' => 'name' ],
			'site_url'                  => [ 'source' => 'site_info', 'siteField' => 'url' ],
			'system_date'               => [ 'source' => 'system', 'systemField' => 'current_date' ],
			'system_time'               => [ 'source' => 'system', 'systemField' => 'current_time' ],
		];

		if ( isset( $legacy_rules[ $source_key ] ) ) {
			return array_merge( $rule, $legacy_rules[ $source_key ] );
		}

		if ( isset( $this->aliases[ $source_key ] ) ) {
			$rule['source'] = $this->aliases[ $source_key ];
		}

		return $rule;
	}

	/**
	 * Get the registry's default mapping for one variable.
	 *
	 * @param string $variable Variable name.
	 * @return array
	 */
	private function get_default_rule_for_variable( $variable ) {
		$normalized = strtolower( preg_replace( '/[^a-z0-9]+/', '_', $variable ) );

		if ( in_array( $normalized, [ 'name', 'full_name', 'fullname', 'display_name' ], true ) ) {
			$rule = [ 'key' => $variable, 'source' => 'current_user', 'userField' => 'display_name' ];
		} elseif ( in_array( $normalized, [ 'email', 'user_email' ], true ) ) {
			$rule = [ 'key' => $variable, 'source' => 'current_user', 'userField' => 'user_email' ];
		} elseif ( in_array( $normalized, [ 'date', 'created_at' ], true ) ) {
			$rule = [ 'key' => $variable, 'source' => 'system', 'systemField' => 'current_date_long' ];
		} else {
			$rule = [ 'key' => $variable, 'source' => 'static', 'value' => '[' . $variable . ']' ];
		}

		/**
		 * Filter the default mapping rule used by fallback renderers.
		 *
		 * @param array                                   $rule     Mapping rule.
		 * @param string                                  $variable Variable name.
		 * @param Hundred_Prints_Variable_Source_Registry $registry Registry instance.
		 */
		return apply_filters( '100prints_default_variable_mapping_rule', $rule, $variable, $this );
	}
}
