<?php
/**
 * Shadow product cloner.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Recording;

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

use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Recording\RecordingCloneContext;
use ScheduledChanges\Recording\RecordingCloneLifecycle;
use ScheduledChanges\Recording\RecordingCloneStrategyResult;
use ScheduledChanges\Support\PhpCompat;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Attributes\AttributeCodec;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Downloads\DownloadCodec;
use ScheduledChangesWooCommerce\CoreCompat\WooCommerce\ProductTypeCapabilities;

/**
 * Creates hidden WooCommerce shadow products for standard Record Changes.
 */
final class ShadowProductCloner {
	private UniquePlaceholderService $placeholders;
	private AttributeCodec $attributes;

	private DownloadCodec $downloads;
	private RecordingCloneLifecycle $clone_lifecycle;
	private ProductTypeCapabilities $capabilities;

	public function __construct(
		?UniquePlaceholderService $placeholders = null,
		?AttributeCodec $attributes = null,
		?DownloadCodec $downloads = null,
		?RecordingCloneLifecycle $clone_lifecycle = null,
		?ProductTypeCapabilities $capabilities = null
	) {
		$placeholders        ??= new UniquePlaceholderService();
		$this->placeholders    = $placeholders;
		$this->attributes      = $attributes ?? new AttributeCodec();
		$this->downloads       = $downloads ?? new DownloadCodec();
		$this->clone_lifecycle = $clone_lifecycle ?? new RecordingCloneLifecycle();
		$this->capabilities    = $capabilities ?? new ProductTypeCapabilities();
	}

	/**
	 * Clones a source product without mutating the source.
	 */
	public function clone( int $source_product_id, Uuid $session_uuid, ?int $owner_id = null ): ShadowCloneResult {
		ShadowProductAccessGuard::ensurePostStatusRegistered();

		$source = wc_get_product( $source_product_id );

		if ( ! $source instanceof \WC_Product || $source instanceof \WC_Product_Variation ) {
			throw new \RuntimeException( 'Record Changes requires a source product.' );
		}

		$shadow      = $this->newProductForType( $source );
		$placeholder = array();

		$this->copyProductFields( $source, $shadow, $session_uuid, $placeholder );
		$shadow_id = $shadow->save();

		wp_update_post(
			array(
				'ID'            => $shadow_id,
				'post_status'   => ShadowProductAccessGuard::POST_STATUS,
				'post_name'     => 'scheduled-changes-recording-' . substr( $session_uuid->value(), 0, 12 ) . '-' . $source_product_id,
				'post_password' => (string) get_post_field( 'post_password', $source_product_id, 'raw' ),
				'post_author'   => (int) get_post_field( 'post_author', $source_product_id, 'raw' ),
				'post_date'     => (string) get_post_field( 'post_date', $source_product_id, 'raw' ),
				'post_date_gmt' => (string) get_post_field( 'post_date_gmt', $source_product_id, 'raw' ),
			)
		);
		$this->copyPostLevelFields( $source_product_id, $shadow_id );
		ShadowProductAccessGuard::markShadow(
			$shadow_id,
			$session_uuid->value(),
			$source_product_id,
			$source_product_id,
			'product'
		);
		update_post_meta(
			$shadow_id,
			ShadowProductAccessGuard::INTENDED_SLUG_META,
			$this->getProductValue( $source, 'get_slug', 'edit' )
		);
		update_post_meta(
			$shadow_id,
			ShadowProductAccessGuard::INTENDED_STATUS_META,
			$source->get_status( 'edit' )
		);
		update_post_meta(
			$shadow_id,
			ShadowProductAccessGuard::INTENDED_VISIBILITY_META,
			$this->getProductValue( $source, 'get_catalog_visibility', 'edit' )
		);

		$variation_map         = array(
			'source_to_shadow'   => array(),
			'shadow_to_source'   => array(),
			'stable_references'  => array(),
			'attribute_mappings' => array(),
		);
		$variation_placeholder = array();

		if ( $source instanceof \WC_Product_Variable ) {
			$clone_data            = $this->cloneVariations(
				$source,
				$shadow_id,
				$session_uuid,
				$variation_map
			);
			$variation_map         = $clone_data['variation_map'];
			$variation_placeholder = $clone_data['placeholder_map'];
			\WC_Product_Variable::sync( $shadow_id );
			wc_delete_product_transients( $shadow_id );
		}

		$placeholder_context = array(
			'product'    => $placeholder,
			'variations' => $variation_placeholder,
		);
		$clone_context       = new RecordingCloneContext(
			$session_uuid,
			$source_product_id,
			$shadow_id,
			$owner_id ?? (int) get_current_user_id(),
			$variation_map,
			'clone',
			$source->get_type(),
			$placeholder_context,
			'delete_shadow'
		);
		$lifecycle_data      = $this->runAfterCloneLifecycle( $clone_context );

		return new ShadowCloneResult(
			$shadow_id,
			$variation_map,
			array(
				'clone_provider'      => 'woocommerce-core',
				'product_type'        => $source->get_type(),
				'clone_lifecycle'     => $lifecycle_data->toArray(),
				'unique_placeholders' => $placeholder_context,
			)
		);
	}

