<?php
/**
 * Importer entry point.
 *
 * @package Ealab\Deployer
 */

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

use EaLab\WP_Deployer\vendor\Dakwamine\Component\ComponentBasedObject;
use EaLab\WP_Deployer\vendor\Dakwamine\Component\ComponentBucketType;
use EaLab\WP_Deployer\vendor\Dakwamine\Component\RootDependencyDefinition;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\WpDeployException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\File\KeyValueFile;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\Config\ConfigProviderManagerBase;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\Config\Exclusions\ExclusionsConfigProviderManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\Config\TransmutationHandlerShortcodeReferences\TransmutationHandlerShortcodeReferencesConfigProviderManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\Config\ValueHandlerOption\ValueHandlerOptionConfigProviderManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\ItemTracker\ItemTrackerBase;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\SqlChangesTracker;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\TrackerConfig\TrackerConfigProviderManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\TrackingManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\Date;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\FileManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\InvalidValue;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\Plugin;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\Verbosity;
use Throwable;

/**
 * Importer entry point.
 */
class Importer extends ComponentBasedObject {

	/**
	 * The file manager for WP_Deployer.
	 *
	 * @var FileManager|null
	 */
	private ?FileManager $file_manager = null;

	/**
	 * The tracker config provider manager.
	 *
	 * @var TrackerConfigProviderManager|null
	 */
	private ?TrackerConfigProviderManager $tracker_config_provider_manager = null;

	/**
	 * The tracking manager.
	 *
	 * @var TrackingManager|null
	 */
	private ?TrackingManager $tracking_manager = null;

	/**
	 * Counts the items to import, unselected dependencies included.
	 *
	 * @param string     $subdirectory The bundle ID.
	 * @param array|null $item_full_uuids The items selected for import. If empty, all items are selected.
	 *
	 * @return array<string,mixed> Stats:
	 *     - "dependencies_added" (int): Dependencies which were not selected for import, that will be imported.
	 *     - "input" (int): The count of the $item_full_uuids in argument.
	 *     - "total" (int): Total count, dependencies included.
	 *     - "complete_full_uuid_list" (array<string>): The complete full UUID list.
	 *
	 * @throws WpDeployException Unsolvable error.
	 */
	public function countImportItems( string $subdirectory, ?array $item_full_uuids = null ): array {
		if ( null === $item_full_uuids ) {
			// Get all the item UUIDs to import.
			$item_full_uuids = $this->file_manager->getAllAvailableUuidsForImport( $subdirectory );

			$total = count( $item_full_uuids );

			return array(
				'complete_full_uuid_list' => array_values( $item_full_uuids ),
				'dependencies_added'      => 0,
				'input'                   => 0,
				'total'                   => $total,
			);
		}

		$input = count( $item_full_uuids );

		// Get the expanded list.
		$expanded_list = $this->expandImportListToDependencies( $subdirectory, $item_full_uuids );

		$total = count( $expanded_list );

		return array(
			'complete_full_uuid_list' => $expanded_list,
			'dependencies_added'      => $total - $input,
			'input'                   => $input,
			'total'                   => $total,
		);
	}

	/**
	 * Call this when finalizing an import of a bundle.
	 *
	 * This will set metadata and such.
	 *
	 * @param string        $subdirectory The bundle subdirectory, aka ID.
	 * @param callable|null $progress_callback The progress callback for import.
	 *
	 * @throws WpDeployException Bundle errors.
	 * @throws Throwable Unmanaged errors.
	 */
	public function finalizeBundleImport( string $subdirectory, callable &$progress_callback = null ): void {
		$progress_callback = $progress_callback ?? static function ( array $progress ) {
		};

		// Set the imported date.
		$local_state = $this->file_manager->loadDeploymentBundleMetadata( $subdirectory, 'local-state.json' ) ?? new KeyValueFile();
		$local_state->setValue( 'imported_date', Date::getCurrentDateTime()->format( Date::ISO_MILLISECONDS ) );
		$this->file_manager->saveDeploymentBundleMetadata( $subdirectory, 'local-state.json', $local_state );

		$progress_callback( array( 'message_line' => sprintf( _dgettext( 'deployer-core', 'Import of %s done.' ), $subdirectory ) ) );
	}

