<?php
/**
 * CLI for Deploy & Sync Content.
 *
 * @package EaLab\WP_Deployer
 */

namespace EaLab\WP_Deployer\includes;

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

use EaLab\WP_Deployer\vendor\Dakwamine\Component\ComponentBasedObject;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\InvalidConfigException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\WpDeployException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Import\Importer;
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\FileManager;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Utility\Verbosity;

add_action(
	'cli_init',
	static function () {
		\WP_CLI::add_command( 'wd', Cli::class );

		\WP_CLI::add_hook(
			'before_invoke:search-replace',
			static function ( $args ) {
				// Disable the tracking system before a search and replace because it triggers
				// the tracking system, and we don't want to.
				SqlChangesTracker::disableCurrentSessionTracking();

				return $args;
			}
		);

		\WP_CLI::add_hook(
			'after_invoke:search-replace',
			static function ( $args ) {
				// Enable the tracking system after a search and replace, because we
				// disabled it before invoking the command.
				SqlChangesTracker::enableCurrentSessionTracking();

				return $args;
			}
		);
	},
);

/**
 * Deploy & Sync Content commands. Type "wp help wd" for a list of available subcommands.
 */
class Cli {
	/**
	 * Attempts to fix the configuration of the tracking system.
	 *
	 * ## EXAMPLES
	 *
	 *     wp wd confix
	 *     wp wd configFix
	 *
	 * @alias confix
	 */
	public function configFix() {
		try {
			$tracker_config_provider_manager = ComponentBasedObject::getRootComponentByClassName( TrackerConfigProviderManager::class, true );

			$current_config_provider = $tracker_config_provider_manager->getCurrentConfigProvider()->collect() ?? array();

			$tracker_config_provider_manager->updateCurrentConfig( false, $current_config_provider );
		} catch ( InvalidConfigException | WpDeployException | \Throwable $e ) {
			\WP_CLI::error( __( 'Config could not be resaved. Error message: ', 'deploy-sync-content' ) . wp_strip_all_tags( $e->getMessage() ) );
		}

		\WP_CLI::success( __( 'Config resaved.', 'deploy-sync-content' ) );
	}

	/**
	 * Imports items from the file system.
	 *
	 * ## OPTIONS
	 *
	 * [<bundle-id>]
	 * : The bundle ID to import. This is the name of the directory containing the bundle.
	 * If left empty, the last bundle in alphabetical order will be used.
	 *
	 * [--force-import]
	 * : Set this to ignore any conflicting changes and import anyway.
	 * Use with caution, as this can lead to data loss.
	 *
	 * [--item-full-uuids=<item-full-uuids>]
	 * : Restricts the import to the specified items.
	 * It can be a comma-separated list of item full UUIDs.
	 * If not specified, all candidate items will be selected for import.
	 * Note: dependencies of the selected items will also be imported.
	 *
	 * [--verbose[=<verbosity-level>]]
	 * : Sets the verbose level of the output.
	 * - 0: no output at all (errors excluded).
	 * - 1: basic info.
	 * - 2: with progress bar.
	 * - 3: with status for each imported item.
	 * If set without value, value is "3". Defaults to "2" if unset.
	 *
	 * ## EXAMPLES
	 *
	 *     wp wd im 20240102152339363249
	 *     wp wd import 20240102152339363249
	 *     wp wd import 20240102152339363249 --force-import
	 *     wp wd import 20240102152339363249 --item-full-uuids="12345678-1234-1234-1234-123456789012"
	 *
	 * @alias im
	 */
	public function import( $args, $assoc_args ) {
		$verbosity = $assoc_args['verbose'] ?? Verbosity::VERBOSITY_2;
		$verbosity = Verbosity::validateVerbosityLevel( $verbosity );

		Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 )
		&& \WP_CLI::log( _dgettext( 'deployer-core', 'Import has started…' ) );

		$bundle_id = $args[0] ?? null;

		if ( empty( $bundle_id ) ) {
			Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 )
			&& \WP_CLI::log( _dgettext( 'deployer-core', 'No bundle ID specified for import. Looking for the last created bundle…' ) );

			$file_manager = ComponentBasedObject::getRootComponentByClassName( FileManager::class, true );

			$all_bundle_ids = $file_manager->getExistingExportedDataSubdirectories();

			ksort( $all_bundle_ids, SORT_NATURAL );

			$bundle_info = end( $all_bundle_ids );

			$bundle_id = $bundle_info['subdirectory'] ?? null;

			if ( empty( $bundle_info ) ) {
				Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 )
				&& \WP_CLI::error( _dgettext( 'deployer-core', 'No bundle ID found to import.' ) );