	public function cleanupAddOnCloneData( RecordingCloneContext $context, string $reason ): RecordingCloneStrategyResult {
		return $this->clone_lifecycle->cleanup( $context, $reason );
	}

	private function runAfterCloneLifecycle( RecordingCloneContext $context ): RecordingCloneStrategyResult {
		$result = $this->clone_lifecycle->afterClone( $context );

		if ( ! $result->hasFailures() ) {
			return $result;
		}

		$this->clone_lifecycle->cleanup( $context, 'clone_failed' );

		throw new \RuntimeException(
			'Recording clone lifecycle failed: ' . $result->failureSummary()
		);
	}

	private function newProductForType( \WC_Product $source ): \WC_Product {
		switch ( $source->get_type() ) {
			case 'simple':
				$class_name = 'WC_Product_Simple';
				break;
			case 'variable':
				$class_name = 'WC_Product_Variable';
				break;
			case 'grouped':
				$class_name = 'WC_Product_Grouped';
				break;
			case 'external':
				$class_name = 'WC_Product_External';
				break;
			default:
				$class_name = '';
				break;
		}

		if ( '' === $class_name || ! class_exists( $class_name ) ) {
			throw new \RuntimeException( 'Record Changes native shadow cloning supports simple, variable, grouped, and external products.' );
		}

		return new $class_name();
	}

	/**
	 * @param array<string,mixed> $placeholder_map Placeholder metadata.
	 */
	private function copyProductFields(
		\WC_Product $source,
		\WC_Product $shadow,
		Uuid $session_uuid,
		array &$placeholder_map
	): void {
		$this->setProductValue( $shadow, 'set_name', $this->getProductValue( $source, 'get_name', 'edit' ) );
		$shadow->set_status( ShadowProductAccessGuard::POST_STATUS );
		$this->setProductValue( $shadow, 'set_catalog_visibility', 'hidden' );
		$this->setProductValue( $shadow, 'set_featured', $this->getProductValue( $source, 'get_featured', 'edit' ) );
		$shadow->set_description( $source->get_description( 'edit' ) );
		$this->setProductValue( $shadow, 'set_short_description', $this->getProductValue( $source, 'get_short_description', 'edit' ) );
		$shadow->set_menu_order( $source->get_menu_order() );
		$this->setProductValue( $shadow, 'set_reviews_allowed', $this->getProductValue( $source, 'get_reviews_allowed', 'edit' ) );
		$shadow->set_purchase_note( $source->get_purchase_note( 'edit' ) );
		$this->copyCommercialFields( $source, $shadow, $session_uuid, $source->get_id(), $placeholder_map );
		$this->copyMediaAndRelationships( $source, $shadow );

		if ( 'external' === $source->get_type() && 'external' === $shadow->get_type() ) {
			$this->setProductValue( $shadow, 'set_product_url', $this->getProductValue( $source, 'get_product_url', 'edit' ) );
			$this->setProductValue( $shadow, 'set_button_text', $this->getProductValue( $source, 'get_button_text', 'edit' ) );
		}

		if ( 'grouped' === $source->get_type() && 'grouped' === $shadow->get_type() ) {
			$this->setProductValue( $shadow, 'set_children', $this->getProductValue( $source, 'get_children' ) );
		}

		if ( $source instanceof \WC_Product_Variable && $shadow instanceof \WC_Product_Variable ) {
			$shadow->set_attributes(
				$this->attributes->decode(
					$this->attributes->encode( $source->get_attributes( 'edit' ) ),
					false
				)
			);
			$this->setProductValue( $shadow, 'set_default_attributes', $this->getProductValue( $source, 'get_default_attributes', 'edit' ) );
		}
	}

