<?php
/**
 * Item value handler for option values.
 *
 * @package Ealab\Deployer
 */

namespace Ealab\Deployer\Tracking\ItemValueHandler;

use Dakwamine\Component\RootDependencyDefinition;
use Ealab\Deployer\Exception\WpDeployException;
use Ealab\Deployer\Tracking\Config\ConfigCollector;
use Ealab\Deployer\Tracking\Config\ValueHandlerOption\ValueHandlerOptionConfigProviderManager;
use Ealab\Deployer\Tracking\FieldConfig;
use Ealab\Deployer\Tracking\FieldStorageTrait;
use Ealab\Deployer\Tracking\TrackableItem;
use Symfony\Component\Yaml\Yaml;

/**
 * Item value handler for option values.
 */
class OptionItemValueHandler extends ItemValueHandlerBase {
	// This item value handler uses the FieldStorageTrait to store the sub item value handler.
	// This is the same mechanism as in the TrackableItem class.
	use FieldStorageTrait;

	/**
	 * Config collector.
	 *
	 * @var ConfigCollector|null
	 */
	private ?ConfigCollector $config_collector = null;

	/**
	 * The config manager for "value_handler.option".
	 *
	 * This item value handler requires that its config is provided by a config manager.
	 * Indeed, we cannot save it directly in the tracker config because the options config
	 * are tied to plugins, not to trackers. In other words, the configs are loaded only
	 * when the plugin is loaded, not when the tracker is loaded.
	 *
	 * @var ValueHandlerOptionConfigProviderManager|null
	 */
	private ?ValueHandlerOptionConfigProviderManager $config_provider_manager = null;

	/**
	 * {@inheritDoc}
	 */
	public function getDependencyDefinitions(): array {
		$definitions = parent::getDependencyDefinitions();

		$definitions[] = new RootDependencyDefinition( ConfigCollector::class, $this->config_collector );
		$definitions[] = new RootDependencyDefinition( ValueHandlerOptionConfigProviderManager::class, $this->config_provider_manager );

		return $definitions;
	}

	/**
	 * {@inheritDoc}
	 */

	/**
	 * {@inheritDoc}
	 */
	public static function getFormBuild(): ?array {
		$build = Yaml::parse(
			<<<YAML
- load: field_label
  content: Option name field name
- type: TextField
  dataLocation: ~.option_name_field_name
  placeholder: "E.g.: option_name, meta_key."
YAML
		);

		$build[0]['content'] = __( 'Option name field name', 'deploy-sync-content' );

		$build[1]['placeholder'] = __( 'E.g.: option_name, meta_key.', 'deploy-sync-content' );

		return $build;
	}

	/**
	 * {@inheritDoc}
	 */
	public static function getHandlerBaseId(): string {
		return 'option';
	}

	/**
	 * {@inheritDoc}
	 */
	public static function getLabel(): string {
		return _dpgettext( 'deployer-core', 'item value handler', 'Option value' );
	}

	/**
	 * {@inheritDoc}
	 */
	protected function getUnprocessedValue( int $operation, ?callable $progress_callback = null ) {
		// Retrieve the value from the option_real_value pseudo field.
		if ( empty( $this->fields['option_real_value'] ) ) {
			// No value to export. This is unlikely to happen.
			throw new WpDeployException( sprintf( 'No value associated to the option: %s.', $this->full_field_name ) );
		}

		// This will "replace" the value of this instance by the value of the option_real_value pseudo field.
		// Call getValue() to get the processed value.
		return $this->fields['option_real_value']->getValue( $operation, $progress_callback );
	}