	/**
	 * {@inheritDoc}
	 */
	public function getDependencyDefinitions(): array {
		$definitions   = parent::getDependencyDefinitions();
		$definitions[] = new RootDependencyDefinition( FileManager::class, $this->file_manager );
		$definitions[] = new RootDependencyDefinition( TrackerConfigProviderManager::class, $this->tracker_config_provider_manager );
		$definitions[] = new RootDependencyDefinition( TrackingManager::class, $this->tracking_manager );

		return $definitions;
	}

	/**
	 * Gets the plugins which must be activated before importing the given items.
	 *
	 * @param array $items_to_import The items to import.
	 *
	 * @return string[]|null The plugins to activate. Null if the active_plugins option item is not found.
	 *
	 * @throws WpDeployException Error while getting the plugins to activate.
	 */
	private function getExpectedActivePluginsFromOptions( array $items_to_import ): ?array {
		// Find the options item which contains the list of activated plugins.
		foreach ( $items_to_import as $full_uuid => $item ) {
			list( $tracker_id ) = explode( '.', $full_uuid, 2 );

			if ( 'options' !== $tracker_id ) {
				continue;
			}

			if ( 'active_plugins' !== $item['fields'][ $tracker_id . '.option_name' ] ) {
				continue;
			}

			$active_plugins = maybe_unserialize( $item['fields'][ $tracker_id . '.option_value' ] );

			if ( ! is_array( $active_plugins ) ) {
				throw new WpDeployException( 'The "active_plugins" option is not an array.' );
			}

			return $active_plugins;
		}

		return null;
	}

	/**
	 * Gets the item tracker of the item with the given UUID.
	 *
	 * @param string $item_full_uuid The full UUID of the item.
	 *
	 * @return ItemTrackerBase|null The item tracker. Null if not found.
	 */
	private function getItemTrackerOfItem( string $item_full_uuid ): ?ItemTrackerBase {
		list( $item_tracker_id ) = explode( '.', $item_full_uuid, 2 );

		return $this->tracking_manager->getItemTrackerByTrackerId( $item_tracker_id );
	}

	/**
	 * Gets the plugins which are required before importing the given items.
	 *
	 * @param array $items_to_import The items to import.
	 *
	 * @return array The plugins to activate.
	 */
	private function getRequiredPluginsForImport( array $items_to_import = array() ): array {
		$plugins = array();

		foreach ( $items_to_import as $item ) {
			if ( empty( $item['plugin'] ) ) {
				// Based on WP Core.
				continue;
			}

			$plugins[ $item['plugin'] ] = $item['plugin'];
		}

		return $plugins;
	}

	/**
	 * Gets the trackers which are required before importing the given items.
	 *
	 * @param array<string,array<string,mixed>> $items_to_import The items to import.
	 *
	 * @return array<string> The trackers required by the import.
	 */
	private function getRequiredTrackersForImport( array $items_to_import = array() ): array {
		$trackers = array();

		foreach ( $items_to_import as $item ) {
			if ( empty( $item['tracker'] ) ) {
				// Based on WP Core.
				continue;
			}

			$trackers[ $item['tracker'] ] = $item['tracker'];
		}

		return $trackers;
	}


	/**
	 * Imports the given items. It prepares the plugins, perform the import, and finalize it.
	 *
	 * @param string             $subdirectory The subdirectory where to import the items.
	 * @param array<string>|null $item_full_uuids The full UUIDs of items to import.
	 *     If null, all available items for import are imported.
	 * @param bool               $force_import Whether to force the import of the items. This will erase any existing
	 *     local modification.
	 * @param callable|null      $progress_callback The callback to call to report the progress of the import.
	 *
	 * @return array<string,string> The result of the import.
	 *
	 * @throws WpDeployException Error while importing the items.
	 * @throws Throwable Unmanaged errors.
	 */
	public function import( string $subdirectory, ?array $item_full_uuids = null, bool $force_import = false, callable $progress_callback = null ): array {
		$items_to_import = $this->prepareImport( $subdirectory, $item_full_uuids, $force_import, $progress_callback );

		// The tracker config is up-to-date. Let's go!
		$import_result = $this->importItems( $items_to_import, $force_import, $progress_callback );

		$this->finalizeBundleImport( $subdirectory );

		return $import_result;
	}

