<?php
/**
 * Product taxonomy field provider.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Discovery\Provider;

use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyCatalog;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyOperationMode;
use ScheduledChangesWooCommerce\CoreCompat\Discovery\TaxonomyTermService;
use ScheduledChanges\Field\FieldCollection;
use ScheduledChanges\Field\FieldDataType;
use ScheduledChanges\Field\FieldDefinition;
use ScheduledChanges\Provider\AbstractFieldProvider;
use ScheduledChanges\Provider\ExecutionContext;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\OperationResult;
use ScheduledChanges\Provider\ProviderCategory;
use ScheduledChanges\Provider\ProviderSnapshot;
use ScheduledChanges\Provider\RollbackContext;
use ScheduledChanges\Provider\ValidationResult;
use ScheduledChanges\Taxonomy\TaxonomyModule as CoreTaxonomyModule;
use ScheduledChanges\Taxonomy\TaxonomyRegistration;
use Throwable;

/**
 * Discovers public and product-attached taxonomy assignments.
 */
final class TaxonomyFieldProvider extends AbstractFieldProvider {
	private TaxonomyCatalog $taxonomies;
	private TaxonomyTermService $terms;
	private CoreTaxonomyModule $modules;

	/** Creates the provider over explicit taxonomy registrations. */
	public function __construct(
		?TaxonomyCatalog $taxonomies = null,
		?TaxonomyTermService $terms = null,
		?CoreTaxonomyModule $modules = null
	) {
		$taxonomies     ??= new TaxonomyCatalog();
		$terms          ??= new TaxonomyTermService();
		$this->taxonomies = $taxonomies;
		$this->terms      = $terms;
		$this->modules    = $modules ?? new CoreTaxonomyModule();
		parent::__construct();
	}

	/** Returns the stable provider ID. */
	public function id(): string {
		return 'product-taxonomies';
	}

	/** Returns the provider schema version. */
	public function version(): string {
		global $wp_version;

		$registration_ids = array_map( static fn ( TaxonomyRegistration $registration ): string => $registration->id(), $this->registrations() );

		return (string) $wp_version . '-' . $this->taxonomies->versionFingerprint() . '-' . substr( hash( 'sha256', implode( '|', $registration_ids ) ), 0, 12 );
	}

	/** Returns the provider ownership category. */
	public function category(): string {
		return ProviderCategory::DISCOVERY;
	}

	/** Determines whether a product context is supported. */
	public function supports( ObjectContext $context ): bool {
		return 'product' === $context->targetType()
			&& null !== $context->targetId()
			&& 'product' === get_post_type( $context->targetId() );
	}

	/** Discovers fields from selected typed taxonomy registrations. */
	public function discover( ObjectContext $context ): FieldCollection {
		$fields = array();

		$taxonomies = $this->taxonomies->productTaxonomies();
		foreach ( $this->registrations() as $registration ) {
			$name     = $registration->taxonomy();
			$taxonomy = $taxonomies[ $name ] ?? null;
			if ( ! $taxonomy instanceof \WP_Taxonomy ) {
				continue;
			}

			$writable = '' !== $registration->capability();
			$modes    = array( 'add', 'remove', 'replace' );

			if ( (bool) $taxonomy->sort ) {
				$modes[] = 'reorder';
			}

			$fields[] = new FieldDefinition(
				'product.taxonomy.' . $name . '.term_ids',
				$registration->label(),
				'WordPress product taxonomy term assignments.',
				$this->owner( $name ),
				$this->id(),
				array( 'product' ),
				array( $context->subtype() ),
				FieldDataType::ID_LIST,
				'taxonomy-terms',
				true,
				$writable,
				$writable,
				$writable,
				false,
				array(),
				'fail',
				array(
					'registration_id' => $registration->id(),
					'taxonomy'        => $name,
					'public'          => (bool) $taxonomy->public,
					'hierarchical'    => $registration->hierarchical(),
					'term_creation'   => $registration->allowsTermCreation(),
					'show_in_rest'    => (bool) $taxonomy->show_in_rest,
					'sortable'        => (bool) $taxonomy->sort,
					'operation_modes' => $modes,
				),
				$registration->capability(),
				array( 'term_relationship_update' ),
				array( 'term_relationships', 'product_transients', 'object_cache' ),
				array( 'ordered' => (bool) $taxonomy->sort )
			);
		}

		return new FieldCollection( $fields );
	}

	/**
	 * @return mixed
	 */
	public function read( ObjectContext $context, FieldDefinition $field ) {
		$taxonomy = $this->taxonomy( $field );

		return $this->terms->termIds( (int) $context->targetId(), $taxonomy );
	}

