<?php
/**
 * Builds the data for the interactive UI.
 *
 * @package EaLab\WP_Deployer
 */

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

use EaLab\WP_Deployer\vendor\Dakwamine\Component\ComponentBasedObject;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Exception\WpDeployException;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Import\Importability;
use EaLab\WP_Deployer\vendor\Ealab\Deployer\Tracking\Config\ConfigProviderManagerBase;
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\Symfony\Component\Yaml\Yaml;

require_once __DIR__ . '/../admin/partials/component/deployment-item--label-from-item.php';

/**
 * Sets the message for the given export or import form structure.
 *
 * Works for both forms as long their structure is similar.
 *
 * @param array       $structure The structure given by deploy_sync_content__form_structure__[export|import]().
 * @param string      $message The message to set. Must be translated beforehand.
 * @param string|null $variant Optional. The variant, as one of the variants supported by React Bootstrap.
 *
 * @return array The structure with the message.
 */
function deploy_sync_content__form_structure__export_import__set_message( array $structure, string $message, ?string $variant = null ): array {
	if ( empty( $message ) ) {
		return $structure;
	}

	require_once DEPLOY_SYNC_CONTENT__PLUGIN_DIR . '/includes/helpers.php';
	$alert_message_block = deploy_sync_content__form_structure__build_message_block( $message, $variant );

	if ( empty( $alert_message_block ) ) {
		return $structure;
	}

	$structure['templates']['deploy-sync-content--form-bundle'][1]['content']['mainContent']['content'][0]['content'] = $alert_message_block;

	return $structure;
}

/**
 * Gets the data for the interactive data.
 *
 * @param array $deployment_bundle Bundle details.
 * @param array $context Context info.
 *
 * @return array The data for the UI.
 *
 * @throws WpDeployException Any error.
 * @throws \EaLab\WP_Deployer\vendor\Dakwamine\Component\Exception\UnmetDependencyException Dependency error.
 */