	/**
	 * Gets the expanded list of items to import.
	 *
	 * Use this to get the full list of items to import, including dependencies.
	 *
	 * This operation is costly and intensive on the file system, so it should be used only when needed.
	 *
	 * @param string $subdirectory The subdirectory where to import the items.
	 * @param array  $item_full_uuids The full UUIDs of the items to import.
	 *
	 * @return string[] The full UUIDs of the items to import, including dependencies.
	 *
	 * @throws WpDeployException Error while expanding the import list (item not found).
	 */
	public function expandImportListToDependencies( string $subdirectory, array $item_full_uuids ): array {
		$i = 0;

		$expanded_list = array();

		$to_inspect = array_values( $item_full_uuids );

		// We actually need to recount each time because the list is modified during the loop.
		// phpcs:disable Squiz.PHP.DisallowSizeFunctionsInLoops.Found
		while ( $i < count( $to_inspect ) ) {
			$current_inspected_uid = $to_inspect[ $i ];

			if ( isset( $expanded_list[ $current_inspected_uid ] ) ) {
				// Already inspected.
				// In this case, it means that the same UUID is listed multiple times in the given list.
				// It's not a serious issue, so we just skip it.
				++$i;
				continue;
			}

			$expanded_list[ $current_inspected_uid ] = $current_inspected_uid;

			// Now retrieve the dependencies of the item.
			$data = $this->file_manager->loadDeploymentItemsData( $subdirectory, array( $current_inspected_uid ) );

			if ( empty( $data ) ) {
				throw new WpDeployException( 'The item with UUID "' . $current_inspected_uid . '" is missing from the exported data.' );
			}

			foreach ( $data[ $current_inspected_uid ]['depends_on'] as $uuid ) {
				if ( isset( $expanded_list[ $uuid ] ) ) {
					// Already inspected.
					continue;
				}

				// Add the item for inspection.
				$to_inspect[] = $uuid;
			}

			++$i;
		}

		return $expanded_list;
	}