	/**
	 * @param array<string,mixed> $placeholder_map Placeholder metadata.
	 */
	private function copyVariationFields(
		\WC_Product_Variation $source,
		\WC_Product_Variation $shadow,
		Uuid $session_uuid,
		array &$placeholder_map
	): void {
		$this->setProductValue( $shadow, 'set_name', $this->getProductValue( $source, 'get_name', 'edit' ) );
		$shadow->set_status( ShadowProductAccessGuard::POST_STATUS );
		$shadow->set_description( $source->get_description( 'edit' ) );
		$shadow->set_menu_order( $source->get_menu_order() );
		$shadow->set_attributes( $source->get_attributes( 'edit' ) );
		$this->copyCommercialFields( $source, $shadow, $session_uuid, $source->get_id(), $placeholder_map );
		$shadow->set_image_id( $source->get_image_id( 'edit' ) );
	}

	/**
	 * @param array<string,mixed> $placeholder_map Placeholder metadata.
	 */
	private function copyCommercialFields(
		\WC_Product $source,
		\WC_Product $shadow,
		Uuid $session_uuid,
		int $source_object_id,
		array &$placeholder_map
	): void {
		$shadow->set_regular_price( $source->get_regular_price( 'edit' ) );
		$shadow->set_sale_price( $source->get_sale_price( 'edit' ) );
		$shadow->set_date_on_sale_from( $source->get_date_on_sale_from( 'edit' ) );
		$shadow->set_date_on_sale_to( $source->get_date_on_sale_to( 'edit' ) );
		$shadow->set_manage_stock( $source->get_manage_stock( 'edit' ) );
		$shadow->set_stock_quantity( $source->get_stock_quantity( 'edit' ) );
		$shadow->set_stock_status( $source->get_stock_status( 'edit' ) );
		$shadow->set_backorders( $source->get_backorders( 'edit' ) );
		$this->setProductValue( $shadow, 'set_sold_individually', $this->getProductValue( $source, 'get_sold_individually', 'edit' ) );
		$this->setProductValue( $shadow, 'set_low_stock_amount', $this->getProductValue( $source, 'get_low_stock_amount', 'edit' ) );
		$shadow->set_weight( $source->get_weight( 'edit' ) );
		$shadow->set_length( $source->get_length( 'edit' ) );
		$shadow->set_width( $source->get_width( 'edit' ) );
		$shadow->set_height( $source->get_height( 'edit' ) );
		$shadow->set_shipping_class_id( $source->get_shipping_class_id( 'edit' ) );
		$this->setProductValue( $shadow, 'set_tax_status', $this->getProductValue( $source, 'get_tax_status', 'edit' ) );
		$shadow->set_tax_class( $source->get_tax_class( 'edit' ) );
		$shadow->set_downloadable( $source->get_downloadable( 'edit' ) );
		$shadow->set_virtual( $source->get_virtual( 'edit' ) );
		$source_downloads = $source->get_downloads( 'edit' );
		$shadow->set_downloads( $this->downloads->decode( $this->downloads->encode( $source_downloads ) ) );
		$shadow->set_download_limit( $source->get_download_limit( 'edit' ) );
		$shadow->set_download_expiry( $source->get_download_expiry( 'edit' ) );
		$this->copyUniqueFields( $source, $shadow, $session_uuid, $source_object_id, $placeholder_map );

		if ( $this->capabilities->supports( $source, 'cogs' ) && method_exists( $source, 'get_cogs_value' ) && method_exists( $shadow, 'set_cogs_value' ) ) {
			$shadow->set_cogs_value( $source->get_cogs_value() );
		}
	}

	private function copyMediaAndRelationships( \WC_Product $source, \WC_Product $shadow ): void {
		$shadow->set_image_id( $source->get_image_id( 'edit' ) );
		$this->setProductValue( $shadow, 'set_gallery_image_ids', $this->getProductValue( $source, 'get_gallery_image_ids', 'edit' ) );
		$this->setProductValue( $shadow, 'set_upsell_ids', $this->getProductValue( $source, 'get_upsell_ids', 'edit' ) );
		$this->setProductValue( $shadow, 'set_cross_sell_ids', $this->getProductValue( $source, 'get_cross_sell_ids', 'edit' ) );
	}

	private function copyPostLevelFields( int $source_product_id, int $shadow_product_id ): void {
		if ( ! taxonomy_exists( 'pos_product_visibility' ) ) {
			return;
		}

		wp_set_object_terms(
			$shadow_product_id,
			has_term( 'pos-hidden', 'pos_product_visibility', $source_product_id )
				? array( 'pos-hidden' )
				: array(),
			'pos_product_visibility',
			false
		);
	}

