<?php
/**
 * Core module composition.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Bootstrap;

use ScheduledChanges\Admin\AdminAppPage;
use ScheduledChanges\Admin\AdminActionRegistry;
use ScheduledChanges\Admin\AdminAssetRegistry;
use ScheduledChanges\Admin\AdminDataRegistry;
use ScheduledChanges\Admin\AdminScreenRegistry;
use ScheduledChanges\Audit\AuditDiagnosticsModule;
use ScheduledChanges\Audit\AuditDiagnosticsServices;
use ScheduledChanges\Execution\ExecutionModule;
use ScheduledChanges\Execution\ExecutionServices;
use ScheduledChanges\Extension\BundledModuleIds;
use ScheduledChanges\Extension\ModuleRuntime;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\ProviderDiscoveryModule;
use ScheduledChanges\Provider\ProviderDiscoveryServices;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Recording\RecordingLaunchCompilerRouter;
use ScheduledChanges\Recording\RecordingModule;
use ScheduledChanges\Recording\RecordingService;
use ScheduledChanges\Recording\RecordingWorkspaceRouter;
use ScheduledChanges\Repository\AuditRepository;
use ScheduledChanges\Repository\AdminDashboardRepository;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Repository\WpdbLaunchRepository;
use ScheduledChanges\Repository\WpdbLaunchVersionRepository;
use ScheduledChanges\Repository\WpdbRecordingSessionRepository;
use ScheduledChanges\Rest\LaunchesController;
use ScheduledChanges\Rest\RecordingsController;
use ScheduledChanges\Rest\RestServiceProvider;
use ScheduledChanges\Rest\SystemStatusController;
use ScheduledChanges\Rollback\RollbackModule;
use ScheduledChanges\Scheduler\CurrentActionContext;
use ScheduledChanges\Scheduler\SchedulingModule;
use ScheduledChanges\Scheduler\WordPressActionSchedulerGateway;
use ScheduledChanges\Snapshot\SnapshotModule;
use ScheduledChanges\Templates\LaunchTemplatesModule;
use ScheduledChanges\Taxonomy\TaxonomyModule;
use ScheduledChanges\Taxonomy\WordPressCategoryModule;
use ScheduledChanges\Taxonomy\WordPressTagModule;
use ScheduledChanges\Validation\ValidationModule;
use ScheduledChanges\Validation\PlanValidator;
use ScheduledChanges\WordPressContent\NativeContentModule;
use ScheduledChanges\WordPressContent\NativeWordPressFieldSchema;
use ScheduledChanges\WordPressContent\RegisteredMetaModule;
use ScheduledChanges\WordPressContent\Recording\NativeWordPressRecordingDiffBuilder;
use ScheduledChanges\WordPressContent\Recording\NativeWordPressRecordingLaunchCompiler;
use ScheduledChanges\WordPressContent\Recording\NativeWordPressRecordingSnapshotter;
use ScheduledChanges\WordPressContent\Recording\NativeWordPressRecordingWorkspaceAdapter;
use ScheduledChanges\WpCli\CliServiceProvider;
use ScheduledChanges\WpCli\JpsCommand;
use ScheduledChanges\WpCli\LaunchTemplatesCommand;

/**
 * Creates and registers services from the active core module graph.
 */
final class CoreModuleProvider {
	/**
	 * Boots the resolved core module graph.
	 */
	public function boot( ModuleRuntime $runtime ): CoreModuleServices {
		$this->assertSchedulingRuntimeAvailable( $runtime );

		$operational       = ( new AuditDiagnosticsModule() )->createServices();
		$repositories      = $this->repositories();
		$provider_services = $this->providerServices( $runtime, $repositories, $operational->audit() );
		$validator         = ( new ValidationModule() )
			->createServices( $provider_services->providers(), $provider_services->fields() )
			->validator();
		$execution         = $this->executionRuntime( $runtime, $provider_services, $operational->runs() );
		$scheduling        = $this->schedulingRuntime(
			$runtime,
			$provider_services,
			$repositories,
			$operational,
			$validator,
			$execution
		);
		$rollback          = $this->rollbackRuntime(
			$runtime,
			$repositories,
			$operational,
			$provider_services,
			$execution
		);
		$recording         = $this->recordingService(
			$runtime,
			$repositories,
			$provider_services->providers(),
			$provider_services->fields(),
			$execution->services()
		);
		$services          = new CoreModuleServices(
			$provider_services->manager(),
			$scheduling->scheduler(),
			$scheduling->queue(),
			$rollback->service(),
			$rollback->endVersionCompiler(),
			$recording
		);

		$this->registerDelivery(
			$runtime,
			$services,
			$repositories,
			$operational->runs()
		);

		return $services;
	}