	/**
	 * {@inheritDoc}
	 */
	public function init( TrackableItem $holder_item, string $full_field_name, $value, int $origin, FieldConfig $field_config ): void {
		parent::init( $holder_item, $full_field_name, $value, $origin, $field_config );

		/**
		 * An option field is a polymorphic field. It can be a scalar, a reference, etc.
		 * What it does is to store the value in the option_real_value pseudo field (thanks to the FieldStorageTrait).
		 * And this pseudo field can be configured to use any value handler, be excluded, etc.
		 * This configuration is not stored in the tracker config, but in a dedicated config provider.
		 * See ValueHandlerOptionConfigProviderManager for more details.
		 * Start by retrieving the configuration of the value handler.
		 *
		 * The structure of the returned array is like this:
		 *
		 * array(
		 *     '<tracker_id>' => array(
		 *         '<full_field_name_of_the_field_containing_the_value>' => array(
		 *             'fields'                 => array(
		 *                 '<a_value_in_the_option_name_column>' => array(
		 *                     'exclude'       => <exclude_true_false>,
		 *                     'value_handler' => '<item_value_handler_full_id>',
		 *                     'processors'    => array(
		 *                         array(
		 *                             'id'        => '<processor_id>',
		 *                             '<any_key>' => <processor_config>,
		 *                         ),
		 *                     ),
		 *                 ),
		 *             ),
		 *         ),
		 *     ),
		 * );
		 *
		 * Explanation of the placeholders:
		 *   - <tracker_id>:
		 *       The name of the tracker (here, "options").
		 *   - <full_field_name_of_the_field_containing_the_value>:
		 *       The full field name (here, "options.option_value").
		 *   - <a_value_in_the_option_name_column>:
		 *       The name of the option (here, "admin_email", option_xyz).
		 *   - <exclude_true_false>:
		 *       Set to true to exclude this field from the export/import process.
		 *   - <item_value_handler_full_id>:
		 *       The full ID of the item value handler to use for this option (here, "scalar", "reference:options").
		 *   - <processor_id>:
		 *       ID of an item value processor which will transform the value on export and import.
		 *       Processors are optional.
		 *   - <processor_config>:
		 *       Optional config of the processor, indexed by <any_key> (they will be passed to the processor).
		 *
		 * A hook can be used to alter the configuration.
		 *
		 * Examples on how to define the value handler for a specific field:
		 *
		 * @code
		 * $value['options']['options.option_value']['fields']['admin_email']
		 *     = array( 'value_handler' => 'scalar' );
		 * $value['options']['options.option_value']['fields']['option_xyz']
		 *     = array( 'value_handler' => 'reference:options' );
		 * @endcode
		 *
		 * @var array<string,array<string,mixed>> $definitions
		 */
		$definitions = $this->config_collector->collect( $this->config_provider_manager, 'value_handler__option' );

		// Each field from each tracker may have a different configuration.
		// So we retrieve the configuration for the current tracker and the current field.
		$tracker_id             = $this->getHolder()->getRegistry()->getItemTracker()->getTrackerId();
		$option_name_field_name = $field_config->getRawConfig()['option_name_field_name'] ?? 'option_name';

		// Get the option_name of the item which contains this value handler.
		$option_name = $this->getHolder()->getRawDataFields()[ $option_name_field_name ] ?? null;

		if ( empty( $option_name ) || ! is_string( $option_name ) ) {
			throw new WpDeployException( 'The option_name field is not defined on the item which contains this value handler.' );
		}

		// Retrieve the configuration of the sub value handler, if any.
		// By default, the real value is treated as scalar.
		$sub_field_config = new FieldConfig();
		$sub_field_config->init( $definitions[ $tracker_id ][ $this->full_field_name ]['fields'][ $option_name ] ?? array() );

		if ( $sub_field_config->isExcluded() ) {
			// Bubble up the exclusion to the holder item.
			$holder_item->setExcludedFromExportImport();

			// No need to go further.
			// Dev note: if we ever want to include dependencies of excluded trackable items,
			// we will need to continue the execution of this method so that the dependencies
			// are processed.
			return;
		}

		$this->setValue(
			$holder_item,
			'option_real_value',
			$value,
			$origin,
			$sub_field_config
		);
	}

	/**
	 * {@inheritDoc}
	 */
	public static function normalizeFormSubmittedData( ?array $to_normalize = null ): ?array {
		if ( empty( $to_normalize ) ) {
			return null;
		}

		if ( empty( $to_normalize['option_name_field_name'] ) ) {
			// Invalid config.
			return null;
		}

		$normalized = parent::normalizeFormSubmittedData( $to_normalize );

		$normalized['option_name_field_name'] = $to_normalize['option_name_field_name'];

		return $normalized;
	}

	/**
	 * {@inheritDoc}
	 */
	public function processDependencies(): array {
		$found_dependencies = parent::processDependencies();

		try {
			/** @var ItemValueHandlerBase $value_handler */
			foreach ( $this->getComponentsByClassName( ItemValueHandlerBase::class ) as $value_handler ) {
				// Process the dependencies of the sub value handler (there is only one, in theory).
				$found_dependencies = array_merge( $found_dependencies, $value_handler->processDependencies() );
			}
		} catch ( WpDeployException $e ) {
			$this->getHolder()->setDependencyValidityStatus( false );
			throw $e;
		}

		return $found_dependencies;
	}
}