				exit;
			}

			if ( Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 ) ) {
				/** @var \DateTime|null $bundle_datetime */
				$bundle_datetime = $bundle_info['datetime'] ?? null;

				if ( ! empty( $bundle_datetime ) ) {
					$bundle_datetime_formatted = $bundle_datetime->format( ' \(Y-m-d H:i:s\ \U\T\C)' );
					\WP_CLI::log( sprintf( _dgettext( 'deployer-core', 'Found the following bundle: %1$s%2$s.' ), $bundle_id, $bundle_datetime_formatted ) );
				}
			}
		}

		$force_import = $assoc_args['force-import'] ?? false;

		if ( ! empty( $assoc_args['item-full-uuids'] ) ) {
			$item_full_uuids = explode( ',', $assoc_args['item-full-uuids'] );
			$item_full_uuids = array_filter( $item_full_uuids );
		} else {
			$item_full_uuids = null;
		}

		$progress_bar = null;

		$importer = ComponentBasedObject::getRootComponentByClassName( Importer::class, true );

		// We use this variable to prevent ticks when an item
		// was already processed. This happens when retrying.
		$processed_items = array();

		// Progress callback for the importer.
		$progress_callback = static function ( array $progress = array() ) use ( $verbosity, &$progress_bar, &$processed_items ) {
			$total_count            = $progress['total_count'] ?? null;
			$tick                   = $progress['tick'] ?? null;
			$message                = $progress['message'] ?? null;
			$message_line           = $progress['message_line'] ?? null;
			$message_line_verbosity = $progress['message_line_verbosity'] ?? Verbosity::VERBOSITY_1;
			$processed_item         = $progress['processed_item'] ?? null;

			if ( $processed_item ) {
				$is_new_processed_item = ! in_array( $processed_item, $processed_items );

				if ( $tick && ! $is_new_processed_item ) {
					// Cancel the positive tick to treat this as a message only.
					$tick = 0;
				}

				$processed_items[ $processed_item ] = $processed_item;
			}

			if ( ! empty( $message_line ) ) {
				Verbosity::verbosityIsAtLeast( $verbosity, $message_line_verbosity )
				&& \WP_CLI::log( $message_line );
			}

			if ( ! Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_2 ) ) {
				return;
			}

			if ( ! empty( $total_count ) ) {
				// Initialize the progress bar if not yet initialized.
				$progress_bar = $progress_bar ?? \WP_CLI\Utils\make_progress_bar( 'Start Importing ...', $total_count );
			}

			if ( empty( $progress_bar ) ) {
				// Not yet initialized.
				return;
			}

			// Tick 0 means we update only the message.
			( $tick || $message ) && $progress_bar->tick( $tick ?? 0, $message ? (string) $message : null );
		};

		$items_to_import = $importer->prepareImport( $bundle_id, $item_full_uuids, $force_import, $progress_callback );

		$result = $importer->importItems( $items_to_import, $force_import, $progress_callback );

		$importer->finalizeBundleImport( $bundle_id );

		Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_2 )
		&& $progress_bar && $progress_bar->finish();

		// Display the result for each item.
		Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_3 )
		&& array_walk(
			$result,
			static function ( $message, $item_full_uuid ) {
				\WP_CLI::line( sprintf( _dgettext( 'deployer-core', '%s: %s' ), $item_full_uuid, $message ) );
			}
		);

		Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 )
		&& \WP_CLI::success( _dgettext( 'deployer-core', 'Import done.' ) );
	}

	/**
	 * Prepares a synchronization by downloading the items.
	 *
	 * ## EXAMPLES
	 *
	 *     wp wd downsync
	 *
	 * @alias downsync
	 */
	public function downsync(): void {
		$verbosity = $assoc_args['verbose'] ?? Verbosity::VERBOSITY_2;
		$verbosity = Verbosity::validateVerbosityLevel( $verbosity );

		Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 )
		&& \WP_CLI::line( __( 'Started the downsync. Please wait.', 'deploy-sync-content' ) );

		try {
			$file_manager = ComponentBasedObject::getRootComponentByClassName( FileManager::class, true );

			$bundle_id = $file_manager->downloadBundleFromProd();

			if ( Verbosity::verbosityIsAtLeast( $verbosity, Verbosity::VERBOSITY_1 ) ) {
				\WP_CLI::line( __( 'Preparations done.', 'deploy-sync-content' ) );
				\WP_CLI::line( __( 'Apply the synchronization with this command:', 'deploy-sync-content' ) );
				\WP_CLI::line( '  wp wd im ' . $bundle_id );

				$routes = apply_filters( 'deploy_sync_content__routes', array() );

				if ( ! empty( $routes['synchronize'] ) ) {
					$route        = $routes['synchronize'];
					$complete_url = add_query_arg(
						array(
							'bundletoimport' => $bundle_id,
						),
						admin_url( $route['path'] ?? DEPLOY_SYNC_CONTENT__BASIC_URI . '&tab=' . $route['id'] ) );
					\WP_CLI::line( __( 'Alternatively, you can open the web UI:', 'deploy-sync-content' ) );
					\WP_CLI::line( '  ' . $complete_url );
				}
			}
		} catch ( \Throwable $e ) {
			\WP_CLI::error( __( 'Cannot prepare synchronization. Error message: ', 'deploy-sync-content' ) . wp_strip_all_tags( $e->getMessage() ) );
		}
	}

	/**
	 * Resets the last modification dates of the local trackers.
	 *
	 * This is usually needed right after a database copy on a development instance,
	 * to get rid of "Modified" false positives created by the duplication tools.
	 *
	 * ## EXAMPLES
	 *
	 *     wp wd reset
	 *     wp wd resetLastModificationDates
	 *
	 * @alias reset
	 */
	public function resetLastModificationDates(): void {
		try {
			$tracking_manager = ComponentBasedObject::getRootComponentByClassName( TrackingManager::class, true );

			$tracking_manager->resetLastModificationDates();
		} catch ( \Throwable $e ) {
			\WP_CLI::error( __( 'Cannot reset last modification dates. Error message: ', 'deploy-sync-content' ) . wp_strip_all_tags( $e->getMessage() ) );
		}

		\WP_CLI::success( __( 'Reset done.', 'deploy-sync-content' ) );
	}
}