	/**
	 * Creates storage dependencies shared across module builders.
	 */
	private function repositories(): CoreModuleRepositories {
		return new CoreModuleRepositories(
			new WpdbRecordingSessionRepository(),
			new WpdbLaunchRepository(),
			new WpdbLaunchVersionRepository()
		);
	}

	/**
	 * Stops composition before side-effectful hooks are registered without a queue runtime.
	 */
	private function assertSchedulingRuntimeAvailable( ModuleRuntime $runtime ): void {
		if ( ! $this->selected( $runtime, SchedulingModule::MODULE_ID ) ) {
			return;
		}

		if ( ! ( new WordPressActionSchedulerGateway() )->available() ) {
			throw new \RuntimeException( 'Action Scheduler APIs are unavailable.' );
		}
	}

	/**
	 * Creates provider and field discovery services.
	 */
	private function providerServices(
		ModuleRuntime $runtime,
		CoreModuleRepositories $repositories,
		AuditRepository $audit
	): ProviderDiscoveryServices {
		$native_module          = $this->selected( $runtime, NativeContentModule::MODULE_ID ) ? new NativeContentModule() : null;
		$taxonomy_registrations = array();
		if ( $this->selected( $runtime, WordPressCategoryModule::MODULE_ID ) ) {
			$taxonomy_registrations[] = ( new WordPressCategoryModule() )->registration();
		}
		if ( $this->selected( $runtime, WordPressTagModule::MODULE_ID ) ) {
			$taxonomy_registrations[] = ( new WordPressTagModule() )->registration();
		}
		$registered_meta_module = $this->selected( $runtime, RegisteredMetaModule::MODULE_ID ) ? new RegisteredMetaModule() : null;
		$meta_registry          = $registered_meta_module ? $registered_meta_module->createRegistry() : null;
		$native_schema          = new NativeWordPressFieldSchema(
			$meta_registry,
			$this->selected( $runtime, TaxonomyModule::MODULE_ID ) ? new TaxonomyModule() : null,
			$taxonomy_registrations,
			null !== $registered_meta_module
		);

		$provider_module   = new ProviderDiscoveryModule();
		$provider_services = $provider_module->createServices(
			$native_module ? array( $native_module->createProvider( $native_schema, $registered_meta_module ? $registered_meta_module->createService( $meta_registry ) : null ) ) : array()
		);
		$provider_module->registerHooks( $provider_services, $audit );

		if ( $native_module ) {
			$native_module->registerHooks( $repositories->recordings() );
		}

		return $provider_services;
	}

	/**
	 * Creates execution and snapshot services when selected.
	 */
	private function executionRuntime(
		ModuleRuntime $runtime,
		ProviderDiscoveryServices $provider_services,
		RunRepository $runs
	): CoreExecutionRuntime {
		$providers         = $provider_services->providers();
		$fields            = $provider_services->fields();
		$snapshot_services = null;
		if (
			$this->selected( $runtime, ExecutionModule::MODULE_ID )
			|| $this->selected( $runtime, RollbackModule::MODULE_ID )
		) {
			$snapshot_services = ( new SnapshotModule() )->createServices( $providers );
		}

		$execution_module   = null;
		$execution_services = null;

		if ( $this->selected( $runtime, ExecutionModule::MODULE_ID ) ) {
			if ( null === $snapshot_services ) {
				throw new \RuntimeException( 'Execution requires the snapshot module.' );
			}

			$execution_module   = new ExecutionModule();
			$execution_services = $execution_module->createServices(
				$providers,
				$fields,
				$runs,
				$snapshot_services->coordinator()
			);
		}

		return new CoreExecutionRuntime( $execution_module, $execution_services, $snapshot_services );
	}