	/**
	 * @param mixed $value
	 */
	public function validate(
		ObjectContext $context,
		FieldDefinition $field,
		$value
	): ValidationResult {
		$base = parent::validate( $context, $field, $value );
		if ( ! $base->isValid() ) {
			return $base;
		}

		$taxonomy = $this->taxonomy( $field );

		foreach ( is_array( $value ) ? $value : array() as $term_id ) {
			if ( (int) $term_id <= 0 || ! term_exists( (int) $term_id, $taxonomy->name ) ) {
				return ValidationResult::invalid( 'term_not_found', 'A requested taxonomy term does not exist.', $field->id() );
			}
		}

		return ValidationResult::valid();
	}

	/**
	 * @param mixed $value
	 */
	public function write(
		ObjectContext $context,
		FieldDefinition $field,
		$value,
		ExecutionContext $execution
	): OperationResult {
		$validation = $this->validate( $context, $field, $value );
		if ( ! $validation->isValid() ) {
			return OperationResult::failure( 'validation_failed', $validation->errors()[0]['message'] );
		}

		$normalized = $this->normalize( $field, $value );
		$before     = $this->normalize( $field, $this->read( $context, $field ) );

		if ( $before === $normalized ) {
			return OperationResult::success( 'Taxonomy terms are already applied.', false );
		}

		if ( $execution->dryRun() ) {
			return OperationResult::success( 'Dry run validated the taxonomy replacement.', true );
		}

		return $this->terms->apply(
			(int) $context->targetId(),
			$this->taxonomy( $field )->name,
			TaxonomyOperationMode::REPLACE,
			is_array( $normalized ) ? $normalized : array()
		);
	}

	/** Captures selected taxonomy values. */
	public function snapshot( ObjectContext $context ): ProviderSnapshot {
		$values = array();

		foreach ( $this->discover( $context ) as $field ) {
			$values[ $field->id() ] = $this->normalize( $field, $this->read( $context, $field ) );
		}

		return new ProviderSnapshot( $this->id(), $this->version(), $values );
	}

	/** Restores selected taxonomy values with readback. */
	public function rollback(
		ObjectContext $context,
		ProviderSnapshot $snapshot,
		RollbackContext $rollback
	): OperationResult {
		unset( $rollback );

		if ( $snapshot->providerId() !== $this->id() ) {
			return OperationResult::failure( 'provider_mismatch', 'Snapshot belongs to another provider.' );
		}

		$fields = $this->discover( $context );

		try {
			foreach ( $snapshot->values() as $field_id => $value ) {
				$field = $fields->get( $field_id );

				if ( null === $field || ! $field->writable() ) {
					continue;
				}

				$result = $this->terms->apply(
					(int) $context->targetId(),
					$this->taxonomy( $field )->name,
					TaxonomyOperationMode::REPLACE,
					is_array( $value ) ? $value : array()
				);

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

			return OperationResult::success( 'Taxonomy snapshot restored and verified.' );
		} catch ( Throwable $throwable ) {
			return OperationResult::failure( 'rollback_failed', $throwable->getMessage() );
		}
	}

	/** Resolves a field's still-selected taxonomy registration. */
	private function taxonomy( FieldDefinition $field ): \WP_Taxonomy {
		$name            = (string) ( $field->validationRules()['taxonomy'] ?? '' );
		$registration_id = (string) ( $field->validationRules()['registration_id'] ?? '' );
		$registered      = array_filter(
			$this->registrations(),
			static fn ( TaxonomyRegistration $registration ): bool => $registration->id() === $registration_id && $registration->taxonomy() === $name
		);
		$taxonomy        = $this->taxonomies->productTaxonomies()[ $name ] ?? null;

		if ( array() === $registered || ! $taxonomy instanceof \WP_Taxonomy ) {
			throw new \RuntimeException( 'Discovered taxonomy is no longer registered.' );
		}

		return $taxonomy;
	}

	/** @return TaxonomyRegistration[] */
	private function registrations(): array {
		return $this->modules->forContext( $this->id(), 'product' );
	}

	/** Returns the customer-facing taxonomy owner. */
	private function owner( string $taxonomy ): string {
		if ( in_array( $taxonomy, array( 'product_cat', 'product_tag', 'product_shipping_class' ), true ) || strncmp( $taxonomy, 'pa_', strlen( 'pa_' ) ) === 0 ) {
			return 'WooCommerce';
		}

		return 'Registered taxonomy';
	}
}
