<?php
/**
 * WooCommerce extension module provider.
 *
 * @package ScheduledChangesWooCommerce
 */

namespace ScheduledChangesWooCommerce;

use ScheduledChangesWooCommerce\CoreCompat\Admin\ProductEditorRecordingModule;
use ScheduledChanges\Bootstrap\CoreModuleServices;
use ScheduledChanges\Bootstrap\Plugin;
use ScheduledChanges\Discovery\DiscoverySettings;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\Provider\RawMetadataFieldProvider;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\Provider\RegisteredMetaFieldProvider;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\Provider\TaxonomyFieldProvider;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\ProductTypeDiscovery;
use ScheduledChanges\Discovery\RawMetadataDiagnosticsModule;
use ScheduledChanges\Discovery\RegisteredMetadataRegistry;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\RestSchemaInspector;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyCatalog;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyTermService;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Extension\ModuleDefinition;
use ScheduledChanges\Extension\ExtensionDefinition;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Recording\RecordingLaunchCompilerRouter;
use ScheduledChanges\Recording\RecordingLaunchCompilerContract;
use ScheduledChanges\Recording\RecordingWorkspaceAdapter;
use ScheduledChanges\Recording\RecordingWorkspaceRouter;
use ScheduledChangesWooCommerce\CoreCompat\Recording\ShadowProductAccessGuard;
use ScheduledChangesWooCommerce\CoreCompat\Recording\WooCommerceRecordingModule;
use ScheduledChanges\Repository\LaunchRepository;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Repository\RecordingSessionRepository;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Support\VersionPolicy;
use ScheduledChanges\Taxonomy\TaxonomyRegistration;
use ScheduledChanges\Taxonomy\WordPressCategoryModule;
use ScheduledChanges\Taxonomy\WordPressTagModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Downloads\DownloadCodec;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Downloads\DownloadableProductModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Inventory\CostOfGoodsModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Relationships\ExternalProductModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Inventory\InventoryAdjustmentService;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Inventory\InventoryModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\NativeProductProvider;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\NativeFieldSchema;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Pricing\PricingModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\ProductSynchronizer;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Attributes\ProductAttributesModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Identity\ProductIdentityModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Media\ProductMediaModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Relationships\ProductRelationshipsModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Shipping\ShippingModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Tax\TaxModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Variations\VariationStructureModule;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\WooCommerceOperationHandler;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\WooCommerceProviderAuthorization;
use ScheduledChangesWooCommerce\Handoff\WooCommerceExtensionHandoff;

/**
 * Registers the scaffold through public core hooks.
 */
final class ModuleProvider {
	private const RAW_METADATA_DIAGNOSTICS_MODULE_ID = 'scheduled-changes-raw-metadata-diagnostics';

	/**
	 * Registers dependency diagnostics and module manifest.
	 *
	 * @param string $plugin_file Main plugin file.
	 */
	public static function boot( string $plugin_file ): void {
		$status = DependencyStatus::current();

		self::registerManifest( $plugin_file, $status );

		if ( ! $status['loadable'] ) {
			if ( ! defined( 'SCHEDULED_CHANGES_WC_BUNDLED' ) ) {
				self::registerAdminNotice( $status['message'] );
			}
			return;
		}

		self::registerTaxonomyModules();
		self::registerProviderAuthorization();
		if ( self::moduleSelected( WooCommerceRecordingModule::MODULE_ID ) ) {
			( new WooCommerceRecordingModule() )->createAccessGuard()->register();
			self::registerRecordingWorkspace();
		}
		if ( self::moduleSelected( ProductIdentityModule::MODULE_ID ) ) {
			self::registerCoreCompatibilityHooks();
			self::registerNativeProviderServices();
		}
		self::registerOperationHandlers();
		if ( self::moduleSelected( VariationStructureModule::MODULE_ID ) ) {
			self::registerVariationValidationHooks();
		}
		self::registerAdminAndDiagnosticSurfaces();
		if ( self::moduleSelected( ProductEditorRecordingModule::MODULE_ID ) ) {
			self::registerProductEditorHooks();
		}

		do_action( 'scheduled_changes_woocommerce_provider_loaded', $plugin_file, $status );
	}

	/**
	 * Contributes WooCommerce category and tag registrations when selected.
	 */
	private static function registerTaxonomyModules(): void {
		add_filter(
			'scheduled_changes_taxonomy_registrations',
			static function ( array $registrations ): array {
				$runtime  = \ScheduledChanges\Bootstrap\Plugin::instance()->moduleRuntime();
				$selected = null === $runtime ? array() : $runtime->bootOrderIds();

				if ( in_array( WordPressCategoryModule::MODULE_ID, $selected, true ) ) {
					$registrations[] = new TaxonomyRegistration( 'woocommerce-product-categories', 'product-taxonomies', 'product_cat', array( 'product' ), 'Product categories', 'assign_product_terms', true, true );
				}

				if ( in_array( WordPressTagModule::MODULE_ID, $selected, true ) ) {
					$registrations[] = new TaxonomyRegistration( 'woocommerce-product-tags', 'product-taxonomies', 'product_tag', array( 'product' ), 'Product tags', 'assign_product_terms', false, true );
				}

				return $registrations;
			}
		);
	}

