<?php
/**
 * WooCommerce operation executor.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\WooCommerce;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are escaped at UI boundaries.

use DateTimeInterface;
use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Domain\Operation\OperationType;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Domain\Run\FailureClassification;
use ScheduledChanges\Domain\Run\OperationRunStatus;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyOperationMode;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyTermService;
use ScheduledChanges\Execution\ExecutionState;
use ScheduledChanges\Execution\OperationExecutionResult;
use ScheduledChanges\Execution\ProviderOperationHandler;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\ExecutionContext;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\OperationResult;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Rollback\CreatedTargetRollbackHandler;
use ScheduledChanges\Rollback\CreatedVariationRollbackPolicy;
use ScheduledChanges\Rollback\EndBehaviorType;
use ScheduledChanges\Support\Json;
use ScheduledChanges\Support\PhpCompat;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Downloads\DownloadCodec;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Inventory\InventoryAdjustmentService;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Inventory\InventoryQuantityMode;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Variations\VariationRequest;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Variations\VariationService;

/**
 * Dispatches WooCommerce-owned handler IDs and verifies each observable write.
 */
final class WooCommerceOperationExecutor implements ProviderOperationHandler, CreatedTargetRollbackHandler {
	private ProviderRegistry $providers;
	private FieldRegistry $fields;
	private ?VariationService $variations;
	private ?InventoryAdjustmentService $inventory;
	private ?ProductSynchronizer $synchronizer;
	private ?DownloadCodec $downloads;
	private ?TaxonomyTermService $taxonomy_terms;
	private ?RunRepository $runs     = null;
	private bool $inventory_enabled  = true;
	private bool $variations_enabled = true;
	public function __construct( ProviderRegistry $providers, FieldRegistry $fields, ?VariationService $variations = null, ?InventoryAdjustmentService $inventory = null, ?ProductSynchronizer $synchronizer = null, ?DownloadCodec $downloads = null, ?TaxonomyTermService $taxonomy_terms = null, ?RunRepository $runs = null, bool $inventory_enabled = true, bool $variations_enabled = true ) {
		$this->providers          = $providers;
		$this->fields             = $fields;
		$this->variations         = $variations;
		$this->inventory          = $inventory;
		$this->synchronizer       = $synchronizer;
		$this->downloads          = $downloads;
		$this->taxonomy_terms     = $taxonomy_terms;
		$this->runs               = $runs;
		$this->inventory_enabled  = $inventory_enabled;
		$this->variations_enabled = $variations_enabled;
	}

	public function supports( Operation $operation ): bool {
		if ( OperationType::ADJUST_STOCK === $operation->type() && ! $this->inventory_enabled ) {
			return false;
		}
		if ( ! $this->variations_enabled && in_array( $operation->type(), array( OperationType::CREATE_VARIATION, OperationType::CLONE_VARIATION, OperationType::UPDATE_VARIATION, OperationType::ENABLE_VARIATION, OperationType::DISABLE_VARIATION, OperationType::TRASH_VARIATION ), true ) ) {
			return false;
		}
		$handler = strtolower( $operation->executionHandler() );

		if ( in_array( $handler, array( 'provider.write', 'provider.execute', 'system.capture_snapshot' ), true ) ) {
			return false;
		}

		return $this->handlerMatchesOperationType( $operation );
	}

	public function supportsCreatedTarget( Operation $operation ): bool {
		return $this->variations_enabled && in_array( $operation->type(), array( OperationType::CREATE_VARIATION, OperationType::CLONE_VARIATION ), true );
	}

	public function registerCreatedTarget( Operation $operation, array $source_results, ExecutionState $state ): void {
		if ( ! $this->supportsCreatedTarget( $operation ) ) {
			return;
		}

		$details      = $source_results[ $operation->uuid()->value() ]['details'] ?? array();
		$variation_id = is_array( $details ) ? (int) ( $details['variation_id'] ?? 0 ) : 0;

		if ( $variation_id > 0 ) {
			$state->register( $operation->targetReference(), $variation_id );
		}
	}

	public function rollbackCreatedTarget( Operation $operation, array $source_results, string $policy ): array {
		if ( ! $this->supportsCreatedTarget( $operation ) ) {
			return $this->failed( 'created_target_rollback_unsupported', 'This WooCommerce handler cannot roll back the created target.' );
		}

		$details      = $source_results[ $operation->uuid()->value() ]['details'] ?? array();
		$variation_id = is_array( $details ) ? (int) ( $details['variation_id'] ?? 0 ) : 0;

		$variation = $variation_id > 0 ? wc_get_product( $variation_id ) : null;

		if ( ! $variation instanceof \WC_Product_Variation ) {
			return $this->failed( 'created_variation_unavailable', 'The created variation could not be resolved.' );
		}

		if ( CreatedVariationRollbackPolicy::KEEP === $policy ) {
			return $this->skipped( 'created_variation_kept', 'The created variation was kept by policy.' );
		}

		if ( CreatedVariationRollbackPolicy::DISABLE === $policy ) {
			$this->variations()->setEnabled( $variation_id, false );
			$variation = wc_get_product( $variation_id );

			return $variation instanceof \WC_Product_Variation && 'private' === $variation->get_status( 'edit' )
				? $this->successful( 'Created variation disabled.', array( 'variation_id' => $variation_id ) )
				: $this->failed( 'variation_disable_verification_failed', 'Created variation was not disabled.' );
		}

		$this->variations()->trash( $variation_id );

		return 'trash' === get_post_status( $variation_id )
			? $this->successful( 'Created variation moved to trash.', array( 'variation_id' => $variation_id ) )
			: $this->failed( 'variation_remove_verification_failed', 'Created variation was not moved to trash.' );
	}

