<?php
/**
 * Recording snapshot capture.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Recording;

// phpcs:disable WordPress.DB -- Scheduler-owned custom tables require direct wpdb queries.

use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Support\Json;

/**
 * Captures canonical provider snapshots for products and variations.
 */
final class RecordingSnapshotter {
	private ProviderRegistry $providers;
	private RecordingPendingTermStore $pending_terms;
	public function __construct( ProviderRegistry $providers, ?RecordingPendingTermStore $pending_terms = null ) {
		$pending_terms     ??= new RecordingPendingTermStore();
		$this->providers     = $providers;
		$this->pending_terms = $pending_terms;
	}

	/**
	 * Captures a product and its variations through registered providers.
	 *
	 * @return array<string,mixed>
	 */
	public function captureProduct( int $product_id ): array {
		$product = wc_get_product( $product_id );

		if ( ! $product instanceof \WC_Product || $product instanceof \WC_Product_Variation ) {
			throw new \RuntimeException( 'Recording snapshots require a source product.' );
		}

		$objects = array(
			$this->captureObject( $product ),
		);

		if ( $product instanceof \WC_Product_Variable ) {
			foreach ( $this->variationIds( $product ) as $variation_id ) {
				$variation = wc_get_product( (int) $variation_id );

				if ( $variation instanceof \WC_Product_Variation ) {
					$objects[] = $this->captureObject( $variation );
				}
			}
		}

		return array(
			'captured_at_utc' => gmdate( 'c' ),
			'objects'         => $objects,
		);
	}

	/**
	 * Returns variation IDs, including non-public recording shadows.
	 *
	 * @return int[]
	 */
	private function variationIds( \WC_Product_Variable $product ): array {
		$ids = array_map( 'intval', $product->get_children() );

		if ( ! ShadowProductAccessGuard::isShadow( $product->get_id() ) ) {
			return $ids;
		}

		global $wpdb;

		$shadow_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT p.ID
				FROM {$wpdb->posts} p
				INNER JOIN {$wpdb->postmeta} m
					ON p.ID = m.post_id
					AND m.meta_key = %s
					AND m.meta_value = %s
				WHERE p.post_parent = %d
					AND p.post_type = %s",
				ShadowProductAccessGuard::SHADOW_META_KEY,
				'1',
				$product->get_id(),
				'product_variation'
			)
		);

		return array_values( array_unique( array_merge( $ids, array_map( 'intval', $shadow_ids ) ) ) );
	}

	/**
	 * @return array<string,mixed>
	 */
	private function captureObject( \WC_Product $product ): array {
		$object_type = $product instanceof \WC_Product_Variation ? 'variation' : 'product';
		$context     = new ObjectContext(
			$object_type,
			$product->get_id(),
			$product->get_type()
		);
		$providers   = array();
		$warnings    = array();

		foreach ( $this->providers->all() as $provider_id => $provider ) {
			if ( ! $provider->supports( $context ) ) {
				continue;
			}

			try {
				$snapshot                  = $provider->snapshot( $context );
				$values                    = $snapshot->values();
				$providers[ $provider_id ] = array(
					'provider_id'      => $snapshot->providerId(),
					'provider_version' => $snapshot->providerVersion(),
					'values'           => $values,
					'checksum'         => hash( 'sha256', Json::canonicalEncode( $values ) ),
				);
			} catch ( \Throwable $throwable ) {
				$warnings[] = array(
					'provider_id' => $provider_id,
					'code'        => 'snapshot_failed',
					'message'     => $throwable->getMessage(),
				);
			}
		}

		$object = array(
			'reference'    => $object_type . ':' . $product->get_id(),
			'object_id'    => $product->get_id(),
			'object_type'  => $object_type,
			'product_type' => $product->get_type(),
			'providers'    => $providers,
			'warnings'     => $warnings,
		);

		if ( 'product' === $object_type && ShadowProductAccessGuard::isShadow( $product->get_id() ) ) {
			$pending = $this->pending_terms->snapshot( $product->get_id() );

			if ( array() !== $pending['terms'] || array() !== $pending['assignments'] ) {
				$object['recording_pending_terms'] = $pending;
			}
		}

		return $object;
	}
}