	/**
	 * Supplies WooCommerce-only compatibility behavior to neutral core services.
	 */
	private static function registerCoreCompatibilityHooks(): void {
		add_filter(
			'scheduled_changes_validation_target_exists',
			static function ( bool $exists, TargetDefinition $target ): bool {
				if ( 'woocommerce-core' !== $target->providerId() ) {
					return $exists;
				}

				$product = null === $target->targetId() ? null : wc_get_product( $target->targetId() );

				if ( ! $product instanceof \WC_Product ) {
					return false;
				}

				return ( 'variation' === $target->targetType() ) === ( $product instanceof \WC_Product_Variation );
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_validation_target_subtype',
			static function ( string $subtype, TargetDefinition $target ): string {
				if ( 'woocommerce-core' !== $target->providerId() || null === $target->targetId() ) {
					return $subtype;
				}

				$product = wc_get_product( $target->targetId() );

				return $product instanceof \WC_Product ? $product->get_type() : '';
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_validation_unique_value_owner_id',
			static function ( int $owner_id, string $kind, string $value ): int {
				return 'sku' === $kind ? (int) wc_get_product_id_by_sku( $value ) : $owner_id;
			},
			10,
			3
		);

		add_filter(
			'scheduled_changes_export_target_mapping',
			static function ( array $mapping, TargetDefinition $target ): array {
				if ( 'woocommerce-core' !== $target->providerId() || null === $target->targetId() ) {
					return $mapping;
				}

				$product = wc_get_product( $target->targetId() );

				if ( $product instanceof \WC_Product ) {
					$mapping['sku'] = $product->get_sku( 'edit' );
				}

				return $mapping;
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_import_target_id',
			static function ( int $target_id, array $mapping, string $strategy, TargetDefinition $source_target ): int {
				if ( 'woocommerce-core' !== $source_target->providerId() ) {
					return $target_id;
				}

				if ( 'id' === $strategy ) {
					return (int) ( $mapping['target_id'] ?? $mapping['object_id'] ?? 0 );
				}

				$sku = trim( (string) ( $mapping['sku'] ?? '' ) );

				return '' === $sku ? 0 : (int) wc_get_product_id_by_sku( $sku );
			},
			10,
			4
		);
	}

	/**
	 * Authorizes WooCommerce-owned targets for provider-neutral mutation services.
	 */
	private static function registerProviderAuthorization(): void {
		( new WooCommerceProviderAuthorization() )->register();
	}

	/**
	 * Registers the module manifest when compatible core APIs are available.
	 *
	 * @param string                                                                                                                    $plugin_file Main plugin file.
	 * @param array{loadable:bool,lifecycle_status:string,message:string,verified_lifecycle:string[],certification:array<string,mixed>} $status Dependency status.
	 */
	private static function registerManifest( string $plugin_file, array $status ): void {
		if ( ! class_exists( ModuleDefinition::class ) || ! class_exists( ExtensionDefinition::class ) ) {
			return;
		}

		add_filter(
			'scheduled_changes_extensions',
			static function ( array $extensions ) use ( $plugin_file ): array {
				$extensions[] = new ExtensionDefinition(
					'scheduled-changes-woocommerce-extension',
					'Scheduled Changes for WooCommerce',
					SCHEDULED_CHANGES_WC_VERSION,
					$plugin_file,
					array(
						ProductIdentityModule::MODULE_ID,
						PricingModule::MODULE_ID,
						InventoryModule::MODULE_ID,
						ProductAttributesModule::MODULE_ID,
						ShippingModule::MODULE_ID,
						TaxModule::MODULE_ID,
						ProductMediaModule::MODULE_ID,
						DownloadableProductModule::MODULE_ID,
						ProductRelationshipsModule::MODULE_ID,
						CostOfGoodsModule::MODULE_ID,
						ExternalProductModule::MODULE_ID,
						VariationStructureModule::MODULE_ID,
						WooCommerceRecordingModule::MODULE_ID,
						ProductEditorRecordingModule::MODULE_ID,
					),
					'Packages independently selectable WooCommerce product-data, variation, recording-workspace, and product-editor delivery modules.',
					ExtensionDefinition::VISIBILITY_PUBLIC,
					'documentation/compatibility-and-modules.md'
				);

				return $extensions;
			}
		);

		add_filter(
			'scheduled_changes_modules',
			static function ( array $modules ) use ( $plugin_file, $status ): array {
				$modules[] = new ModuleDefinition(
					ProductEditorRecordingModule::MODULE_ID,
					'WooCommerce Product Editor Recording Module',
					SCHEDULED_CHANGES_WC_VERSION,
					'woocommerce-core',
					'woocommerce-core',
					$plugin_file,
					$status['verified_lifecycle'],
					$status['lifecycle_status'],
					$status['message'],
					$status['certification'],
					array(
						'dependencies'       => array( WooCommerceRecordingModule::MODULE_ID, 'scheduled-changes-scheduling', 'scheduled-changes-admin-shell' ),
						'capabilities'       => array( 'woocommerce.product-editor-recording' ),
						'package_form'       => ModuleDefinition::PACKAGE_BUNDLED_EXTENSION,
						'resources'          => array(
							ModuleDefinition::RESOURCE_ASSET => array( 'scheduled-changes-product-editor-recording' ),
							ModuleDefinition::RESOURCE_WORDPRESS_HOOK => array(
								'admin_post_scheduled_changes_start_product_editor_recording',
								'wp_ajax_scheduled_changes_cancel_product_editor_recording',
								'wp_ajax_scheduled_changes_finalize_product_editor_recording',
								'wp_ajax_scheduled_changes_recording_create_pending_term',
								'wp_ajax_scheduled_changes_save_product_editor_recording',
								'wp_ajax_scheduled_changes_schedule_product_editor_recording',
								'wp_ajax_scheduled_changes_start_product_editor_recording',
							),
						),
						'visibility'         => ModuleDefinition::VISIBILITY_GROUPED_PUBLIC,
						'source_package'     => 'scheduled-changes-woocommerce-extension',
						'group'              => 'woocommerce',
						'description'        => 'Adds permission-checked Record Changes controls, notices, scripts, styles, AJAX actions, and pending taxonomy-term handling to the WooCommerce product editor.',
						'registration_phase' => ModuleDefinition::PHASE_DELIVERY,
						'documentation_url'  => 'documentation/compatibility-and-modules.md',
					)
				);

				$modules[] = new ModuleDefinition(
					WooCommerceRecordingModule::MODULE_ID,
					'WooCommerce Recording Workspace Module',
					SCHEDULED_CHANGES_WC_VERSION,
					'woocommerce-core',
					'woocommerce-core',
					$plugin_file,
					$status['verified_lifecycle'],
					$status['lifecycle_status'],
					$status['message'],
					$status['certification'],
					array(
						'dependencies'       => array( 'scheduled-changes-recording', ProductIdentityModule::MODULE_ID, 'scheduled-changes-snapshots-rollback' ),
						'capabilities'       => array( 'woocommerce.recording-workspace' ),
						'package_form'       => ModuleDefinition::PACKAGE_BUNDLED_EXTENSION,
						'resources'          => array( ModuleDefinition::RESOURCE_FIELD => array( 'woocommerce.recording-workspace' ) ),
						'visibility'         => ModuleDefinition::VISIBILITY_GROUPED_PUBLIC,
						'source_package'     => 'scheduled-changes-woocommerce-extension',
						'group'              => 'woocommerce',
						'description'        => 'Records WooCommerce products through source-safe shadow workspaces, selected-module snapshots and diffs, variation remapping, launch compilation, and certified cleanup.',
						'registration_phase' => ModuleDefinition::PHASE_PROVIDERS,
						'documentation_url'  => 'documentation/compatibility-and-modules.md',
					)
				);

				$modules[] = new ModuleDefinition(
					ProductIdentityModule::MODULE_ID,
					'Product Identity Module',
					SCHEDULED_CHANGES_WC_VERSION,
					'woocommerce-core',
					'woocommerce-core',
					$plugin_file,
					$status['verified_lifecycle'],
					$status['lifecycle_status'],
					$status['message'],
					$status['certification'],
					array(
						'dependencies'       => array( 'scheduled-changes-provider-discovery', 'scheduled-changes-snapshots-rollback' ),
						'capabilities'       => array( 'woocommerce.product-identity' ),
						'package_form'       => ModuleDefinition::PACKAGE_BUNDLED_EXTENSION,
						'resources'          => array(
							ModuleDefinition::RESOURCE_FIELD => array( 'woocommerce.product-identity' ),
							ModuleDefinition::RESOURCE_WORDPRESS_HOOK => array( 'scheduled_changes_woocommerce_provider_loaded' ),
						),
						'visibility'         => ModuleDefinition::VISIBILITY_GROUPED_PUBLIC,
						'source_package'     => 'scheduled-changes-woocommerce-extension',
						'group'              => 'woocommerce',
						'description'        => 'Schedules product and variation identity, type, status, catalog visibility, descriptions, SKU/global ID, purchase note, menu order, CRUD readback, and synchronization contracts.',
						'registration_phase' => ModuleDefinition::PHASE_PROVIDERS,
						'documentation_url'  => 'documentation/compatibility-and-modules.md',
					)
				);

				$modules[] = new ModuleDefinition(
					PricingModule::MODULE_ID,
					'Pricing Module',
					SCHEDULED_CHANGES_WC_VERSION,
					'woocommerce-core',
					'woocommerce-core',
					$plugin_file,
					$status['verified_lifecycle'],
					$status['lifecycle_status'],
					$status['message'],
					$status['certification'],
					array(
						'dependencies'       => array(
							'scheduled-changes-provider-discovery',
							ProductIdentityModule::MODULE_ID,
							'scheduled-changes-launch-templates',
							'scheduled-changes-snapshots-rollback',
						),
						'capabilities'       => array( 'woocommerce.pricing' ),
						'package_form'       => ModuleDefinition::PACKAGE_BUNDLED_EXTENSION,
						'resources'          => array(
							ModuleDefinition::RESOURCE_FIELD => array( 'woocommerce.pricing' ),
						),
						'visibility'         => ModuleDefinition::VISIBILITY_GROUPED_PUBLIC,
						'source_package'     => 'scheduled-changes-woocommerce-extension',
						'group'              => 'woocommerce',
						'description'        => 'Schedules regular and sale prices, sale windows, canonical normalization, validation, readback, synchronization, and rollback for products and variations.',
						'registration_phase' => ModuleDefinition::PHASE_PROVIDERS,
						'documentation_url'  => 'documentation/compatibility-and-modules.md',
					)
				);

				$modules[] = new ModuleDefinition(
					InventoryModule::MODULE_ID,
					'Inventory Module',
					SCHEDULED_CHANGES_WC_VERSION,
					'woocommerce-core',
					'woocommerce-core',
					$plugin_file,
					$status['verified_lifecycle'],
					$status['lifecycle_status'],
					$status['message'],
					$status['certification'],
					array(
						'dependencies'       => array(
							'scheduled-changes-provider-discovery',
							ProductIdentityModule::MODULE_ID,
							'scheduled-changes-execution',
							'scheduled-changes-launch-templates',
							'scheduled-changes-snapshots-rollback',
						),
						'capabilities'       => array( 'woocommerce.inventory' ),
						'package_form'       => ModuleDefinition::PACKAGE_BUNDLED_EXTENSION,
						'resources'          => array(
							ModuleDefinition::RESOURCE_FIELD => array( 'woocommerce.inventory' ),
							ModuleDefinition::RESOURCE_META  => array( '_scheduled_changes_inventory_*' ),
						),
						'visibility'         => ModuleDefinition::VISIBILITY_GROUPED_PUBLIC,
						'source_package'     => 'scheduled-changes-woocommerce-extension',
						'group'              => 'woocommerce',
						'description'        => 'Schedules stock management, quantity, status, backorders, sold-individually behavior, low-stock thresholds, and idempotent absolute or delta adjustments.',
						'registration_phase' => ModuleDefinition::PHASE_PROVIDERS,
						'documentation_url'  => 'documentation/compatibility-and-modules.md',
					)
				);

				$field_module_definitions = array(
					array(
						ProductAttributesModule::MODULE_ID,
						'Product Attributes Module',
						'woocommerce.attributes',
						'Schedules global and custom attribute definitions, terms, visibility, variation flags, ordering, defaults, and variation assignments without enabling variation creation.',
					),
					array(
						VariationStructureModule::MODULE_ID,
						'Variation Structure Module',
						'woocommerce.variations',
						'Creates, clones, updates, enables, disables, trashes, verifies, and rolls back WooCommerce variation structures.',
					),
					array( ShippingModule::MODULE_ID, 'Shipping Module', 'woocommerce.shipping', 'Schedules product and variation weight, dimensions, and shipping class with canonical decimal normalization.' ),
					array( TaxModule::MODULE_ID, 'Tax Module', 'woocommerce.tax', 'Schedules WooCommerce tax status and tax class independently from shipping fields.' ),
					array( ProductMediaModule::MODULE_ID, 'Product Media Module', 'woocommerce.media', 'Schedules featured images and ordered product galleries with attachment validation and rollback.' ),
					array( DownloadableProductModule::MODULE_ID, 'Downloadable Products Module', 'woocommerce.downloads', 'Schedules downloadable and virtual state, download files, limits, expiry, validation, and rollback.' ),
					array( ProductRelationshipsModule::MODULE_ID, 'Product Relationships Module', 'woocommerce.relationships', 'Schedules upsells, cross-sells, and ordered grouped-product relationships.' ),
					array( CostOfGoodsModule::MODULE_ID, 'Cost of Goods Module', 'woocommerce.cost-of-goods', 'Schedules native WooCommerce cost-of-goods values when the installed WooCommerce version supports them.' ),
					array( ExternalProductModule::MODULE_ID, 'External Products Module', 'woocommerce.external-products', 'Schedules external product URLs and purchase button text.' ),
				);

				foreach ( $field_module_definitions as $definition ) {
					$dependencies = array( 'scheduled-changes-provider-discovery', ProductIdentityModule::MODULE_ID, 'scheduled-changes-snapshots-rollback' );
					if ( VariationStructureModule::MODULE_ID === $definition[0] ) {
						$dependencies[] = 'scheduled-changes-execution';
					}

					$modules[] = new ModuleDefinition(
						$definition[0],
						$definition[1],
						SCHEDULED_CHANGES_WC_VERSION,
						'woocommerce-core',
						'woocommerce-core',
						$plugin_file,
						$status['verified_lifecycle'],
						$status['lifecycle_status'],
						$status['message'],
						$status['certification'],
						array(
							'dependencies'       => $dependencies,
							'capabilities'       => array( $definition[2] ),
							'package_form'       => ModuleDefinition::PACKAGE_BUNDLED_EXTENSION,
							'resources'          => array( ModuleDefinition::RESOURCE_FIELD => array( $definition[2] ) ),
							'visibility'         => ModuleDefinition::VISIBILITY_GROUPED_PUBLIC,
							'source_package'     => 'scheduled-changes-woocommerce-extension',
							'group'              => 'woocommerce',
							'description'        => $definition[3],
							'registration_phase' => ModuleDefinition::PHASE_PROVIDERS,
							'documentation_url'  => 'documentation/compatibility-and-modules.md',
						)
					);
				}

				return $modules;
			}
		);
	}

	/**
	 * Shows a concise dependency notice on admin screens.
	 *
	 * @param string $message Diagnostic message.
	 */
	private static function registerAdminNotice( string $message ): void {
		if ( ! is_admin() ) {
			return;
		}

		add_action(
			'admin_notices',
			static function () use ( $message ): void {
				printf(
					'<div class="notice notice-error"><p>%s</p></div>',
					esc_html( $message )
				);
			}
		);
	}

	/**
	 * Registers native WooCommerce field providers through the public SDK hook.
	 */
	private static function registerNativeProviderServices(): void {
		add_filter(
			'scheduled_changes_field_providers',
			static function ( array $providers ): array {
				if ( ! class_exists( NativeProductProvider::class ) ) {
					return $providers;
				}

				$identity    = new ProductIdentityModule();
				$attributes  = new ProductAttributesModule();
				$downloads   = new DownloadableProductModule();
				$providers[] = new NativeProductProvider(
					new NativeFieldSchema( self::selectedNativeFieldKeys() ),
					$identity->createCapabilities(),
					$attributes->createCodec(),
					$downloads->createCodec(),
					$identity->createSynchronizer()
				);
				$providers[] = new RegisteredMetaFieldProvider();
				$providers[] = new TaxonomyFieldProvider();
				if (
					class_exists( RawMetadataDiagnosticsModule::class )
					&& self::moduleSelected( self::RAW_METADATA_DIAGNOSTICS_MODULE_ID )
				) {
					$providers[] = new RawMetadataFieldProvider();
				}

				return $providers;
			}
		);
	}

	/**
	 * Registers native WooCommerce operation and created-target handlers.
	 */
	private static function registerOperationHandlers(): void {
		$factory = static function (
			array $handlers,
			?ProviderRegistry $providers = null,
			?FieldRegistry $fields = null,
			?RunRepository $runs = null
		): array {
			if ( null === $providers || null === $fields || ! class_exists( WooCommerceOperationHandler::class ) ) {
				return $handlers;
			}

			$handlers[] = new WooCommerceOperationHandler(
				$providers,
				$fields,
				( new VariationStructureModule() )->createService(),
				new InventoryAdjustmentService(),
				new ProductSynchronizer(),
				new DownloadCodec(),
				new TaxonomyTermService(),
				$runs,
				self::moduleSelected( InventoryModule::MODULE_ID ),
				self::moduleSelected( VariationStructureModule::MODULE_ID )
			);

			return $handlers;
		};

		add_filter( 'scheduled_changes_provider_operation_handlers', $factory, 10, 4 );
		add_filter( 'scheduled_changes_created_target_rollback_handlers', $factory, 10, 4 );
	}

	/** Blocks structural variation operations when a stored module requirement is unavailable. */
	private static function registerVariationValidationHooks(): void {
		add_filter(
			'scheduled_changes_validate_external_operation',
			static function ( $health, Operation $operation ) {
				if ( true !== $health ) {
					return $health;
				}

				$requirements = $operation->validationRequirements();
				$modules      = is_array( $requirements['modules'] ?? null ) ? $requirements['modules'] : array();
				$missing      = array_values(
					array_filter(
						$modules,
						static fn ( $module_id ): bool => is_string( $module_id ) && ! self::moduleSelected( $module_id )
					)
				);

				return array() === $missing
					? true
					: new \WP_Error(
						'scheduled_changes_variation_module_requirement_missing',
						'Structural variation operation requires unavailable modules: ' . implode( ', ', $missing ) . '.'
					);
			},
			10,
			2
		);
	}

	/**
	 * Registers WooCommerce recording workspace contracts.
	 */
	private static function registerRecordingWorkspace(): void {
		add_filter(
			'scheduled_changes_recording_workspace_adapter',
			static function (
				$adapter,
				ProviderRegistry $providers,
				FieldRegistry $fields,
				RecordingSessionRepository $recordings
			): RecordingWorkspaceAdapter {
				unset( $recordings );

				$woocommerce = ( new WooCommerceRecordingModule() )->createWorkspaceAdapter( $providers, $fields );

				if ( $adapter instanceof RecordingWorkspaceRouter ) {
					return $adapter->withAdapter( array( 'product' ), $woocommerce );
				}

				return new RecordingWorkspaceRouter( array( 'product' => $woocommerce ) );
			},
			10,
			4
		);

		add_filter(
			'scheduled_changes_recording_launch_compiler',
			static function (
				$compiler,
				ProviderRegistry $providers,
				FieldRegistry $fields,
				LaunchRepository $launches,
				LaunchVersionRepository $versions
			): RecordingLaunchCompilerContract {

				$woocommerce = ( new WooCommerceRecordingModule() )->createLaunchCompiler( $providers, $fields, $launches, $versions );

				if ( $compiler instanceof RecordingLaunchCompilerRouter ) {
					return $compiler->withCompiler( array( 'product' ), $woocommerce );
				}

				return new RecordingLaunchCompilerRouter( array( 'product' => $woocommerce ) );
			},
			10,
			5
		);
	}

	/**
	 * Registers WooCommerce-owned admin, REST, CLI, and diagnostic surfaces.
	 */
	private static function registerAdminAndDiagnosticSurfaces(): void {
		add_filter(
			'scheduled_changes_admin_target_row',
			static function ( array $target, array $stored_target ): array {
				return self::woocommerceTargetRow( $target, $stored_target );
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_recordable_targets',
			static function ( array $targets, int $limit ): array {
				return array_merge( $targets, self::recordableProductRows( $limit ) );
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_admin_system_rows',
			static function ( array $rows ): array {
				return array_merge( $rows, self::woocommerceSystemRows() );
			}
		);

		add_filter(
			'scheduled_changes_system_compatibility',
			static function ( array $compatibility ): array {
				$recommended                  = VersionPolicy::recommended();
				$compatibility['woocommerce'] = array(
					'providerId'  => 'woocommerce-core',
					'targetType'  => 'product',
					'current'     => defined( 'WC_VERSION' ) ? (string) WC_VERSION : '',
					'minimum'     => VersionPolicy::MIN_WOOCOMMERCE,
					'recommended' => (string) ( $recommended['woocommerce'] ?? '' ),
					'status'      => defined( 'WC_VERSION' )
						? VersionPolicy::classify( (string) WC_VERSION, VersionPolicy::MIN_WOOCOMMERCE, VersionPolicy::TESTED_WOOCOMMERCE )
						: 'unavailable',
				);

				return $compatibility;
			}
		);

		add_filter(
			'scheduled_changes_rest_launch_row',
			static function ( array $row ): array {
				$row['productCount'] = (int) ( $row['targetCount'] ?? 0 );

				return $row;
			}
		);

		add_filter(
			'scheduled_changes_rest_launch_schema',
			static function ( array $schema ): array {
				if ( is_array( $schema['properties'] ?? null ) ) {
					$schema['properties']['productCount'] = array( 'type' => 'integer' );
				}

				return $schema;
			}
		);

		add_filter(
			'scheduled_changes_system_status_payload',
			static function ( array $payload, ?ProviderManager $providers ): array {
				$compatibility = is_array( $payload['compatibility']['woocommerce'] ?? null )
					? $payload['compatibility']['woocommerce']
					: array();

				$payload['woocommerceVersion']         = (string) ( $compatibility['current'] ?? '' );
				$payload['minimumWooCommerce']         = (string) ( $compatibility['minimum'] ?? VersionPolicy::MIN_WOOCOMMERCE );
				$payload['woocommerceHandoff']         = ( new WooCommerceExtensionHandoff() )->report( $providers );
				$payload['recommended']['woocommerce'] = (string) ( $compatibility['recommended'] ?? '' );

				if ( is_array( $payload['recordings'] ?? null ) ) {
					$payload['recordings']['orphanedShadows'] = (int) ( $payload['recordings']['orphanedWorkspaces'] ?? 0 );
				}

				return $payload;
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_system_status_schema',
			static function ( array $schema ): array {
				if ( is_array( $schema['properties'] ?? null ) ) {
					$schema['properties']['woocommerceVersion'] = array( 'type' => 'string' );
					$schema['properties']['minimumWooCommerce'] = array( 'type' => 'string' );
					$schema['properties']['woocommerceHandoff'] = array( 'type' => 'object' );
				}

				return $schema;
			}
		);

		add_filter(
			'scheduled_changes_cli_doctor_payload',
			static function ( array $payload, ?ProviderManager $providers ): array {
				$payload['woocommerce_handoff'] = ( new WooCommerceExtensionHandoff() )->report( $providers );
				$payload['orphaned_shadows']    = (int) ( $payload['orphaned_workspaces'] ?? 0 );

				return $payload;
			},
			10,
			2
		);

		add_filter(
			'scheduled_changes_launch_templates',
			static fn ( array $templates ): array => array_merge( $templates, self::woocommerceLaunchTemplates() )
		);

		add_filter(
			'scheduled_changes_provider_diagnostic_contexts',
			static function ( array $contexts ): array {
				$contexts[] = new ObjectContext( 'product', 1, 'simple' );
				$contexts[] = new ObjectContext( 'product', 1, 'variable' );
				$contexts[] = new ObjectContext( 'variation', 1, 'variation' );

				return $contexts;
			}
		);

		$target_reference_resolver = static function ( $context, string $target_reference ): ?ObjectContext {
			if ( $context instanceof ObjectContext ) {
				return $context;
			}

				$product_id = self::targetProductId( $target_reference );
				$product    = $product_id > 0 && function_exists( 'wc_get_product' ) ? wc_get_product( $product_id ) : null;

			if ( ! $product instanceof \WC_Product ) {
				return null;
			}

				return new ObjectContext(
					$product instanceof \WC_Product_Variation ? 'variation' : 'product',
					$product_id,
					$product->get_type()
				);
		};
		add_filter( 'scheduled_changes_target_reference_context', $target_reference_resolver, 10, 2 );
		add_filter( 'scheduled_changes_cli_field_scan_context', $target_reference_resolver, 10, 2 );

		add_action( 'add_meta_boxes_product', array( self::class, 'addProductEditorActions' ) );
	}

	/**
	 * Adds Scheduled Changes shortcuts to the WooCommerce product editor.
	 *
	 * @param \WP_Post $post Product post object.
	 */
	public static function addProductEditorActions( \WP_Post $post ): void {
		add_meta_box(
			'scheduled-changes-product-actions',
			__( 'Scheduled Changes', 'jjs-post-scheduler' ),
			static function () use ( $post ): void {
				$product_id = (int) $post->ID;
				$links      = array(
					__( 'View scheduled launches', 'jjs-post-scheduler' ) => admin_url( 'admin.php?page=jjs-scheduler&product_id=' . $product_id ),
				);
				?>
				<ul>
					<?php foreach ( $links as $label => $url ) : ?>
						<li><a href="<?php echo esc_url( $url ); ?>"><?php echo esc_html( (string) $label ); ?></a></li>
					<?php endforeach; ?>
				</ul>
				<?php
			},
			'product',
			'side',
			'default'
		);
	}

	/**
	 * Enriches a neutral target row with WooCommerce labels and editor links.
	 *
	 * @param array<string,mixed> $target Target display row.
	 * @param array<string,mixed> $stored_target Stored target/source row.
	 * @return array<string,mixed>
	 */
	private static function woocommerceTargetRow( array $target, array $stored_target ): array {
		$provider_id = (string) ( $target['providerId'] ?? $stored_target['provider_id'] ?? $stored_target['source_provider_id'] ?? $stored_target['workspace_provider_id'] ?? '' );

		if ( '' !== $provider_id && 'woocommerce-core' !== $provider_id ) {
			return $target;
		}

		$product_id = (int) ( $target['id'] ?? $stored_target['target_id'] ?? $stored_target['object_id'] ?? 0 );
		$product    = $product_id > 0 && function_exists( 'wc_get_product' ) ? wc_get_product( $product_id ) : null;

		if ( ! $product instanceof \WC_Product ) {
			return $target;
		}

		$thumbnail = '';

		if ( function_exists( 'get_post_thumbnail_id' ) ) {
			$thumbnail_id = (int) get_post_thumbnail_id( $product_id );
			$thumbnail    = $thumbnail_id > 0 ? (string) wp_get_attachment_image_url( $thumbnail_id, 'thumbnail' ) : '';
		}

		$edit_url = get_edit_post_link( $product_id, 'raw' );
		$view_url = get_permalink( $product_id );

		$target['id']              = $product_id;
		$target['name']            = get_the_title( $product_id );
		$target['thumbnail']       = $thumbnail;
		$target['editUrl']         = is_string( $edit_url ) ? $edit_url : '';
		$target['viewUrl']         = is_string( $view_url ) ? $view_url : '';
		$target['providerId']      = 'woocommerce-core';
		$target['providerLabel']   = __( 'WooCommerce', 'jjs-post-scheduler' );
		$target['targetType']      = $product instanceof \WC_Product_Variation ? 'variation' : 'product';
		$target['subtype']         = $product->get_type();
		$target['typeLabel']       = $product instanceof \WC_Product_Variation
			? __( 'Variation', 'jjs-post-scheduler' )
			: __( 'Product', 'jjs-post-scheduler' );
		$target['icon']            = 'box';
		$target['canEdit']         = current_user_can( 'edit_post', $product_id );
		$target['sku']             = (string) $product->get_sku();
		$target['targetReference'] = '' !== (string) ( $target['targetReference'] ?? '' )
			? (string) $target['targetReference']
			: (string) ( $product instanceof \WC_Product_Variation ? 'variation:' : 'product:' ) . $product_id;

		return $target;
	}

	/**
	 * Returns WooCommerce products available to the dashboard recording picker.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	private static function recordableProductRows( int $limit ): array {
		if ( ! function_exists( 'get_posts' ) || ! function_exists( 'wc_get_product' ) ) {
			return array();
		}

		$rows = get_posts(
			array(
				'post_type'      => 'product',
				'post_status'    => array( 'publish', 'future', 'draft', 'pending', 'private' ),
				'posts_per_page' => max( 1, min( 100, $limit ) ),
				'orderby'        => 'modified',
				'order'          => 'DESC',
				'fields'         => 'ids',
			)
		);

		return array_values(
			array_filter(
				array_map(
					static fn ( int $product_id ): ?array => self::recordableProductRow( $product_id ),
					array_map( 'intval', is_array( $rows ) ? $rows : array() )
				)
			)
		);
	}

	/**
	 * Builds one WooCommerce recording picker row.
	 *
	 * @return array<string,mixed>|null
	 */
	private static function recordableProductRow( int $product_id ): ?array {
		if (
			$product_id <= 0
			|| ! function_exists( 'get_post_type' )
			|| 'product' !== get_post_type( $product_id )
			|| ! current_user_can( 'edit_post', $product_id )
			|| ShadowProductAccessGuard::isShadow( $product_id )
		) {
			return null;
		}

		$product = wc_get_product( $product_id );

		if ( ! $product instanceof \WC_Product || $product instanceof \WC_Product_Variation ) {
			return null;
		}

		return self::woocommerceTargetRow(
			array(
				'id'              => $product_id,
				'reference'       => (string) $product->get_sku(),
				'targetReference' => 'product:' . $product_id,
				'targetType'      => 'product',
				'subtype'         => $product->get_type(),
				'name'            => get_the_title( $product_id ),
				'thumbnail'       => '',
				'editUrl'         => '',
				'viewUrl'         => '',
				'upcomingCount'   => 0,
				'failedCount'     => 0,
				'nextChange'      => '',
				'providerId'      => 'woocommerce-core',
				'parentReference' => '',
			),
			array( 'target_id' => $product_id )
		);
	}

	/**
	 * Returns WooCommerce-specific system rows for admin/support diagnostics.
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	private static function woocommerceSystemRows(): array {
		$registered_metadata       = new RegisteredMetadataRegistry();
		$registered_metadata_count = count( $registered_metadata->forSubtype( 'product' ) )
			+ count( $registered_metadata->forSubtype( 'product_variation' ) );
		$product_types             = ( new ProductTypeDiscovery() )->all();
		$product_taxonomies        = ( new TaxonomyCatalog() )->productTaxonomies();
		$rest_schema_hints         = ( new RestSchemaInspector() )->productHints();
		$rows                      = array(
			array(
				'label' => __( 'WooCommerce', 'jjs-post-scheduler' ),
				'value' => defined( 'WC_VERSION' ) ? (string) WC_VERSION : __( 'Unavailable', 'jjs-post-scheduler' ),
			),
			array(
				'label' => __( 'Minimum WooCommerce', 'jjs-post-scheduler' ),
				'value' => VersionPolicy::MIN_WOOCOMMERCE,
			),
			array(
				'label' => __( 'Registered metadata schemas', 'jjs-post-scheduler' ),
				'value' => (string) $registered_metadata_count,
			),
			array(
				'label' => __( 'Discovered product types', 'jjs-post-scheduler' ),
				'value' => (string) count( $product_types ),
			),
			array(
				'label' => __( 'Discovered product taxonomies', 'jjs-post-scheduler' ),
				'value' => (string) count( $product_taxonomies ),
			),
			array(
				'label' => __( 'Product REST schema hints', 'jjs-post-scheduler' ),
				'value' => (string) count( $rest_schema_hints ),
			),
		);

		if (
			class_exists( RawMetadataDiagnosticsModule::class )
			&& self::moduleSelected( self::RAW_METADATA_DIAGNOSTICS_MODULE_ID )
		) {
			$rows[] = array(
				'label' => __( 'Advanced raw metadata', 'jjs-post-scheduler' ),
				'value' => ( new DiscoverySettings() )->rawMetadataEnabled() ? __( 'Enabled', 'jjs-post-scheduler' ) : __( 'Disabled by default', 'jjs-post-scheduler' ),
			);
		}

		return $rows;
	}

	/** Determines whether the resolved runtime selected one module. */
	private static function moduleSelected( string $module_id ): bool {
		$runtime = Plugin::instance()->moduleRuntime();

		if ( null !== $runtime ) {
			return in_array( $module_id, $runtime->bootOrderIds(), true );
		}

		static $early_selected = null;
		if ( null === $early_selected ) {
			$early_selected = ( new \ScheduledChanges\Extension\ModuleRuntime() )->bootOrderIds();
		}

		return in_array( $module_id, $early_selected, true );
	}

	/**
	 * Returns field keys contributed by selected WooCommerce data modules.
	 *
	 * @return string[]
	 */
	private static function selectedNativeFieldKeys(): array {
		$modules = array(
			new ProductIdentityModule(),
			new PricingModule(),
			new InventoryModule(),
			new ProductAttributesModule(),
			new ShippingModule(),
			new TaxModule(),
			new ProductMediaModule(),
			new DownloadableProductModule(),
			new ProductRelationshipsModule(),
			new CostOfGoodsModule(),
			new ExternalProductModule(),
		);
		$keys    = array();

		foreach ( $modules as $module ) {
			if ( self::moduleSelected( $module::MODULE_ID ) ) {
				$keys = array_merge( $keys, $module->fieldKeys() );
			}
		}

		return array_values( array_unique( $keys ) );
	}

	/**
	 * Returns WooCommerce-owned reusable launch blueprints.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	private static function woocommerceLaunchTemplates(): array {
		$templates = self::moduleSelected( PricingModule::MODULE_ID )
			? ( new PricingModule() )->templates()
			: array();

		if ( self::moduleSelected( InventoryModule::MODULE_ID ) ) {
			$templates = array_merge( $templates, ( new InventoryModule() )->templates() );
		}

		return $templates;
	}

	/**
	 * Resolves a WooCommerce product ID from legacy CLI input or target reference.
	 */
	private static function targetProductId( string $target_reference ): int {
		$target_reference = trim( $target_reference );

		if ( ctype_digit( $target_reference ) ) {
			return (int) $target_reference;
		}

		if ( preg_match( '/^(?:product|variation):(\d+)$/', $target_reference, $matches ) ) {
			return (int) $matches[1];
		}

		return 0;
	}

	/**
	 * Registers product-editor recording hooks after core services are booted.
	 */
	private static function registerProductEditorHooks(): void {
		add_action(
			'scheduled_changes_core_services_ready',
			static function ( CoreModuleServices $services ): void {
				( new ProductEditorRecordingModule() )->register( $services );
			}
		);
	}
}