	/**
	 * Imports the given items.
	 *
	 * Note: do not import too many items at once, otherwise the memory limit may be reached.
	 *
	 * @param array<string,array> $items_to_import Items to import, keyed by their full UUID.
	 * @param bool                $force_import Whether to force the import of the items,
	 *     even if they are already imported and if they have been modified on both sides.
	 * @param callable|null       $progress_callback A callback to call on export/import events.
	 * @param array<string>       $already_processed The list of items full UUIDs already processed during the import.
	 *
	 * @return array<string,string> The import result, an array keyed by item full UUIDs and with a status as value.
	 *
	 * @throws WpDeployException Error while importing the items.
	 */
	public function importItems( array $items_to_import, bool $force_import = false, ?callable $progress_callback = null, ?array &$already_processed = array() ): array {
		// Disable the current session tracking because we will insert / update data which
		// will trigger the tracking triggers.
		SqlChangesTracker::disableCurrentSessionTracking();

		/**
		 * Array of callables to call after the import.
		 *
		 * Dev note: this may trigger unwanted changes after the import.
		 * E.g. the clear_elementor_cache_on_import will delete some DB entries
		 * such as _elementor_css postmeta, thus will update the last modification date.
		 *
		 * One would need to trick the import system to disable conflicts and imports
		 * of such data, but launch the post processor anyway.
		 *
		 * Another way to do this would be to have a system to merge tracker configs
		 * on the same tracker (posts), and just ignore the items that are modified
		 * after import. This solution would be cleaner.
		 *
		 * @var array<string,callable> $import_post_actions
		 */
		$import_post_actions = array();

		$original_progress_callback = $progress_callback ?? function ( array $progress ) {
		};

		// Wrap the original progress callback with our specific implementation.
		$progress_callback = static function ( array $progress ) use ( $original_progress_callback, &$import_post_actions ) {
			$original_progress_callback( $progress );

			// Importer::importItems() specific implementation.
			if ( is_array( $progress['add_import_post_actions'] ?? null ) ) {
				// When a same key is given, it will replace the previous callable.
				$import_post_actions = array_merge( $import_post_actions, $progress['add_import_post_actions'] );
			}
		};

		$progress_callback( array( 'total_count' => count( $items_to_import ) ) );

		$complete_import_result = array();

		$already_processed = $already_processed ?? array();

		foreach ( $items_to_import as $item_full_uuid => $item ) {
			if ( in_array( $item_full_uuid, $already_processed, true ) ) {
				// Already processed.
				continue;
			}

			$item_tracker = $this->getItemTrackerOfItem( $item_full_uuid );

			if ( null === $item_tracker ) {
				// When calling the importItem() method, if the item tracker is not found,
				// it can mean that the plugin which is responsible for this item is not activated.
				// This should not happen because we already checked the plugins before via managePluginActivation().
				// i18n: ID of a missing and expected item tracker.
				throw new WpDeployException( sprintf( _dgettext( 'deployer-core', 'No item tracker found for item: %s. Please enable the plugin that manages the missing tracker, or regenerate the bundle with the <code>active_plugins</code> option included.' ), $item_full_uuid ) );
			}

			$import_result = array();

			$item_tracker->importItem( $item_full_uuid, $item, $items_to_import, $import_result, $already_processed, $force_import, $progress_callback );

			foreach ( $import_result as $result_item_full_uuid => $result_item ) {
				if ( isset( $complete_import_result[ $result_item_full_uuid ] ) ) {
					// Ignore if the item is already listed to avoid overwriting the result.
					// This can happen if the same item is listed multiple times in the import list
					// (required by multiple items).
					// In this case, we want to keep the first result.
					// Note: in theory, the only case where the value is different between the first and subsequent
					// results is when the item is new: the first pass will say "import", the subsequent passes will
					// say "skip".
					continue;
				}

				$complete_import_result[ $result_item_full_uuid ] = $result_item;
			}
		}

		foreach ( $import_post_actions as $import_post_action ) {
			if ( ! is_callable( $import_post_action ) ) {
				// Some code has an implementation error.
				// Ignore this.
				continue;
			}

			// Call the post action callback, accompanied by the $progress_callback
			// if implementations need it.
			$import_post_action( $progress_callback );
		}

		// Allow tracking again.
		// The post actions occurring before will not trigger the tracking triggers.
		SqlChangesTracker::enableCurrentSessionTracking();

		return $complete_import_result;
	}