	public function apply(
		Operation $operation,
		TargetDefinition $target,
		ExecutionContext $context,
		ExecutionState $state
	): OperationExecutionResult {
		if ( ! $this->handlerMatchesOperationType( $operation ) ) {
			return OperationExecutionResult::failure(
				'handler_type_mismatch',
				'Execution handler is not compatible with the immutable operation type.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		try {
			switch ( $operation->executionHandler() ) {
				case 'provider.write':
					return $this->providerWrite( $operation, $target, $context, $state );
				case 'woocommerce.create_variation':
					return $this->createVariation( $operation, $target, $state );
				case 'woocommerce.clone_variation':
					return $this->cloneVariation( $operation, $target, $state );
				case 'woocommerce.update_variation':
					return $this->updateVariation( $operation, $target, $state );
				case 'woocommerce.enable_variation':
					return $this->setVariationEnabled( $target, $state, true );
				case 'woocommerce.disable_variation':
					return $this->setVariationEnabled( $target, $state, false );
				case 'woocommerce.trash_variation':
					return $this->trashVariation( $target, $state );
				case 'wordpress.create_taxonomy_term': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- Stable machine-readable handler ID.
				case 'WordPress.create_taxonomy_term':
					return $this->createTaxonomyTerm( $operation, $state );
				case 'wordpress.replace_taxonomy_terms': // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- Stable machine-readable handler ID.
				case 'WordPress.replace_taxonomy_terms':
					return $this->replaceTaxonomyTerms( $operation, $target, $state );
				case 'woocommerce.adjust_stock':
					return $this->adjustStock( $operation, $target, $state );
				case 'system.synchronize_parent':
					return $this->synchronize( $target, $state );
				case 'system.clear_product_cache':
					return $this->clearCache( $target, $state );
				case 'system.capture_snapshot':
					return $this->captureSnapshotToken();
				case 'woocommerce.end_created_variation':
					return $this->endCreatedVariation( $operation );
				case 'provider.execute':
					return $this->providerDefined( $operation, $context, $state );
				default:
					return OperationExecutionResult::failure(
						'unsupported_handler',
						'No execution handler is registered for ' . $operation->executionHandler() . '.',
						FailureClassification::UNSUPPORTED_HANDLER,
						false
					);
			}
		} catch ( \Throwable $throwable ) {
			$classification = $this->classification( $operation, $throwable->getMessage() );

			return OperationExecutionResult::failure(
				'handler_exception',
				$throwable->getMessage(),
				$classification,
				in_array(
					$classification,
					array(
						FailureClassification::TEMPORARY_PROVIDER,
						FailureClassification::EXTERNAL_TIMEOUT,
						FailureClassification::CACHE_PURGE,
						FailureClassification::NOTIFICATION,
					),
					true
				)
			);
		}
	}

	/**
	 * Prevents imported or extension-built plans from selecting an unrelated sink.
	 */
	private function handlerMatchesOperationType( Operation $operation ): bool {
		$handler = strtolower( $operation->executionHandler() );
		$type    = $operation->type();

		if ( 'provider.write' === $handler ) {
			return OperationType::writesField( $type ) && null !== $operation->fieldPath();
		}

		$expected = array(
			'woocommerce.create_variation'      => OperationType::CREATE_VARIATION,
			'woocommerce.clone_variation'       => OperationType::CLONE_VARIATION,
			'woocommerce.update_variation'      => OperationType::UPDATE_VARIATION,
			'woocommerce.enable_variation'      => OperationType::ENABLE_VARIATION,
			'woocommerce.disable_variation'     => OperationType::DISABLE_VARIATION,
			'woocommerce.trash_variation'       => OperationType::TRASH_VARIATION,
			'wordpress.create_taxonomy_term'    => OperationType::CREATE_TAXONOMY_TERM,
			'wordpress.replace_taxonomy_terms'  => OperationType::PROVIDER_DEFINED,
			'woocommerce.adjust_stock'          => OperationType::ADJUST_STOCK,
			'system.synchronize_parent'         => OperationType::SYNCHRONIZE_PARENT,
			'system.clear_product_cache'        => OperationType::CLEAR_CACHE,
			'system.capture_snapshot'           => OperationType::CAPTURE_SNAPSHOT,
			'woocommerce.end_created_variation' => OperationType::PROVIDER_DEFINED,
			'provider.execute'                  => OperationType::PROVIDER_DEFINED,
		);

		return isset( $expected[ $handler ] ) && $expected[ $handler ] === $type;
	}

	private function providerWrite(
		Operation $operation,
		TargetDefinition $target,
		ExecutionContext $execution,
		ExecutionState $state
	): OperationExecutionResult {
		$provider  = $this->providers->get( $operation->providerId() );
		$object_id = $state->resolve( $target->reference() );

		if ( null === $provider || null === $object_id || null === $operation->fieldPath() ) {
			return OperationExecutionResult::failure(
				'provider_context_unavailable',
				'Provider, target, or field context is unavailable.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$object_context = new ObjectContext(
			$target->targetType(),
			$object_id,
			$target->subtype()
		);
		$catalog        = $this->fields->discover( $object_context );
		$field          = $catalog->fields()->get( $operation->fieldPath() );

		if ( null === $field || $catalog->writableOwner( $field->id() )->id() !== $provider->id() ) {
			return OperationExecutionResult::failure(
				'provider_ownership_unavailable',
				'Canonical field ownership is unavailable at execution time.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$desired_value = $state->desiredValue( $operation );
		$result        = $provider->write( $object_context, $field, $desired_value, $execution );
		if ( ! $result->successful() ) {
			return $this->providerFailure( $operation, $result );
		}

		$desired = $provider->normalize( $field, $desired_value );
		$current = $provider->normalize( $field, $provider->read( $object_context, $field ) );

		if ( $current !== $desired ) {
			return OperationExecutionResult::failure(
				'verification_failed',
				'Provider readback did not match the requested canonical value.',
				FailureClassification::PERMANENT_VALIDATION,
				false,
				array( 'provider_code' => $result->code() )
			);
		}

		return OperationExecutionResult::success(
			$result->message(),
			$result->changed(),
			true,
			$result->details()
		);
	}

	private function createVariation(
		Operation $operation,
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$after      = $this->objectValue( $operation->after() );
		$parent_ref = (string) ( $after['parent_reference'] ?? $target->parentReference() ?? '' );
		$parent_id  = $state->resolve( $parent_ref );

		if ( null === $parent_id ) {
			return OperationExecutionResult::failure(
				'variation_parent_unresolved',
				'Variation parent target could not be resolved.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$parent_ready = $this->ensureVariableParent( $parent_id );

		if ( null !== $parent_ready ) {
			return $parent_ready;
		}

		$attributes = $this->stringMap( $after['attributes'] ?? array() );
		$values     = is_array( $after['values'] ?? null )
			? $after['values']
			: array_diff_key(
				$after,
				array_flip( array( 'parent_reference', 'attributes', 'source_variation_id' ) )
			);
		$result     = $this->variations()->createOrGet(
			$parent_id,
			new VariationRequest( $operation->uuid(), $attributes, $values )
		);

		$state->register( $target->reference(), $result->variationId() );
		$created = wc_get_product( $result->variationId() );

		if (
			! $created instanceof \WC_Product_Variation
			|| ! $this->variationMatches( $created, $parent_id, $attributes, $values )
		) {
			return OperationExecutionResult::failure(
				'variation_verification_failed',
				'Created variation could not be verified.',
				FailureClassification::PERMANENT_VALIDATION,
				false,
				$created instanceof \WC_Product_Variation
					? $this->variationVerificationDetails( $created, $parent_id, $attributes, $values )
					: array()
			);
		}

		return OperationExecutionResult::success(
			$result->created() ? 'Variation created and verified.' : 'Variation creation was already applied.',
			$result->created(),
			true,
			array( 'variation_id' => $result->variationId() )
		);
	}

	private function ensureVariableParent( int $parent_id ): ?OperationExecutionResult {
		$parent = wc_get_product( $parent_id );

		if ( $parent instanceof \WC_Product_Variable ) {
			return null;
		}

		if ( ! $parent instanceof \WC_Product || $parent instanceof \WC_Product_Variation ) {
			return OperationExecutionResult::failure(
				'variation_parent_invalid',
				'Variation parent must be an existing product.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$result = wp_set_object_terms( $parent_id, 'variable', 'product_type', false );

		if ( is_wp_error( $result ) ) {
			return OperationExecutionResult::failure(
				'variation_parent_conversion_failed',
				$result->get_error_message(),
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		clean_post_cache( $parent_id );
		clean_object_term_cache( $parent_id, 'product' );
		wc_delete_product_transients( $parent_id );
		$this->clearProductTypeCaches( $parent_id );
		\WC_Product_Variable::sync( $parent_id );
		$converted = wc_get_product( $parent_id );

		return $converted instanceof \WC_Product_Variable
			? null
			: OperationExecutionResult::failure(
				'variation_parent_conversion_failed',
				'Variation parent could not be converted to a variable product.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
	}

	private function clearProductTypeCaches( int $product_id ): void {
		if ( class_exists( '\WC_Cache_Helper' ) ) {
			$cache_key = \WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id;
			wp_cache_delete( $cache_key, 'products' );
			\WC_Cache_Helper::invalidate_cache_group( 'product_' . $product_id );
		}

		if (
			class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' )
			&& \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_instance_caching' )
			&& function_exists( 'wc_get_container' )
			&& class_exists( '\Automattic\WooCommerce\Internal\Caches\ProductCache' )
		) {
			wc_get_container()
				->get( \Automattic\WooCommerce\Internal\Caches\ProductCache::class )
				->remove( $product_id );
		}
	}

	private function cloneVariation(
		Operation $operation,
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$after      = $this->objectValue( $operation->after() );
		$parent_ref = (string) ( $after['parent_reference'] ?? $target->parentReference() ?? '' );
		$parent_id  = $state->resolve( $parent_ref );
		$source_id  = (int) ( $after['source_variation_id'] ?? 0 );
		$attributes = $this->stringMap( $after['attributes'] ?? array() );
		$values     = is_array( $after['values'] ?? null ) ? $after['values'] : array();

		if ( null === $parent_id || $source_id <= 0 ) {
			return OperationExecutionResult::failure(
				'variation_clone_context_invalid',
				'Variation clone parent or source is invalid.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$result = $this->variations()->cloneVariation(
			$parent_id,
			$source_id,
			new VariationRequest( $operation->uuid(), $attributes, $values )
		);
		$state->register( $target->reference(), $result->variationId() );
		$created = wc_get_product( $result->variationId() );

		return $created instanceof \WC_Product_Variation
			&& $this->variationMatches( $created, $parent_id, $attributes, $values )
				? OperationExecutionResult::success(
					$result->created() ? 'Variation cloned and verified.' : 'Variation clone was already applied.',
					$result->created(),
					true,
					array( 'variation_id' => $result->variationId() )
				)
				: OperationExecutionResult::failure(
					'variation_verification_failed',
					'Cloned variation did not match the requested overrides.',
					FailureClassification::PERMANENT_VALIDATION,
					false
				);
	}

	private function updateVariation(
		Operation $operation,
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$variation_id = $state->resolve( $target->reference() );
		$after        = $this->objectValue( $operation->after() );

		if ( null === $variation_id ) {
			return OperationExecutionResult::failure(
				'variation_target_unresolved',
				'Variation target could not be resolved.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$attributes = isset( $after['attributes'] )
			? $this->stringMap( $after['attributes'] )
			: null;
		$values     = is_array( $after['values'] ?? null )
			? $after['values']
			: array_diff_key( $after, array( 'attributes' => true ) );
		$this->variations()->update( $variation_id, $attributes, $values );
		$variation = wc_get_product( $variation_id );

		if (
			! $variation instanceof \WC_Product_Variation
			|| ! $this->variationMatches(
				$variation,
				$variation->get_parent_id(),
				$attributes,
				$values
			)
		) {
			return OperationExecutionResult::failure(
				'variation_verification_failed',
				'Updated variation did not match the requested values.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		return OperationExecutionResult::success(
			'Variation updated through WooCommerce CRUD.',
			true,
			true,
			array( 'variation_id' => $variation_id )
		);
	}

	private function setVariationEnabled(
		TargetDefinition $target,
		ExecutionState $state,
		bool $enabled
	): OperationExecutionResult {
		$variation_id = $state->resolve( $target->reference() );

		if ( null === $variation_id ) {
			return OperationExecutionResult::failure(
				'variation_target_unresolved',
				'Variation target could not be resolved.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$this->variations()->setEnabled( $variation_id, $enabled );
		$variation = wc_get_product( $variation_id );
		$expected  = $enabled ? 'publish' : 'private';

		if ( ! $variation instanceof \WC_Product_Variation || $variation->get_status( 'edit' ) !== $expected ) {
			return OperationExecutionResult::failure(
				'variation_status_verification_failed',
				'Variation status readback did not match.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		return OperationExecutionResult::success( 'Variation status updated and verified.', true );
	}

	private function trashVariation(
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$variation_id = $state->resolve( $target->reference() );

		if ( null === $variation_id ) {
			return OperationExecutionResult::failure(
				'variation_target_unresolved',
				'Variation target could not be resolved.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		if ( 'trash' !== get_post_status( $variation_id ) ) {
			$this->variations()->trash( $variation_id );
		}

		return 'trash' === get_post_status( $variation_id )
			? OperationExecutionResult::success( 'Variation trashed and verified.', true )
			: OperationExecutionResult::failure(
				'variation_trash_verification_failed',
				'Variation trash state could not be verified.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
	}

	private function createTaxonomyTerm( Operation $operation, ExecutionState $state ): OperationExecutionResult {
		$after    = $this->objectValue( $operation->after() );
		$token    = (string) ( $after['token'] ?? '' );
		$taxonomy = sanitize_key( (string) ( $after['taxonomy'] ?? '' ) );
		$name     = trim( (string) ( $after['name'] ?? '' ) );
		$slug     = sanitize_title( (string) ( $after['slug'] ?? $name ) );

		if ( '' === $token || '' === $taxonomy || '' === $name ) {
			return OperationExecutionResult::failure(
				'taxonomy_term_request_invalid',
				'Pending taxonomy term request is incomplete.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$taxonomy_object = get_taxonomy( $taxonomy );

		if ( ! $taxonomy_object instanceof \WP_Taxonomy ) {
			return OperationExecutionResult::failure(
				'taxonomy_missing',
				'Pending taxonomy term taxonomy is not registered.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$capability = (string) ( $taxonomy_object->cap->manage_terms ?? 'manage_terms' );

		if ( '' !== $capability && ! current_user_can( $capability ) ) {
			return OperationExecutionResult::failure(
				'permission_denied',
				'Current user cannot create the pending taxonomy term.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$existing = term_exists( '' !== $slug ? $slug : $name, $taxonomy );

		if ( is_array( $existing ) && (int) ( $existing['term_id'] ?? 0 ) > 0 ) {
			$term_id = (int) $existing['term_id'];
			$state->register( 'term:' . $token, $term_id );

			return OperationExecutionResult::success(
				'Pending taxonomy term already exists and was resolved.',
				false,
				true,
				array( 'term_id' => $term_id )
			);
		}

		$parent = (int) ( $after['parent_term_id'] ?? 0 );
		if ( $parent <= 0 && '' !== (string) ( $after['parent_token'] ?? '' ) ) {
			$parent = (int) ( $state->resolve( 'term:' . (string) $after['parent_token'] ) ?? 0 );
		}

		$created = wp_insert_term(
			$name,
			$taxonomy,
			array_filter(
				array(
					'slug'   => '' === $slug ? null : $slug,
					'parent' => $parent > 0 ? $parent : null,
				),
				static fn ( $value ): bool => null !== $value
			)
		);

		if ( is_wp_error( $created ) ) {
			return OperationExecutionResult::failure(
				'taxonomy_term_create_failed',
				$created->get_error_message(),
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$term_id = (int) ( $created['term_id'] ?? 0 );

		if ( $term_id <= 0 || ! term_exists( $term_id, $taxonomy ) ) {
			return OperationExecutionResult::failure(
				'taxonomy_term_verification_failed',
				'Created taxonomy term could not be verified.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$state->register( 'term:' . $token, $term_id );

		return OperationExecutionResult::success(
			'Pending taxonomy term created and verified.',
			true,
			true,
			array( 'term_id' => $term_id )
		);
	}

	private function replaceTaxonomyTerms(
		Operation $operation,
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$product_id = $state->resolve( $target->reference() );
		$after      = $this->objectValue( $operation->after() );
		$taxonomy   = sanitize_key( (string) ( $after['taxonomy'] ?? '' ) );

		if ( null === $product_id || '' === $taxonomy ) {
			return OperationExecutionResult::failure(
				'taxonomy_assignment_context_invalid',
				'Taxonomy assignment target or taxonomy is invalid.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$term_ids = array_values(
			array_filter(
				array_map( 'intval', is_array( $after['term_ids'] ?? null ) ? $after['term_ids'] : array() ),
				static fn ( int $term_id ): bool => $term_id > 0
			)
		);

		foreach ( is_array( $after['term_references'] ?? null ) ? $after['term_references'] : array() as $reference ) {
			$resolved = $state->resolve( (string) $reference );

			if ( null === $resolved ) {
				return OperationExecutionResult::failure(
					'pending_term_unresolved',
					'Pending taxonomy term was not resolved before assignment.',
					FailureClassification::PERMANENT_VALIDATION,
					false,
					array( 'term_reference' => (string) $reference )
				);
			}

			$term_ids[] = $resolved;
		}

		return $this->fromProviderResult(
			$operation,
			$this->taxonomyTerms()->apply(
				$product_id,
				$taxonomy,
				TaxonomyOperationMode::REPLACE,
				array_values( array_unique( $term_ids ) )
			)
		);
	}

	private function adjustStock(
		Operation $operation,
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$product_id = $state->resolve( $target->reference() );
		$after      = $this->objectValue( $operation->after() );
		$mode       = \ScheduledChanges\Support\EnumValue::tryFrom( InventoryQuantityMode::class, (string) ( $after['mode'] ?? '' ) );

		if ( null === $product_id || null === $mode ) {
			return OperationExecutionResult::failure(
				'inventory_context_invalid',
				'Inventory adjustment target or mode is invalid.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		return $this->fromProviderResult(
			$operation,
			$this->inventory()->apply(
				$product_id,
				$mode,
				(int) ( $after['amount'] ?? -1 ),
				$operation->idempotencyKey()
			)
		);
	}

	private function synchronize(
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$product = wc_get_product( $state->resolve( $target->reference() ) );

		if ( ! $product instanceof \WC_Product ) {
			return OperationExecutionResult::failure(
				'synchronization_target_missing',
				'WooCommerce synchronization target was not found.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$this->synchronizer()->synchronize( $product );

		return OperationExecutionResult::success( 'WooCommerce synchronization completed.', true );
	}

	private function clearCache(
		TargetDefinition $target,
		ExecutionState $state
	): OperationExecutionResult {
		$object_id = $state->resolve( $target->reference() );

		if ( null === $object_id ) {
			return OperationExecutionResult::failure(
				'cache_target_missing',
				'Cache target could not be resolved.',
				FailureClassification::CACHE_PURGE,
				true
			);
		}

		wc_delete_product_transients( $object_id );
		clean_post_cache( $object_id );

		return OperationExecutionResult::success( 'Product caches cleared.', true );
	}

	private function captureSnapshotToken(): OperationExecutionResult {
		return OperationExecutionResult::success(
			'Execution guard captured the required target snapshot.',
			false
		);
	}

	private function endCreatedVariation( Operation $operation ): OperationExecutionResult {
		$after               = $this->objectValue( $operation->after() );
		$source_version_id   = (int) ( $after['source_version_id'] ?? 0 );
		$source_operation_id = (string) ( $after['source_operation_id'] ?? '' );
		$behavior            = \ScheduledChanges\Support\EnumValue::tryFrom( \ScheduledChanges\Rollback\EndBehaviorType::class, (string) ( $after['behavior'] ?? '' ) );

		if ( null === $this->runs || $source_version_id <= 0 || '' === $source_operation_id || null === $behavior ) {
			return OperationExecutionResult::failure(
				'end_variation_context_invalid',
				'Created variation end behavior could not resolve its source.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$source_run = $this->runs->findLatestSuccessfulForVersion( $source_version_id );
		if ( null === $source_run || null === $source_run->id() ) {
			return OperationExecutionResult::failure(
				'end_variation_source_run_missing',
				'No successful source run is available for the created variation.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$result       = $this->runs->operationResults( $source_run->id() )[ $source_operation_id ] ?? array();
		$details      = is_array( $result['details'] ?? null ) ? $result['details'] : array();
		$variation_id = (int) ( $details['variation_id'] ?? 0 );
		$variation    = wc_get_product( $variation_id );

		if ( ! $variation instanceof \WC_Product_Variation ) {
			return OperationExecutionResult::failure(
				'end_variation_missing',
				'The variation created by the source launch no longer exists.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		if ( EndBehaviorType::DISABLE_CREATED_VARIATION === $behavior ) {
			$this->variations()->setEnabled( $variation_id, false );
			$variation = wc_get_product( $variation_id );

			return 'private' === $variation->get_status( 'edit' )
				? OperationExecutionResult::success(
					'Created variation disabled by scheduled end behavior.',
					true,
					true,
					array( 'variation_id' => $variation_id )
				)
				: OperationExecutionResult::failure(
					'end_variation_disable_verification_failed',
					'Scheduled end behavior did not disable the created variation.',
					FailureClassification::PERMANENT_VALIDATION,
					false
				);
		}

		if ( EndBehaviorType::REMOVE_CREATED_VARIATION !== $behavior ) {
			return OperationExecutionResult::failure(
				'end_variation_behavior_invalid',
				'Unsupported created variation end behavior.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$this->variations()->trash( $variation_id );

		return 'trash' === get_post_status( $variation_id )
			? OperationExecutionResult::success(
				'Created variation moved to trash by scheduled end behavior.',
				true,
				true,
				array( 'variation_id' => $variation_id )
			)
			: OperationExecutionResult::failure(
				'end_variation_remove_verification_failed',
				'Scheduled end behavior did not move the created variation to trash.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
	}

	private function providerDefined(
		Operation $operation,
		ExecutionContext $context,
		ExecutionState $state
	): OperationExecutionResult {
		$result = PublicHooks::executeProviderOperation( $operation, $context, $state );

		return $result ?? OperationExecutionResult::failure(
			'provider_handler_missing',
			'No extension handler accepted the provider-defined operation.',
			FailureClassification::UNSUPPORTED_HANDLER,
			false
		);
	}

	private function providerFailure(
		Operation $operation,
		OperationResult $result
	): OperationExecutionResult {
		$classification = $this->classification( $operation, $result->code() . ' ' . $result->message() );
		$retryable      = in_array(
			$classification,
			array(
				FailureClassification::TEMPORARY_PROVIDER,
				FailureClassification::EXTERNAL_TIMEOUT,
				FailureClassification::CACHE_PURGE,
				FailureClassification::NOTIFICATION,
			),
			true
		);

		return OperationExecutionResult::failure(
			$result->code(),
			$result->message(),
			$classification,
			$retryable,
			$result->details()
		);
	}

	private function fromProviderResult(
		Operation $operation,
		OperationResult $result
	): OperationExecutionResult {
		return $result->successful()
			? OperationExecutionResult::success(
				$result->message(),
				$result->changed(),
				true,
				$result->details()
			)
			: $this->providerFailure( $operation, $result );
	}

	private function classification( Operation $operation, string $message ): string {
		$haystack = strtolower( $message );

		if ( $operation->type() === OperationType::CLEAR_CACHE ) {
			return FailureClassification::CACHE_PURGE;
		}

		if ( $operation->type() === OperationType::SEND_NOTIFICATION ) {
			return FailureClassification::NOTIFICATION;
		}

		if (
			$operation->type() === OperationType::SEND_WEBHOOK
			|| strpos( $haystack, 'timeout' ) !== false
		) {
			return FailureClassification::EXTERNAL_TIMEOUT;
		}

		if (
			strpos( $haystack, 'temporary' ) !== false
			|| strpos( $haystack, 'temporarily' ) !== false
			|| strpos( $haystack, 'rate limit' ) !== false
			|| strpos( $haystack, 'unavailable' ) !== false
		) {
			return FailureClassification::TEMPORARY_PROVIDER;
		}

		return FailureClassification::PERMANENT_VALIDATION;
	}

	/**
	 * @param array<string,string>|null $attributes Expected variation attributes.
	 * @param array<string,mixed>       $values Expected WooCommerce CRUD values.
	 */
	private function variationMatches(
		\WC_Product_Variation $variation,
		int $parent_id,
		?array $attributes,
		array $values
	): bool {
		if ( $variation->get_parent_id() !== $parent_id ) {
			return false;
		}

		if (
			null !== $attributes
			&& $this->normalizedAttributes( $attributes )
				!== $this->normalizedAttributes( $variation->get_attributes( 'edit' ) )
		) {
			return false;
		}

		foreach ( $values as $key => $expected ) {
			$key    = (string) $key;
			$actual = $this->variationValue( $variation, $key );

			if (
				$this->normalizeVariationValue( $key, $expected )
				!== $this->normalizeVariationValue( $key, $actual )
			) {
				return false;
			}
		}

		return true;
	}

	/**
	 * @param array<string,string>|null $attributes Expected variation attributes.
	 * @param array<string,mixed>       $values Expected WooCommerce CRUD values.
	 * @return array<string,mixed>
	 */
	private function variationVerificationDetails(
		\WC_Product_Variation $variation,
		int $parent_id,
		?array $attributes,
		array $values
	): array {
		$actual_values = array();

		foreach ( array_keys( $values ) as $key ) {
			$key                   = (string) $key;
			$actual_values[ $key ] = $this->variationValue( $variation, $key );
		}

		return array(
			'expected' => array(
				'parent_id'  => $parent_id,
				'attributes' => null === $attributes ? null : $this->normalizedAttributes( $attributes ),
				'values'     => $values,
			),
			'actual'   => array(
				'parent_id'  => $variation->get_parent_id(),
				'attributes' => $this->normalizedAttributes( $variation->get_attributes( 'edit' ) ),
				'values'     => $actual_values,
			),
		);
	}

	/**
	 * @param array<string,mixed> $attributes Variation attributes.
	 * @return array<string,string>
	 */
	private function normalizedAttributes( array $attributes ): array {
		$normalized = array();

		foreach ( $attributes as $name => $value ) {
			$key                = sanitize_title( str_replace( 'attribute_', '', (string) $name ) );
			$normalized[ $key ] = sanitize_title( (string) $value );
		}

		ksort( $normalized );

		return $normalized;
	}

	/**
	 * @return mixed
	 */
	private function variationValue( \WC_Product_Variation $variation, string $key ) {
		switch ( $key ) {
			case 'status':
				return $variation->get_status( 'edit' );
			case 'regular_price':
				return $variation->get_regular_price( 'edit' );
			case 'sale_price':
				return $variation->get_sale_price( 'edit' );
			case 'sale_start':
				return $variation->get_date_on_sale_from( 'edit' );
			case 'sale_end':
				return $variation->get_date_on_sale_to( 'edit' );
			case 'sku':
				return $variation->get_sku( 'edit' );
			case 'global_unique_id':
				return PhpCompat::methodExists( $variation, 'get_global_unique_id' )
							? $variation->get_global_unique_id( 'edit' )
							: null;
			case 'manage_stock':
				return $variation->get_manage_stock( 'edit' );
			case 'stock_quantity':
				return $variation->get_stock_quantity( 'edit' );
			case 'stock_status':
				return $variation->get_stock_status( 'edit' );
			case 'backorders':
				return $variation->get_backorders( 'edit' );
			case 'weight':
				return $variation->get_weight( 'edit' );
			case 'length':
				return $variation->get_length( 'edit' );
			case 'width':
				return $variation->get_width( 'edit' );
			case 'height':
				return $variation->get_height( 'edit' );
			case 'shipping_class_id':
				return $variation->get_shipping_class_id( 'edit' );
			case 'tax_class':
				return $variation->get_tax_class( 'edit' );
			case 'image_id':
				return $variation->get_image_id( 'edit' );
			case 'downloadable':
				return $variation->get_downloadable( 'edit' );
			case 'virtual':
				return $variation->get_virtual( 'edit' );
			case 'downloads':
				return $variation->get_downloads( 'edit' );
			case 'download_limit':
				return $variation->get_download_limit( 'edit' );
			case 'download_expiry':
				return $variation->get_download_expiry( 'edit' );
			case 'description':
				return $variation->get_description( 'edit' );
			case 'purchase_note':
				return $variation->get_purchase_note( 'edit' );
			case 'menu_order':
				return $variation->get_menu_order();
			default:
				throw new \RuntimeException( 'Unsupported variation verification value: ' . $key );
		}
	}

	/**
	 * @param mixed $value
	 * @return mixed
	 */
	private function normalizeVariationValue( string $key, $value ) {
		switch ( $key ) {
			case 'regular_price':
			case 'sale_price':
			case 'weight':
			case 'length':
			case 'width':
			case 'height':
				return wc_format_decimal( (string) $value );
			case 'manage_stock':
			case 'downloadable':
			case 'virtual':
				return (bool) $value;
			case 'stock_quantity':
			case 'download_limit':
			case 'download_expiry':
			case 'menu_order':
				return null === $value ? null : (int) $value;
			case 'shipping_class_id':
			case 'image_id':
				return (int) $value <= 0 ? 0 : (int) $value;
			case 'sale_start':
			case 'sale_end':
				return $this->dateTimestamp( $value );
			case 'downloads':
				return Json::encode( $this->normalizedDownloads( $value ) );
			default:
				return (string) $value;
		}
	}

	/**
	 * @param mixed $value
	 */
	private function dateTimestamp( $value ): ?int {
		if ( null === $value || '' === $value ) {
			return null;
		}

		if ( $value instanceof DateTimeInterface ) {
			return $value->getTimestamp();
		}

		if ( is_int( $value ) || ctype_digit( (string) $value ) ) {
			return (int) $value;
		}

		return wc_string_to_datetime( (string) $value )->getTimestamp();
	}

	/**
	 * @return array<int,array{id:string,name:string,file:string,enabled:bool}>
	 * @param mixed $value
	 */
	private function normalizedDownloads( $value ): array {
		if ( ! is_array( $value ) ) {
			return array();
		}

		$contains_objects = array_filter(
			$value,
			static fn ( $download ): bool => $download instanceof \WC_Product_Download
		);

		if ( array() !== $contains_objects ) {
			return $this->downloads()->encode( $value );
		}

		$downloads = array_map(
			static fn ( array $download ): array => array(
				'id'      => (string) ( $download['id'] ?? '' ),
				'name'    => (string) ( $download['name'] ?? '' ),
				'file'    => (string) ( $download['file'] ?? '' ),
				'enabled' => (bool) ( $download['enabled'] ?? true ),
			),
			array_filter( $value, 'is_array' )
		);
		usort(
			$downloads,
			static fn ( array $left, array $right ): int => strcmp( $left['id'], $right['id'] )
		);

		return $downloads;
	}

	private function variations(): VariationService {
		if ( null === $this->variations ) {
			$this->variations = new VariationService();
		}

		return $this->variations;
	}

	private function inventory(): InventoryAdjustmentService {
		if ( null === $this->inventory ) {
			$this->inventory = new InventoryAdjustmentService();
		}

		return $this->inventory;
	}

	private function synchronizer(): ProductSynchronizer {
		if ( null === $this->synchronizer ) {
			$this->synchronizer = new ProductSynchronizer();
		}

		return $this->synchronizer;
	}

	private function downloads(): DownloadCodec {
		if ( null === $this->downloads ) {
			$this->downloads = new DownloadCodec();
		}

		return $this->downloads;
	}

	private function taxonomyTerms(): TaxonomyTermService {
		if ( null === $this->taxonomy_terms ) {
			$this->taxonomy_terms = new TaxonomyTermService();
		}

		return $this->taxonomy_terms;
	}

	/**
	 * @return array<string,mixed>
	 * @param mixed $value
	 */
	private function objectValue( $value ): array {
		return is_array( $value ) ? $value : array();
	}

	/**
	 * @return array<string,string>
	 * @param mixed $value
	 */
	private function stringMap( $value ): array {
		if ( ! is_array( $value ) ) {
			return array();
		}

		$result = array();
		foreach ( $value as $key => $item ) {
			$result[ (string) $key ] = (string) $item;
		}

		return $result;
	}

	/**
	 * @param array<string,mixed> $details Result details.
	 * @return array<string,mixed>
	 */
	private function successful( string $message, array $details = array() ): array {
		return array(
			'status'   => OperationRunStatus::SUCCESSFUL,
			'code'     => 'success',
			'message'  => $message,
			'verified' => true,
			'details'  => $details,
		);
	}

	/**
	 * @return array<string,mixed>
	 */
	private function skipped( string $code, string $message ): array {
		return array(
			'status'   => OperationRunStatus::SKIPPED,
			'code'     => $code,
			'message'  => $message,
			'verified' => true,
			'details'  => array(),
		);
	}

	/**
	 * @param array<string,mixed> $details Result details.
	 * @return array<string,mixed>
	 */
	private function failed( string $code, string $message, array $details = array() ): array {
		return array(
			'status'   => OperationRunStatus::FAILED,
			'code'     => $code,
			'message'  => $message,
			'verified' => false,
			'details'  => $details,
		);
	}
}
