<?php
/**
 * Class for a trackable item.
 *
 * @package Ealab\Deployer
 */

namespace EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking;

use EaLab\WP_Deployer\vendor\Dakwamine\Component\ComponentBasedObject;
use EaLab\WP_Deployer\vendor\Dakwamine\Component\Exception\UnmetDependencyException;
use EaLab\WP_Deployer\vendor\Dakwamine\Component\RootDependencyDefinition;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\WpDeployException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\ItemValueHandler\ItemValueHandlerBase;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\ItemValueHandler\ItemValueHandlerManager;

/**
 * Class for a trackable item.
 *
 * This contains a collection of one or more dependent values (fields) for a given item.
 * Roughly, a field is a metadata or data column in a database table.
 */
class TrackableItem extends ComponentBasedObject {

	use FieldStorageTrait;

	/**
	 * The list of TrackableItem instances that this item depends on.
	 *
	 * @var TrackableItem[]
	 */
	protected array $depends_on = array();

	/**
	 * Tells if this item is excluded from export/import.
	 *
	 * This item will still be tracked in the database, but it will not be exported nor imported.
	 *
	 * @var bool True if this item is excluded from export/import, false otherwise.
	 */
	private bool $excluded = false;

	/**
	 * Whether this item has valid dependencies.
	 *
	 * Invalid items should not be exported, nor imported.
	 *
	 * @var bool
	 */
	protected bool $has_valid_dependencies = true;

	/**
	 * The item value handler manager.
	 *
	 * @var ItemValueHandlerManager|null
	 */
	protected ?ItemValueHandlerManager $item_value_handler_manager;

	/**
	 * The local ID of this trackable item.
	 *
	 * Use the getLocalId() method instead of accessing this property directly.
	 *
	 * @var mixed|null
	 */
	private $local_id;

	/**
	 * Values from the database if this TrackableItem was loaded from it.
	 *
	 * When the item is new, this is empty.
	 *
	 * This is useful to check existing values before saving
	 * new values on import.
	 *
	 * @var array|null
	 */
	private ?array $original_database_field_values = null;

	/**
	 * The name of the plugin that owns this item, in the format used by activate_plugin().
	 *
	 * @var string
	 */
	private string $plugin = '';

	/**
	 * The list of raw data, keyed by field name.
	 *
	 * Useful for value handlers that need to access the raw data of its container.
	 *
	 * @var array<string,mixed>
	 */
	private array $raw_data_fields = array();

	/**
	 * The list of raw metadata, keyed by field name.
	 *
	 * Useful for value handlers that need to access the raw metadata of its container.
	 *
	 * @var array<string,mixed>
	 */
	private array $raw_metadata_fields = array();

	/**
	 * The registry containing this item.
	 *
	 * @var TrackableItemCollection
	 */
	private TrackableItemCollection $registry;

	/**
	 * The screen label formatter manager.
	 *
	 * @var ScreenLabelFormatterManager|null
	 */
	private ?ScreenLabelFormatterManager $screen_label_formatter_manager = null;

	/**
	 * UUID assigned to this item.
	 *
	 * @var string
	 */
	private string $uuid;

	/**
	 * Adds a dependent item to this trackable item.
	 *
	 * Dependent items will load before this instance.
	 *
	 * @param TrackableItem $depends_on The item which is required by this instance.
	 */
	public function addItemDependency( TrackableItem $depends_on ): void {
		$this->depends_on[ $depends_on->getFullItemId() ] = $depends_on;
	}