	/**
	 * Manages the activation of the plugins which are required for importing the given items.
	 *
	 * @param array<string,array> $items_to_import The items to import.
	 * @param callable|null       $progress_callback Progress callback for import.
	 *
	 * @throws WpDeployException Error while managing the plugin activation.
	 */
	private function managePluginActivation( array $items_to_import, ?callable $progress_callback = null ): void {
		$progress_callback = $progress_callback ?? static function ( array $progress ) {
		};

		// Require the plugin php file.
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		/*
		 * Implementation note: the current approach uses options:active_plugins to determine
		 * which plugins to activate.
		 */

		/*
		 * Note about the active_plugins option: by activating and deactivating the plugins,
		 * we will trigger the options tracking triggers. This will make the active_plugins
		 * option appear as conflicted, but with an identical content,
		 * when the import page will reload. It's unfortunately impossible to prevent this,
		 * unless we add a costly check on each SQL update attempt of the option.
		 */

		// This will get the slug form of the plugin name.
		$future_activated_plugins = $this->getRequiredPluginsForImport( $items_to_import );

		// The basename form is "plugin-folder/plugin-file.php" & "plugin-file.php".
		$plugins_to_activate_as_basename = array_map(
			fn( $slug ) => Plugin::getPluginBasenameFromSlug( $slug ),
			array_values( $future_activated_plugins )
		);

		$active_plugins = $this->getExpectedActivePluginsFromOptions( $items_to_import );

		if ( ! empty( $active_plugins ) ) {
			// The active_plugins option is in the import list.
			$progress_callback( array( 'message_line' => _dgettext( 'deployer-core', 'The active_plugins option is in the import list. The plugins will be activated and deactivated accordingly.' ) ) );

			// We must make sure that the items which will be imported
			// are not in conflict with the future active plugin list.
			if ( ! empty( array_diff( $plugins_to_activate_as_basename, $active_plugins ) ) ) {
				$message = _dgettext( 'deployer-core', 'The bundle contains the future active plugins list, but it is not compatible with the items to import.' );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}

			// Already activated plugins will be ignored, so it should be safe to work on the full list.
			$future_activated_plugins = $active_plugins;

			$get_current_active_plugins = static function () use ( &$progress_callback ): array {
				// Get the updated active_plugins option value.
				$current_active_plugins = get_option( 'active_plugins', new InvalidValue() );

				if ( $current_active_plugins instanceof InvalidValue ) {
					$message = _dgettext( 'deployer-core', 'Cannot get the active plugins.' );

					$progress_callback( array( 'message_line' => $message ) );

					throw new WpDeployException( $message );
				}

				return $current_active_plugins;
			};

			$get_plugins_to_deactivate = static function () use ( $future_activated_plugins, &$get_current_active_plugins ) {
				return array_diff( $get_current_active_plugins(), $future_activated_plugins );
			};

			// Remove the plugins which are not needed anymore.
			$plugins_to_uninstall = $get_plugins_to_deactivate();

			$list_stringified = empty( $plugins_to_uninstall ) ? _dgettext( 'deployer-core', 'None.' ) : implode( ', ', $plugins_to_uninstall );
			$progress_callback(
				array(
					'message_line'           => sprintf( _dgettext( 'deployer-core', 'Plugins that will be deactivated: %s' ), $list_stringified ),
					'message_line_verbosity' => Verbosity::VERBOSITY_DEACTIVATED,
				)
			);

			deactivate_plugins( $plugins_to_uninstall );

			// Check if the plugins have been really uninstalled.
			$plugins_to_uninstall = $get_plugins_to_deactivate();

			if ( ! empty( $plugins_to_uninstall ) ) {
				$message = _dgettext( 'deployer-core', 'Tried to deactivate plugins but they are still active.' );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}

			$plugins_to_activate_real = array_diff( $future_activated_plugins, $get_current_active_plugins() );
			$list_stringified         = empty( $plugins_to_activate_real ) ? _dgettext( 'deployer-core', 'None.' ) : implode( ', ', $plugins_to_activate_real );
			$progress_callback(
				array(
					'message_line'           => sprintf( _dgettext( 'deployer-core', 'Plugins that will be activated: %s' ), $list_stringified ),
					'message_line_verbosity' => Verbosity::VERBOSITY_DEACTIVATED,
				)
			);

			$result = activate_plugins( $plugins_to_activate_real );

			if ( $result instanceof \WP_Error ) {
				$message = _dgettext( 'deployer-core', 'A plugin could not be activated.' );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}
		}

		// Dev note: from this point, all the required plugins should be installed, activated, and ready.
		// Plugins that were updated should already have their database upgraded.
		// And if there are missing activated plugins, it means that the user should have included the
		// active_plugins option in the import list.
		$progress_callback( array( 'message_line' => _dgettext( 'deployer-core', 'Plugins check done.' ) ) );
	}