	/**
	 * Creates scheduling services and queue hooks when selected.
	 */
	private function schedulingRuntime(
		ModuleRuntime $runtime,
		ProviderDiscoveryServices $provider_services,
		CoreModuleRepositories $repositories,
		AuditDiagnosticsServices $operational,
		PlanValidator $validator,
		CoreExecutionRuntime $execution
	): CoreSchedulingRuntime {
		if ( $this->selected( $runtime, SchedulingModule::MODULE_ID ) ) {
			if ( null === $execution->module() || null === $execution->services() ) {
				throw new \RuntimeException( 'Scheduling requires the execution module.' );
			}

			$actions           = new WordPressActionSchedulerGateway();
			$action_context    = new CurrentActionContext();
			$scheduling_module = new SchedulingModule();
			$scheduling        = $scheduling_module->createServices(
				$provider_services->manager(),
				$repositories->launches(),
				$repositories->versions(),
				$operational->runs(),
				$operational->audit(),
				$validator,
				$execution->services()->locks(),
				$execution->services()->engine(),
				$actions,
				$action_context
			);
			$scheduling_module->registerHooks( $scheduling );

			return new CoreSchedulingRuntime( $scheduling->scheduler(), $scheduling->queue() );
		}

		return new CoreSchedulingRuntime( null, null );
	}

	/**
	 * Creates rollback services when selected.
	 */
	private function rollbackRuntime(
		ModuleRuntime $runtime,
		CoreModuleRepositories $repositories,
		AuditDiagnosticsServices $operational,
		ProviderDiscoveryServices $provider_services,
		CoreExecutionRuntime $execution
	): CoreRollbackRuntime {
		$rollback_module      = $this->selected( $runtime, RollbackModule::MODULE_ID ) ? new RollbackModule() : null;
		$rollback             = null;
		$end_version_compiler = null;
		$execution_services   = $execution->services();
		$snapshot_services    = $execution->snapshots();
		$providers            = $provider_services->providers();
		$fields               = $provider_services->fields();

		if ( null !== $rollback_module && null !== $snapshot_services ) {
			$end_version_compiler = $rollback_module->createEndVersionCompiler( $repositories->versions() );

			if ( null !== $execution_services ) {
				$rollback = $rollback_module->createRollbackService(
					$snapshot_services,
					$repositories->versions(),
					$operational->runs(),
					$providers,
					$fields,
					$execution_services->locks(),
					$operational->audit()
				);
			}
		}

		return new CoreRollbackRuntime( $rollback, $end_version_compiler );
	}

	/**
	 * Creates Record Changes only when its module and execution dependency exist.
	 */
	private function recordingService(
		ModuleRuntime $runtime,
		CoreModuleRepositories $repositories,
		ProviderRegistry $providers,
		FieldRegistry $fields,
		?ExecutionServices $execution
	): ?RecordingService {
		if ( ! $this->selected( $runtime, RecordingModule::MODULE_ID ) || null === $execution ) {
			return null;
		}

		$workspace_routes = array();
		$compiler_routes  = array();

		if ( $this->selected( $runtime, NativeContentModule::MODULE_ID ) ) {
			$native_adapter           = new NativeWordPressRecordingWorkspaceAdapter(
				new NativeWordPressRecordingSnapshotter( $providers ),
				new NativeWordPressRecordingDiffBuilder( $fields )
			);
			$native_compiler          = new NativeWordPressRecordingLaunchCompiler(
				$providers,
				$fields,
				$repositories->launches(),
				$repositories->versions()
			);
			$workspace_routes['post'] = $native_adapter;
			$workspace_routes['page'] = $native_adapter;
			$compiler_routes['post']  = $native_compiler;
			$compiler_routes['page']  = $native_compiler;
		}

		$adapter  = PublicHooks::recordingWorkspaceAdapter(
			new RecordingWorkspaceRouter( $workspace_routes ),
			$providers,
			$fields,
			$repositories->recordings()
		);
		$compiler = PublicHooks::recordingLaunchCompiler(
			new RecordingLaunchCompilerRouter( $compiler_routes ),
			$providers,
			$fields,
			$repositories->launches(),
			$repositories->versions()
		);

		if ( null === $adapter || null === $compiler ) {
			return null;
		}

		return ( new RecordingModule() )->createServices(
			$repositories->recordings(),
			$adapter,
			$compiler,
			$execution->locks()
		)->recording();
	}