	/**
	 * Builds the export data for this item.
	 *
	 * @return array The export data.
	 *
	 * @throws WpDeployException If the ID of dependencies could not be retrieved or other error.
	 */
	public function buildExportData(): array {
		$data = array();

		$data['fields'] = $this->getFieldValues( ItemValueHandlerBase::OPERATION__EXPORT );

		// Dependencies.
		$data['depends_on'] = array();

		foreach ( $this->depends_on as $item ) {
			$data['depends_on'][] = $item->getFullItemId();
		}

		$data['plugin'] = $this->plugin;

		// This is the same value as the one used in the tracking metadata table.
		// Added here for convenience.
		$data['uuid'] = $this->uuid;

		// This helps the importer and other systems to know which tracker to use
		// upon receiving the data.
		$data['tracker'] = $this->registry->getItemTracker()->getTrackerId();

		// This is used to check if the item can be imported.
		$data['tracker_config_hash'] = $this->registry->getItemTracker()->getConfigHash();

		return $data;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDependencyDefinitions(): array {
		$definitions   = parent::getDependencyDefinitions();
		$definitions[] = new RootDependencyDefinition( ItemValueHandlerManager::class, $this->item_value_handler_manager );
		$definitions[] = new RootDependencyDefinition( ScreenLabelFormatterManager::class, $this->screen_label_formatter_manager );

		return $definitions;
	}

	/**
	 * Gets the values of this trackable item.
	 *
	 * @param int           $operation See ItemValueHandlerBase::OPERATION__* constants.
	 * @param callable|null $progress_callback The callable called on export/import progress events.
	 * @param array|null    $options Options:
	 *     - "by_field_names", ?string[]: Tells which fields must be retrieved. Leave empty to return all.
	 *       Short and long field names are supported. Note: when using short field names,
	 *       we don't distinguish between metadata and data field names if they share the same name.
	 *     - "short_field_name_keys", ?boolean: Set to true to get short field names as keys. Defaults to false for
	 *     long names.
	 *
	 * @return array The values, keyed by full field name, or short if set in the options.
	 *
	 * @throws WpDeployException Could not get the values.
	 */
	public function getFieldValues( int $operation, ?callable $progress_callback = null, ?array $options = null ): array {
		$values = array();

		$by_field_names = $options['by_field_names'] ?? array();

		$use_short_field_name_keys = $options['short_field_name_keys'] ?? false;

		foreach ( $this->fields as $field_name => $value_handler ) {
			if ( ! empty( $by_field_names ) ) {
				if ( ! in_array( $field_name, $by_field_names, true )
					&& ! in_array( $value_handler->getShortFieldName(), $by_field_names, true ) ) {
					continue;
				}
			}

			$key = $use_short_field_name_keys ? $value_handler->getShortFieldName() : $field_name;

			$values[ $key ] = $value_handler->getValue( $operation, $progress_callback );
		}

		return $values;
	}

	/**
	 * Gets the full item ID. It's in the form of "tracker_id.uuid".
	 *
	 * @return string The full item ID.
	 *
	 * @throws WpDeployException If the ID could not be retrieved.
	 */
	public function getFullItemId(): string {
		return $this->registry->getItemTracker()->getTrackerId() . '.' . $this->getUuid();
	}

	/**
	 * Gets the local ID of this trackable item.
	 *
	 * It may return null if the item is not yet imported or has been deleted.
	 *
	 * @return mixed|null The local ID. Null if not yet imported or deleted.
	 *
	 * @throws WpDeployException If the ID could not be retrieved due to a technical error.
	 */
	public function getLocalId() {
		if ( ! isset( $this->local_id ) ) {
			$this->local_id = $this->getRegistry()->getItemTracker()->getLocalIdFromUuid( $this->getUuid() );
		}

		return $this->local_id;
	}

	/**
	 * Gets the original database field values.
	 *
	 * Should be set only if this item was loaded from the database.
	 *
	 * @return array|null The field values from the database, or null if it's a new item.
	 */
	public function getOriginalDatabaseFieldValues(): ?array {
		return $this->original_database_field_values ?? null;
	}

	/**
	 * Gets the raw field data.
	 *
	 * Useful for value handlers that need to access the raw data of its container.
	 * It should be safe to call this method from the value handlers of this item.
	 *
	 * Fields are indexed by short name (without the tracker ID).
	 *
	 * @return array<string,mixed>
	 */
	public function getRawDataFields(): array {
		return $this->raw_data_fields;
	}

	/**
	 * Gets the raw field metadata.
	 *
	 * Useful for value handlers that need to access the raw metadata of its container.
	 * It should be safe to call this method from the value handlers of this item.
	 *
	 * Fields are indexed by short name (without the tracker ID).
	 *
	 * @return array<string,mixed>
	 */
	public function getRawMetadataFields(): array {
		return $this->raw_metadata_fields;
	}

	/**
	 * Gets the registry containing this item.
	 *
	 * @return TrackableItemCollection The registry.
	 */
	public function getRegistry(): TrackableItemCollection {
		return $this->registry;
	}

	/**
	 * Gets the screen label for this item.
	 *
	 * @return string The screen label.
	 *
	 * @throws WpDeployException If the label could not be retrieved.
	 */
	public function getScreenLabel(): string {
		try {
			return $this->screen_label_formatter_manager->formatFromTrackableItem( $this );
		} catch ( UnmetDependencyException | WpDeployException $e ) {
			// At least attempt to return the UUID.
			return $this->getUuid();
		}
	}

	/**
	 * Gets the UUID.
	 *
	 * @param bool $as_full_uuid Whether to return the full UUID (with tracker ID prepended) or not.
	 *   Defaults to false.
	 *
	 * @return string The UUID.
	 *
	 * @throws WpDeployException If the UUID is not set.
	 */
	public function getUuid( bool $as_full_uuid = false ): string {
		if ( empty( $this->uuid ) ) {
			throw new WpDeployException( 'The UUID is not set on a trackable item instance.' );
		}

		if ( $as_full_uuid ) {
			return $this->getRegistry()->getItemTracker()->getTrackerId() . '.' . $this->uuid;
		}

		return $this->uuid;
	}

	/**
	 * Tells if this item has dependency issues.
	 *
	 * @return bool True if OK, false otherwise.
	 */
	public function hasValidDependencies(): bool {
		return $this->has_valid_dependencies;
	}

	/**
	 * Tells if this item is excluded from export/import.
	 *
	 * @return bool True if this item is excluded from export/import, false otherwise.
	 */
	public function isExcludedFromExportImport(): bool {
		return $this->excluded;
	}

	/**
	 * Process the dependencies for this item.
	 *
	 * This will call the processDependencies() method on all the value handlers.
	 *
	 * @param array<string,TrackableItem> $already_processed_list The list of items that have already been processed.
	 *     Use this to avoid processing the same item multiple times. This value is modified by reference.
	 *
	 * @return array<string,TrackableItem> The list of items that have been processed, keyed by full UUID.
	 *
	 * @throws WpDeployException If an expected dependency is missing.
	 */
	public function processDependencies( array &$already_processed_list = array() ): array {
		// Avoid processing the same item multiple times.
		if ( isset( $already_processed_list[ $this->getUuid() ] ) ) {
			return array();
		}

		// Add this item to the list of processed items.
		$already_processed_list[ $this->getUuid() ] = $this;

		if ( $this->excluded ) {
			// Do not look for dependencies on excluded items.
			// If we ever want to include them, we have to make sure all item value handlers set
			// properly their values with setValue even if the item is excluded.
			// See OptionItemValueHandler::init() for an example of setValue not called.
			return array();
		}

		$my_dependencies = array();

		/** @var ItemValueHandlerBase[] $value_handlers */
		$value_handlers = $this->getComponentsByClassName( ItemValueHandlerBase::class );

		try {
			foreach ( $value_handlers as $value_handler ) {
				$my_dependencies = array_merge( $my_dependencies, $value_handler->processDependencies() );
			}
		} catch ( WpDeployException $e ) {
			$this->setDependencyValidityStatus( false );
			throw $e;
		}

		return $my_dependencies;
	}

	/**
	 * Resets the values of this trackable item.
	 */
	public function resetValues(): void {
		$this->removeComponentsByClassName( ItemValueHandlerBase::class );

		$this->setDependencyValidityStatus( true );
	}

	/**
	 * Sets the dependency validity status.
	 *
	 * @param bool $valid True if OK, false otherwise.
	 *
	 * @return $this The trackable item.
	 */
	public function setDependencyValidityStatus( bool $valid ): TrackableItem {
		$this->has_valid_dependencies = $valid;

		return $this;
	}

	/**
	 * Sets the exclusion status of this trackable item.
	 *
	 * @param bool $excluded Whether this trackable item is excluded from the tracking.
	 */
	public function setExcludedFromExportImport( bool $excluded = true ): void {
		$this->excluded = $excluded;
	}

	/**
	 * Sets the local ID of the trackable item.
	 *
	 * Use this when the database assigned the local ID.
	 *
	 * @param mixed $local_id The local ID assigned by the database, or the composite local ID as string.
	 */
	public function setLocalId( $local_id ): void {
		$this->local_id = $local_id;
	}

	/**
	 * Sets the original database field values.
	 *
	 * Should be set only if this item was loaded from the database.
	 *
	 * @param array $tmd_data_field_values The field values from the database.
	 *
	 * @return \EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\TrackableItem Item for chaining.
	 */
	public function setOriginalDatabaseFieldValues( array $tmd_data_field_values ): TrackableItem {
		$this->original_database_field_values = $tmd_data_field_values;

		return $this;
	}

	/**
	 * Sets the raw data fields attached to this item.
	 *
	 * @param array<string,mixed> $item_raw_data The raw field values.
	 *
	 * @return $this The trackable item.
	 */
	public function setRawDataFields( array $item_raw_data ): TrackableItem {
		$this->raw_data_fields = $item_raw_data;

		return $this;
	}

	/**
	 * Sets the raw data fields attached to this item.
	 *
	 * @param array<string,mixed> $item_raw_metadata The raw metadata field values.
	 *
	 * @return $this The trackable item.
	 */
	public function setRawMetadataFields( array $item_raw_metadata ): TrackableItem {
		$this->raw_metadata_fields = $item_raw_metadata;

		return $this;
	}

	/**
	 * Sets the plugin that owns this item.
	 *
	 * @param string $plugin The plugin name, as used by activate_plugin() (typically "plugin/plugin.php").
	 *
	 * @return $this The trackable item.
	 */
	public function setPlugin( string $plugin ): TrackableItem {
		$this->plugin = $plugin;

		return $this;
	}

	/**
	 * Sets the registry containing this item.
	 *
	 * @param TrackableItemCollection $registry The registry containing this item.
	 *
	 * @return TrackableItem The trackable item.
	 */
	public function setRegistry( TrackableItemCollection $registry ): TrackableItem {
		$this->registry = $registry;

		return $this;
	}

	/**
	 * Sets the UUID for this item.
	 *
	 * @param string $uuid The UUID for this item.
	 *
	 * @return TrackableItem The trackable item.
	 */
	public function setUuid( string $uuid ): TrackableItem {
		$this->uuid = $uuid;

		return $this;
	}

}