	/**
	 * Prepares the import of the given items: dependency check, plugin check, activation, etc.
	 *
	 * @param string             $subdirectory The subdirectory where to import the items.
	 * @param array<string>|null $item_full_uuids The full UUIDs of items to import.
	 *     If null, all available items for import are imported.
	 * @param bool               $force_import Whether to force the import of the items. This will erase any existing
	 *     local modification.
	 * @param callable|null      $progress_callback The callback to call to report the progress of the import.
	 *
	 * @return array<string,mixed> The items to import.
	 *
	 * @throws WpDeployException Error while importing the items.
	 */
	public function prepareImport( string $subdirectory, ?array $item_full_uuids = null, bool $force_import = false, callable $progress_callback = null ): array {
		if ( null === $item_full_uuids ) {
			// Get all the item UUIDs to import.
			$item_full_uuids = $this->file_manager->getAllAvailableUuidsForImport( $subdirectory );
		}

		$progress_callback = $progress_callback ?? static function ( array $progress ) {
		};

		$progress_callback( array( 'message_line' => sprintf( _dgettext( 'deployer-core', 'Items to import for bundle "%1$s": %2$s.' ), $subdirectory, count( $item_full_uuids ) ) ) );

		// Ensure that the given UUIDs have their dependencies resolved.
		$expanded_list = $this->expandImportListToDependencies( $subdirectory, $item_full_uuids );

		$progress_callback( array( 'message_line' => sprintf( _dgettext( 'deployer-core', 'Additional items required for import: %s.' ), count( $expanded_list ) - count( $item_full_uuids ) ) ) );

		$progress_callback( array( 'message_line' => sprintf( _dgettext( 'deployer-core', 'Total items to import: %s.' ), count( $expanded_list ) ) ) );

		// Load the data of the items to import.
		$items_to_import = $this->file_manager->loadDeploymentItemsData( $subdirectory, $expanded_list );

		// Make sure the installed plugins versions matches the ones in the bundle.
		$progress_callback( array( 'message_line' => _dgettext( 'deployer-core', 'Checking the required plugins...' ) ) );

		// This is to prevent issues with DB updates.
		// This same check is also done in the UI to inform the user of any mismatch.
		$bundle_content_metadata = $this->file_manager->loadDeploymentBundleMetadata( $subdirectory, 'content.json' );
		$bundle_active_plugins   = $bundle_content_metadata->getValue( 'active_plugins' ) ?? array();
		$required_plugins        = $this->getRequiredPluginsForImport( $items_to_import );
		$installed_plugins       = Plugin::getInstalledPlugins();

		$list_stringified = empty( $required_plugins ) ? _dgettext( 'deployer-core', 'None' ) : implode( ', ', $required_plugins );

		$progress_callback(
			array(
				'message_line'           => sprintf( _dgettext( 'deployer-core', 'Required plugins for import: %s.' ), $list_stringified ),
				'message_line_verbosity' => Verbosity::VERBOSITY_DEACTIVATED,
			)
		);

		foreach ( $required_plugins as $plugin_slug ) {
			// Here, we compare the plugin version in the bundle with the one installed.
			// Note: having a different plugin version does not necessarily mean that the plugin is incompatible.
			// But having different plugin versions is a suspicious situation in a typical dev > production workflow.
			if ( empty( $bundle_active_plugins[ $plugin_slug ] ) ) {
				$message = sprintf( _dgettext( 'deployer-core', 'The plugin “%s” (%s) is required for the import but is not listed in the bundle metadata.' ), $bundle_active_plugins[ $plugin_slug ]['Name'], $plugin_slug );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}

			if ( empty( $installed_plugins[ $plugin_slug ] ) ) {
				$message = sprintf( _dgettext( 'deployer-core', 'The plugin “%s” (%s) is required for the import but is not installed.' ), $bundle_active_plugins[ $plugin_slug ]['Name'], $plugin_slug );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}

			// Versions are optional in WP.
			$bundle_plugin_version    = $bundle_active_plugins[ $plugin_slug ]['Version'] ?? null;
			$installed_plugin_version = $installed_plugins[ $plugin_slug ]['Version'] ?? null;

			if ( $bundle_plugin_version !== $installed_plugin_version ) {
				$message = sprintf( _dgettext( 'deployer-core', 'The plugin “%s” (%s) is required for the import but the installed version (%s) does not match the version in the bundle (%s).' ), $bundle_active_plugins[ $plugin_slug ]['Name'], $plugin_slug, $installed_plugin_version, $bundle_plugin_version );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}
		}

		// Get the active tracker config before activating the plugins.
		// This is important because the active tracker config will be
		// updated when activating the plugins.
		$active_tracker_config_before_import = $this->tracker_config_provider_manager->getActiveConfig();

		// This will activate the plugins, if the active_plugins options is in the import set.
		// This will also update the active tracker config when having new plugins
		// due to the auto-configuration provider, but this is temporary
		// and will be rewritten when importing the bundle tracker config.
		$this->managePluginActivation( $items_to_import, $progress_callback );

		// Import the deployer config which is in the metadata, for the required trackers.
		$import_export_config  = $bundle_content_metadata->getValue( 'import_export_config', array() );
		$bundle_tracker_config = $import_export_config['tracker']['config'] ?? array();
		$required_trackers     = $this->getRequiredTrackersForImport( $items_to_import );

		foreach ( $required_trackers as $tracker ) {
			if ( empty( $bundle_tracker_config[ $tracker ] ) ) {
				throw new WpDeployException( sprintf( _dgettext( 'deployer-core', 'The tracker “%s” is required for the import but is not listed in the bundle metadata.' ), $tracker ) );
			}
		}

		$progress_callback( array( 'message_line' => _dgettext( 'deployer-core', 'Updating the tracker config...' ) ) );

		// Update the active tracker config by importing the config from the bundle.
		// Keep any configured tracker config that are not in the incoming config from the bundle.
		$this->tracker_config_provider_manager->updateCurrentConfig( false, $bundle_tracker_config );

		// Import the optional import-export config.
		// See TrackingManager::writeExportData() for details about this feature.
		$other_config_provider_manager_classes = array(
			ExclusionsConfigProviderManager::getManagerId()                              => ExclusionsConfigProviderManager::class,
			TransmutationHandlerShortcodeReferencesConfigProviderManager::getManagerId() => TransmutationHandlerShortcodeReferencesConfigProviderManager::class,
			ValueHandlerOptionConfigProviderManager::getManagerId()                      => ValueHandlerOptionConfigProviderManager::class,
		);

		foreach ( $other_config_provider_manager_classes as $manager_id => $other_config_provider_manager_class ) {
			/** @var ConfigProviderManagerBase $other_config_provider_manager */
			$other_config_provider_manager = $this->getComponentByClassName( $other_config_provider_manager_class, true, ComponentBucketType::ROOT );

			if ( ! $other_config_provider_manager instanceof $other_config_provider_manager_class ) {
				$message = sprintf( _dgettext( 'deployer-core', 'Could not instantiate a required config provider manager: %s.' ), $other_config_provider_manager_class );

				$progress_callback( array( 'message_line' => $message ) );

				throw new WpDeployException( $message );
			}

			$bundle_other_config = $import_export_config[ $manager_id ]['config'] ?? null;

			is_array( $bundle_other_config ) && $other_config_provider_manager->updateCurrentConfig( false, $bundle_other_config );
		}

		// The active tracker config is now up-to-date.
		$progress_callback( array( 'message_line' => _dgettext( 'deployer-core', 'The active tracker config is now up-to-date.' ) ) );

		// The only thing that may break the import is a different DB table structure (e.g. added column).
		// TODO: consider adding a check for the DB table structure?
		// But before importing the items, we need to make sure that the user has reviewed
		// the items to import with the up-to-date tracker config (only when not forced).
		$trackers_to_review_again = array();

		foreach ( $required_trackers as $tracker ) {
			if ( ! empty( $active_tracker_config_before_import[ $tracker ] )
				&& ( $active_tracker_config_before_import[ $tracker ] !== $bundle_tracker_config[ $tracker ] ) ) {
				// The tracker was already active before the import,
				// but the incoming config is different.
				// Reload the tracker with the new config.
				$this->tracking_manager->getItemTrackerByTrackerId( $tracker )->init( $bundle_tracker_config[ $tracker ] );

				if ( ! $force_import ) {
					// Any presented before info being not accurate or outdated,
					// the user must review it again (only when the import is not forced).
					$trackers_to_review_again[] = $tracker;
				}
			}
		}

		if ( ! empty( $trackers_to_review_again ) ) {
			$trackers_list = implode( ', ', array_map( fn( $i ) => '“' . $i . '”', $trackers_to_review_again ) );
			$message       = sprintf( _dgettext( 'deployer-core', 'One or more trackers were already active before the import, but the incoming tracker config is different. The incoming tracker config has been imported. Please review the items and import again. The updated trackers are: %s.' ), $trackers_list );

			$progress_callback( array( 'message_line' => $message ) );

			throw new WpDeployException( $message );
		}

		// From this point, the database is ready.
		// The plugins are activated, have the right version, have (hopefully) upgraded their DB tables.
		$progress_callback( array( 'message_line' => _dgettext( 'deployer-core', 'Import preparations complete.' ) ) );

		return $items_to_import;
	}
}