	/**
	 * Registers delivery surfaces selected by active modules.
	 */
	private function registerDelivery(
		ModuleRuntime $runtime,
		CoreModuleServices $services,
		CoreModuleRepositories $repositories,
		RunRepository $runs
	): void {
		$providers = $services->providers();
		$queue     = $services->queue();
		$recording = $services->recording();
		$scheduler = $services->scheduler();
		$rollback  = $services->rollback();
		$dashboard = new AdminDashboardRepository();

		if ( $this->selected( $runtime, BundledModuleIds::ADMIN_SHELL ) ) {
			$module_ids = $runtime->bootOrderIds();
			( new AdminAppPage(
				$providers,
				$queue,
				$repositories->recordings(),
				$recording,
				$dashboard,
				null !== $recording,
				AdminScreenRegistry::forModules( $module_ids ),
				AdminActionRegistry::forModules( $module_ids ),
				AdminDataRegistry::forModules( $module_ids ),
				null,
				AdminAssetRegistry::forModules( $module_ids )
			) )->register();
		}

		if ( $this->selected( $runtime, BundledModuleIds::REST_API ) ) {
			$launches_controller   = ! $this->selected( $runtime, BundledModuleIds::REST_LAUNCHES ) || null === $scheduler
				? null
				: new LaunchesController(
					$dashboard,
					$repositories->launches(),
					$repositories->versions(),
					$scheduler,
					$providers,
					$runs,
					$rollback,
					$this->selected( $runtime, BundledModuleIds::REST_ROLLBACK )
				);
			$status_controller     = new SystemStatusController( $providers, $queue, $repositories->recordings() );
			$recordings_controller = ! $this->selected( $runtime, BundledModuleIds::REST_RECORDINGS ) || null === $recording
				? null
				: new RecordingsController( $recording, $repositories->recordings(), $scheduler );
			( new RestServiceProvider(
				$launches_controller,
				$status_controller,
				$recordings_controller
			) )->register();
		}

		if ( $this->selected( $runtime, BundledModuleIds::WP_CLI ) ) {
			$command         = new JpsCommand(
				$providers,
				$queue,
				$repositories->recordings(),
				$recording,
				$dashboard,
				$repositories->launches(),
				$repositories->versions(),
				$runs,
				$scheduler,
				$rollback
			);
			$module_commands = array();
			if ( $this->selected( $runtime, BundledModuleIds::CLI_SCHEDULING ) ) {
				$module_commands['launch'] = array( $command, 'launch' );
				$module_commands['queue']  = array( $command, 'queue' );
			}
			if ( $this->selected( $runtime, BundledModuleIds::CLI_RECORDING ) ) {
				$module_commands['recording'] = array( $command, 'recording' );
			}
			if ( $this->selected( $runtime, BundledModuleIds::CLI_PROVIDERS ) ) {
				$module_commands['providers'] = array( $command, 'providers' );
				$module_commands['fields']    = array( $command, 'fields' );
			}
			if ( $this->selected( $runtime, BundledModuleIds::CLI_DIAGNOSTICS ) ) {
				$module_commands['logs']       = array( $command, 'logs' );
				$module_commands['conflicts']  = array( $command, 'conflicts' );
				$module_commands['migrations'] = array( $command, 'migrations' );
				$module_commands['doctor']     = array( $command, 'doctor' );
			}
			if ( $this->selected( $runtime, LaunchTemplatesModule::MODULE_ID ) ) {
				$module_commands['templates'] = new LaunchTemplatesCommand(
					( new LaunchTemplatesModule() )->createCatalog()
				);
			}

			( new CliServiceProvider( $module_commands ) )->register();
		}
	}

	/**
	 * Returns whether the resolved module graph includes a module.
	 */
	private function selected( ModuleRuntime $runtime, string $module_id ): bool {
		return in_array( $module_id, $runtime->bootOrderIds(), true );
	}
}