function deploy_sync_content__interactive_form__get_data( array $deployment_bundle, array $context ): array {
	// We use this to pass build info across systems.
	$build_info = new stdClass();

	$tracking_manager = ComponentBasedObject::getRootComponentByClassName( TrackingManager::class, true );

	// This is the deployment bundle, plus any item available for export if we are exporting.
	$deployment_bundle_and_new            = $deployment_bundle;
	$context['deployment_bundle_and_new'] = $deployment_bundle_and_new;

	if ( empty( $context['bundle_id'] ) ) {
		throw new WpDeployException( esc_html__( 'Unknown bundle identifier. A bundle identifier (bundle_id) is required when reviewing an existing export.', 'deploy-sync-content' ) );
	}

	// Get the last generation date of the deployment bundle.
	$file_manager = ComponentBasedObject::getRootComponentByClassName( FileManager::class, true );

	$content_json = $file_manager->loadDeploymentBundleMetadata( $context['bundle_id'], 'content.json' );

	if ( empty( $content_json ) ) {
		throw new WpDeployException( esc_html__( 'Could not load the content.json file for the bundle.', 'deploy-sync-content' ) );
	}

	$last_generation_date = $content_json->getValue( 'last_generation_date' );

	if ( empty( $last_generation_date ) ) {
		throw new WpDeployException( esc_html__( 'Could not retrieve the last generation date from the content.json file for the bundle.', 'deploy-sync-content' ) );
	}

	$last_generation_date = Date::toDateTime( $last_generation_date, new DateTimeZone( 'UTC' ) );

	if ( empty( $last_generation_date ) ) {
		throw new WpDeployException( esc_html__( 'Could not retrieve the last generation date from the content.json file for the bundle.', 'deploy-sync-content' ) );
	}

	$from_date = $content_json->getValue( 'from_date' ) ?? '';
	$from_date = $from_date ? Date::toDateTime( $from_date, new DateTimeZone( 'UTC' ) ) : null;

	if ( 'import' === $context['form_context'] ) {
		$import_export_config = $content_json->getValue( 'import_export_config' );

		if ( ! empty( $import_export_config ) ) {
			// We want to check with the deployment config included in the bundle.
			$with_deployment_config = array_map(
				static function ( $item ) {
					// The import_export_config has config and config_hashes.
					// We want only config.
					return $item['config'];
				},
				$import_export_config
			);

			$context['with_deployment_config'] = $with_deployment_config;

			ConfigProviderManagerBase::setActiveDeploymentConfigOverride( $with_deployment_config );
		}
	}

	if ( 'export' === ( $context['form_context'] ?? null ) ) {
		// We are reviewing an existing deployment bundle for export.
		// Retrieve all the items from the database from the "from_date",
		// and if not available, the last generation date.
		$all_items_from_from_date            = $tracking_manager->buildExportData( null, array( 'since' => $from_date ?? $last_generation_date ) );
		$context['all_items_from_from_date'] = $all_items_from_from_date;

		// Merge the items from the database with the items from the deployment bundle
		// but not a deep merge. The items from the exported bundle are the most important.
		$deployment_bundle_and_new = array_merge( $all_items_from_from_date, $deployment_bundle );

		$context['deployment_bundle_and_new'] = $deployment_bundle_and_new;

		// When exporting, check importability only when not on production.
		$context['bundle_importability'] = $tracking_manager->isProductionEnvironment()
			? null
			: deploy_sync_content__check_importability( $deployment_bundle_and_new, $context );
	} elseif ( 'import' === ( $context['form_context'] ?? null ) ) {
		$context['bundle_importability'] = deploy_sync_content__check_importability(
			$deployment_bundle_and_new,
			$context
		);
	}

	// Group the trackable items by their item tracker.
	$item_trackers_data = array();

	foreach ( $deployment_bundle_and_new as $item ) {
		$tracker = $item['tracker'] ?? '';

		if ( empty( $tracker ) ) {
			// Tracker ID is unset.
			throw new WpDeployException( esc_html__( 'Tracker ID is unset.', 'deploy-sync-content' ) );
		}

		$item_trackers_data[ $tracker ][] = $item;
	}

	// Reorder the trackers by their names.
	ksort( $item_trackers_data );

	// The computed item labels.
	// Useful to compute them once.
	$computed_labels = array();

	/**
	 * Processes the given items to put in the $normalized_items array.
	 *
	 * @param array $item_trackers_data The data to process. Can be of any tracker.
	 *
	 * @return array The processed data.
	 *
	 * @throws WpDeployException
	 */
	$process_item_trackers = static function ( array $item_trackers_data ) use ( $deployment_bundle, $tracking_manager, $context, &$computed_labels, $build_info ): array {
		$complete_normalized_items = array();

		foreach ( $item_trackers_data as $tracker_id => $items ) {
			/*
			 * First we check if this is a subordinated tracker.
			 *
			 * Dev note: the tracker tables may not exist (yet).
			 * This happens when we try to import a bundle that contains items
			 * from new trackers provided by plugins to activate.
			 */
			try {
				// Check if this item tracker is subordinated to another one.
				if ( $tracking_manager->isItemTrackerSubordinatedTo( $tracker_id ) ) {
					// This item tracker is subordinated to another one.
					// Do not process it here. Its items will be processed in the form of the parent item tracker.
					continue;
				}
			} catch ( WpDeployException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
				// This could mean that the tracker is not yet initialized,
				// and may be initialized by a plugin to install.
				// In this case, we will show the items of this tracker
				// so that the user can select them.
				// TODO: in this case we may display a message to the user
				// to inform him about the plugins to install for the following deployment items.
			}

			$process_items = static function ( $items, $maybe_parent_item_full_uuid = null ) use ( $deployment_bundle, $tracking_manager, $context, &$process_items, &$computed_labels, $build_info ) {
				$normalized_tracker_items = array(
					'bundled'    => array(),
					'notBundled' => array(),
				);

				if ( empty( $items ) ) {
					return $normalized_tracker_items;
				}

				foreach ( $items as $data ) {
					$tracker = $tracking_manager->getItemTrackerByTrackerId( $data['tracker'] );

					if ( empty( $tracker ) ) {
						// Invalid data.
						continue;
					}

					$tmd_table_name = $tracker->getTrackingMetaDataTableName( false ) . '.';

					$item_full_uuid = $data['tracker'] . '.' . $data['uuid'];

					$compiled_search_content = $item_full_uuid;

					$importability = $context['bundle_importability'][ $item_full_uuid ] ?? null;

					$computed_labels[ $item_full_uuid ] = deploy_sync_content__deployment_item__label_from_item( $data, $context );

					// Get modification dates for local modification check.
					$last_modification_date = $data['fields'][ $tmd_table_name . 'last_modification_date' ] ?? null;

					// Format modification date for display
					$formatted_modification_date = '';
					if ( ! empty( $last_modification_date ) ) {
						$modification_datetime = Date::toDateTime( $last_modification_date, new DateTimeZone( 'UTC' ) );
						if ( $modification_datetime ) {
							$formatted_modification_date = $modification_datetime->setTimezone( wp_timezone() )->format( Date::getSiteDateTimeFormat() );
						}
					}

					$last_modification_date_iso = '';
					if ( ! empty( $last_modification_date ) ) {
						$modification_datetime_iso = Date::toDateTime( $last_modification_date, new DateTimeZone( 'UTC' ) );
						if ( $modification_datetime_iso ) {
							$last_modification_date_iso = $modification_datetime_iso->format( Date::ISO );
						}
					}

					$current_item_output = array(
						'isBundled'                => ! empty( $deployment_bundle[ $item_full_uuid ] ),
						// Bundled items are selected by default.
						'isSelected'               => ! empty( $deployment_bundle[ $item_full_uuid ] ),
						'itemFullUuid'             => $item_full_uuid,
						'lastModificationDate'     => $last_modification_date_iso,
						'formattedModificationDate' => $formatted_modification_date,
						'publicName'               => &$computed_labels[ $item_full_uuid ],
						'tracker'                  => $data['tracker'],
					);

					foreach ( $data['depends_on'] ?? array() as $depends_on_uuid ) {
						if ( ! isset( $computed_labels[ $depends_on_uuid ] ) ) {
							// Set the UUID as a fallback.
							// It may be overridden later when the item will be computed.
							// If the item is not loaded, then we will have only the fallback.
							$computed_labels[ $depends_on_uuid ] = $depends_on_uuid;
						}

						$current_item_output['dependsOn'][] = array(
							'item' => array(
								'publicName' => &$computed_labels[ $depends_on_uuid ],
								'uuid'       => $depends_on_uuid,
							),
						);
					}

					if ( $maybe_parent_item_full_uuid ) {
						// This item is a sub-item.
						$current_item_output['parentItemFullUuid'] = $maybe_parent_item_full_uuid;
					}

					$compiled_search_content .= ' ' . $current_item_output['publicName'];

					/**
					 * Processes the sub-items of the current item, if any.
					 *
					 * @throws WpDeployException
					 */
					$process_sub_items = static function () use ( &$current_item_output, $item_full_uuid, $data, $tracking_manager, $context, &$process_items ): void {
						// Retrieve the subordinated items.
						// First, attempt to get the subordination info.
						// This only works if the subordinated trackers are initialized / available.
						// Note: When importing, non-initialized trackers will show their deployment items
						// as if they were not subordinated.
						$item_tracker = $tracking_manager->getItemTrackerByItemFullUuid( $item_full_uuid );

						if ( empty( $item_tracker ) ) {
							// Is that really possible?
							return;
						}

						try {
							$subordinated_trackers = $tracking_manager->getItemTrackerSubordinatesOf( $item_tracker );
						} catch ( WpDeployException $e ) {
							// Is that really possible?
							return;
						}

						foreach ( $subordinated_trackers as $subordinated_tracker ) {
							try {
								$subordinated_tracker_id = $subordinated_tracker->getTrackerId();
							} catch ( WpDeployException $e ) {
								// Is that really possible?
								continue;
							}

							$items_from_subordinated_tracker = array_filter(
								$context['deployment_bundle_and_new'],
								function ( $sub_item ) use ( $subordinated_tracker_id, $tracking_manager, $data ) {
									// First, perform a fast check to filter out the items
									// that are not from the current subordinated tracker.
									if ( ! ( ( $sub_item['tracker'] ?? null ) === $subordinated_tracker_id ) ) {
										return false;
									}

									try {
										// Then, perform a more accurate check to filter out the items
										// that are not subordinated to the current item.
										return $tracking_manager->isDeploymentItemSubordinatedTo( $sub_item, $data );
									} catch ( WpDeployException $e ) {
										// TODO: should we report this?
										return false;
									}
								}
							);

							// This will take care of the sub items.
							$result = $process_items( $items_from_subordinated_tracker, $item_full_uuid );

							// Append the bundled and non-bundled items on the same level for subitems.
							$current_item_output['subItems'] = array_merge( $current_item_output['subItems'] ?? array(), $result['bundled'], $result['notBundled'] );
						}
					};

					$process_sub_items();

					if ( isset( $importability ) ) {
						if ( $importability & Importability::UNREACHABLE_PRODUCTION_INSTANCE ) {
							$current_item_output['importability'] = array(
								'isInConflict'            => false,
								'isMerged'                => false,
								'isModified'              => false,
								'isNew'                   => false,
								'isProdUnreachable'       => true,
								'isTrackerConfigMismatch' => false,
							);
						} else {
							$current_item_output['importability'] = array(
								'isInConflict'            => ! ( $importability & Importability::CAN_IMPORT ) && ! ( $importability & Importability::MERGE_IS_VALID ) && ! ( $importability & Importability::TRACKER_CONFIG_MISMATCH ),
								'isMerged'                => ( $importability & Importability::CAN_IMPORT ) && ( $importability & Importability::MERGE_IS_VALID ),
								'isModified'              => ( $importability & Importability::HAS_CHANGES_TO_IMPORT ) && ( $importability & Importability::LOCAL_ITEM_EXISTS ),
								'isNew'                   => ( $importability & Importability::HAS_CHANGES_TO_IMPORT ) && ! ( $importability & Importability::LOCAL_ITEM_EXISTS ),
								'isTrackerConfigMismatch' => (bool) ( $importability & Importability::TRACKER_CONFIG_MISMATCH ),
							);
						}

						if ( 'import' === ( $context['form_context'] ?? null ) ) {
							// We check if the items
							if ($current_item_output['importability']['isNew'] === false
								&& $current_item_output['importability']['isModified'] === false
								&& $current_item_output['importability']['isInConflict'] === false
								&& $current_item_output['importability']['isTrackerConfigMismatch'] === false) {
								// Ignore this item. Don't even display it.
								// Dev note: this may prevent nested items from being imported.
								continue;
							}
						}

						$resolve_button = false;

						if ( $current_item_output['isBundled']
							&& $current_item_output['importability']['isInConflict']
							|| $current_item_output['importability']['isMerged'] ) {
							$current_item_output['itemActions'][] = array( 'resolve' => null );
							$resolve_button                       = true;
						}

						$see_changes_button = false;

						if ( $current_item_output['isBundled']
							&& $current_item_output['importability']['isNew'] ) {
							$current_item_output['itemActions'][] = array( 'seeChanges' => null );
							$see_changes_button                   = true;
						}

						foreach ( array( 'isInConflict', 'isMerged', 'isModified', 'isNew' ) as $status ) {
							if ( $current_item_output['importability'][ $status ] ) {
								$compiled_search_content .= ' ' . $status;
							}
						}

						if ( ! empty( $resolve_button ) ) {
							$current_item_output['mergeIframeUrl'] = add_query_arg(
								array(
									'deployment_bundle' => $context['bundle_id'],
									'embed_mode'        => 1,
									'form_context'      => $context['form_context'],
									'tab'               => 'merge',
									'uuid'              => $item_full_uuid,
								),
								admin_url( DEPLOY_SYNC_CONTENT__BASIC_URI )
							);
						}

						if ( ! empty( $see_changes_button ) ) {
							$current_item_output['diffIframeUrl'] = add_query_arg(
								array(
									'deployment_bundle' => $context['bundle_id'],
									'embed_mode'        => 1,
									'form_context'      => $context['form_context'],
									'tab'               => 'diff',
									'uuid'              => $item_full_uuid,
								),
								admin_url( DEPLOY_SYNC_CONTENT__BASIC_URI )
							);
						}
					} else {
						// TODO: cas où ça peut arriver:
						// - on est en prod,
						// - prod injoignable.
					}

					/*
					 * Changes section. This is an information we give only during export (regeneration):
					 * - "new": The item is not yet exported to the bundle.
					 * - "updated": The item is already in the bundle, and the local version in DB has changes.
					 * - "unchanged": The item is already in the bundle, but no changes found in the local DB.
					 */
					if ( 'export' === ( $context['form_context'] ?? null ) ) {
						$is_in_bundle   = ! empty( $deployment_bundle[ $item_full_uuid ] );
						$is_in_local_db = ! empty( $context['all_items_from_from_date'][ $item_full_uuid ] );

						// In theory, $is_in_bundle and $is_in_local_db cannot be both false.
						if ( $is_in_bundle && $is_in_local_db ) {
							$current_item_output['changesType'] = 'updated';
						} elseif ( $is_in_bundle ) {
							$current_item_output['changesType'] = 'unchanged';
						} elseif ( $is_in_local_db ) {
							$current_item_output['changesType'] = 'new';
						}
					}

					$current_item_output['compiledSearchContent'] = $compiled_search_content;

					// Allows other code to edit the item before inserting it.
					$current_item_output = apply_filters(
						'deploy_sync_content__interactive_form_blueprint_item_data_alter',
						$current_item_output,
						$context,
						$build_info,
						$data,
					);

					$normalized_tracker_items[ $current_item_output['isBundled'] ? 'bundled' : 'notBundled' ][] = array( 'deployment-item' => $current_item_output );
				}

				// Sort the items by their labels.
				usort( $normalized_tracker_items['bundled'], static fn( $a, $b ) => $a['deployment-item']['publicName'] <=> $b['deployment-item']['publicName'] );

				return $normalized_tracker_items;
			};

			$tracker_result = $process_items( $items );

			if ( empty( $tracker_result['bundled'] ) && empty( $tracker_result['notBundled'] ) ) {
				continue;
			}

			$items_assembled = array(
				'trackerId'   => $tracker_id,
				'trackerName' => $tracker_id, // TODO: chercher le label du tracker
			);

			if ( ! empty( $tracker_result['bundled'] ) ) {
				$items_assembled['bundled'] = $tracker_result['bundled'];
			}

			if ( ! empty( $tracker_result['notBundled'] ) ) {
				$items_assembled['notBundled'] = $tracker_result['notBundled'];
			}

			$complete_normalized_items['_bundleTrackers'][]['bundleTracker'] = $items_assembled;
		}

		return $complete_normalized_items;
	};

	$normalized_items = $process_item_trackers( $item_trackers_data );

	$build_handlers = apply_filters(
		'deploy_sync_content__bundle_ui_build_specs',
		array(
			// Implementors must return a blueprint array.
			'blueprint'          => static function (): array {
				return [];
			},
			// The path where to submit the data.
			// Something like "deploy-sync-content/v1/deployment-bundle/synchronize".
			'submitEndpointPath' => '',
		),
		$context
	);

	$final_build = $build_handlers['blueprint']();

	$final_build['data'] = $normalized_items;

	$final_build['data']['submitEndpoint'] = add_query_arg(
		array(
			'bundle_id' => $context['bundle_id'],
		),
		get_rest_url(
			null,
			$build_handlers['submitEndpointPath']
		)
	);

	if ( 'import' === $context['form_context'] ) {
		// Only the import has the option to force import.
		// If needed for export, allow it and add the button in the export structure.
		$final_build['data']['submitEndpointForceAll'] = add_query_arg(
			array(
				'bundle_id' => $context['bundle_id'],
			),
			add_query_arg(
				array(
					'force-import' => '1',
				),
				get_rest_url(
					null,
					$build_handlers['submitEndpointPath']
				)
			)
		);
	}

	if ( 'import' === $context['form_context'] ) {
		if ( ! empty( $context['with_deployment_config'] ) ) {
			// Restore the item trackers.
			ConfigProviderManagerBase::unsetDeploymentActiveConfigOverride();
		}
	}

	// Dev note: this is not the creation date, but the "From" date.
	// The creation date is determined by the bundle ID.
	$final_build['data']['_bundleCreationDatePretty'] = $from_date ? $from_date->format( Date::getSiteDateTimeFormat() ) : '';

	$final_build['data']['_bundleLastGenerationDate'] = $last_generation_date->format( Date::AS_BUNDLE_LABEL );

	$final_build['data']['_bundleLastGenerationDatePretty'] = $last_generation_date->format( Date::getSiteDateTimeFormat() . ' (e)' );

	$final_build['data']['_bundleLastGenerationDateAgo'] = __( 'Last generation', 'deploy-sync-content' ) . ' ' . Date::elapsedTimeSince( $last_generation_date );

	$final_build['data']['_bundleName'] = ! empty( $content_json->getValue( 'bundle_label' ) ) ? $content_json->getValue( 'bundle_label' ) : Date::toDateTimeWithFormat( Date::ISO_COMPACT, $context['bundle_id'] )->format( Date::AS_BUNDLE_LABEL );

	$final_build['data']['_exportedBy'] = $content_json->getValue( 'exported_by' );

	$final_build['data']['_bundle_id'] = $context['bundle_id'];

	// Apply any alterations on the complete build.
	$final_build = apply_filters(
		'deploy_sync_content__interactive_form_blueprint_alter',
		$final_build,
		$context,
		$build_info
	);

	return $final_build;
}

add_filter(
	'deploy_sync_content__bundle_ui_build_specs',
	static function ( array $build_specs, $context ) {
		if ( $context['form_context'] === 'import' ) {
			$build_specs['blueprint'] = static function (): array {
				$yaml = file_get_contents( DEPLOY_SYNC_CONTENT__PLUGIN_DIR . 'includes/blueprint/interactive-form--form-structure--synchronize.yaml' );

				return Yaml::parse( $yaml );
			};

			$build_specs['submitEndpointPath'] = 'deploy-sync-content/v1/deployment-bundle/synchronize';
		}

		return $build_specs;
	},
	-10,
	2
);