	/**
	 * @param array<string,mixed> $placeholder_map Placeholder metadata.
	 */
	private function copyUniqueFields(
		\WC_Product $source,
		\WC_Product $shadow,
		Uuid $session_uuid,
		int $source_object_id,
		array &$placeholder_map
	): void {
		$sku = (string) $source->get_sku( 'edit' );

		if ( '' !== $sku ) {
			$placeholder = $this->placeholders->placeholder(
				$session_uuid,
				$source_object_id,
				'inventory.sku'
			);
			$shadow->set_sku( $placeholder );
			$placeholder_map['inventory.sku'] = array(
				'source_value' => $sku,
				'placeholder'  => $placeholder,
			);
		}

		if ( ! PhpCompat::methodExists( $source, 'get_global_unique_id' ) || ! PhpCompat::methodExists( $shadow, 'set_global_unique_id' ) ) {
			return;
		}

		$global_unique_id = (string) $source->get_global_unique_id( 'edit' );

		if ( '' !== $global_unique_id ) {
			$placeholder = $this->placeholders->placeholder(
				$session_uuid,
				$source_object_id,
				'inventory.global_unique_id'
			);
			$shadow->set_global_unique_id( $placeholder );
			$placeholder_map['inventory.global_unique_id'] = array(
				'source_value' => $global_unique_id,
				'placeholder'  => $placeholder,
			);
		}
	}

	/**
	 * @param array<string,mixed> $variation_map Source/shadow variation map.
	 * @return array{variation_map:array<string,mixed>,placeholder_map:array<string,mixed>}
	 */
	private function cloneVariations(
		\WC_Product_Variable $source,
		int $shadow_product_id,
		Uuid $session_uuid,
		array $variation_map
	): array {
		$placeholder_map = array();

		foreach ( $source->get_children() as $source_variation_id ) {
			$source_variation = wc_get_product( (int) $source_variation_id );

			if ( ! $source_variation instanceof \WC_Product_Variation ) {
				continue;
			}

			$shadow_variation = new \WC_Product_Variation();
			$shadow_variation->set_parent_id( $shadow_product_id );
			$variation_placeholders = array();

			$this->copyVariationFields(
				$source_variation,
				$shadow_variation,
				$session_uuid,
				$variation_placeholders
			);

			$shadow_variation_id = $shadow_variation->save();
			wp_update_post(
				array(
					'ID'          => $shadow_variation_id,
					'post_status' => ShadowProductAccessGuard::POST_STATUS,
				)
			);
			ShadowProductAccessGuard::markShadow(
				$shadow_variation_id,
				$session_uuid->value(),
				$source->get_id(),
				$source_variation->get_id(),
				'variation'
			);
			update_post_meta(
				$shadow_variation_id,
				ShadowProductAccessGuard::INTENDED_STATUS_META,
				$source_variation->get_status( 'edit' )
			);

			$source_key                                       = (string) $source_variation->get_id();
			$shadow_key                                       = (string) $shadow_variation_id;
			$variation_map['source_to_shadow'][ $source_key ] = $shadow_variation_id;
			$variation_map['shadow_to_source'][ $shadow_key ] = $source_variation->get_id();
			$variation_map['stable_references'][ 'variation:' . $source_variation->get_id() ] = 'variation:' . $shadow_variation_id;
			$variation_map['attribute_mappings'][ $source_key ]                               = $source_variation->get_attributes( 'edit' );
			$placeholder_map[ 'variation:' . $source_key ]                                    = $variation_placeholders;
		}

		return array(
			'variation_map'   => $variation_map,
			'placeholder_map' => $placeholder_map,
		);
	}

	/**
	 * Calls an optional WooCommerce getter only when the runtime object supports it.
	 *
	 * @param mixed ...$arguments
	 * @return mixed
	 */
	private function getProductValue( \WC_Product $product, string $method, ...$arguments ) {
		if ( ! method_exists( $product, $method ) ) {
			return null;
		}

		return $product->{$method}( ...$arguments );
	}

	/**
	 * Calls an optional WooCommerce setter only when the runtime object supports it.
	 *
	 * @param mixed $value
	 */
	private function setProductValue( \WC_Product $product, string $method, $value ): void {
		if ( ! method_exists( $product, $method ) ) {
			return;
		}

		$product->{$method}( $value );
	}
}
