<?php
/**
 * Registered product metadata field provider.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Discovery\Provider;

// phpcs:disable WordPress.DB -- Scheduler-owned custom tables require direct wpdb queries.
// phpcs:disable Squiz.Commenting.FunctionComment.Missing -- Compact provider contract methods mirror the abstract provider shape.

use ScheduledChanges\Discovery\MetadataDenylist;
use ScheduledChanges\Discovery\MetadataOwnerDetector;
use ScheduledChanges\Discovery\RegisteredMetadataRegistry;
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;

/**
 * Exposes registered product metadata as read-only discovery in core.
 *
 * Registered metadata may have a schema, but that does not make it native
 * WooCommerce product data. Dedicated compatibility extensions should provide
 * writable field providers for plugin-owned metadata.
 */
final class RegisteredMetaFieldProvider extends AbstractFieldProvider {
	private RegisteredMetadataRegistry $registered;
	private MetadataDenylist $denylist;
	private MetadataOwnerDetector $owners;
	public function __construct(
		?RegisteredMetadataRegistry $registered = null,
		?MetadataDenylist $denylist = null,
		?MetadataOwnerDetector $owners = null
	) {
		$registered     ??= new RegisteredMetadataRegistry();
		$denylist       ??= new MetadataDenylist();
		$owners         ??= new MetadataOwnerDetector();
		$this->registered = $registered;
		$this->denylist   = $denylist;
		$this->owners     = $owners;
		parent::__construct();
	}

	public function id(): string {
		return 'registered-metadata';
	}

	public function version(): string {
		global $wp_version;

		return (string) $wp_version . '-' . $this->registered->versionFingerprint();
	}

	public function category(): string {
		return ProviderCategory::REGISTERED;
	}

	public function supports( ObjectContext $context ): bool {
		return in_array( $context->targetType(), array( 'product', 'variation' ), true )
			&& null !== $context->targetId()
			&& $this->subtype( $context ) === get_post_type( $context->targetId() );
	}

	public function discover( ObjectContext $context ): FieldCollection {
		$fields = array();

		foreach ( $this->registered->forSubtype( $this->subtype( $context ) ) as $key => $registration ) {
			if ( $this->denylist->isDenied( $key ) ) {
				continue;
			}

			$single   = (bool) ( $registration['single'] ?? false );
			$type     = (string) ( $registration['type'] ?? 'string' );
			$writable = false;
			$fields[] = new FieldDefinition(
				$this->fieldId( $context, $key ),
				$this->label( $key, $registration ),
				$this->description( $registration ),
				$this->owners->detect( $key, $registration ),
				$this->id(),
				array( $context->targetType() ),
				array( $context->subtype() ),
				$single ? $this->dataType( $type ) : FieldDataType::ARRAY,
				$this->uiComponent( $type, $single ),
				true,
				$writable,
				$writable,
				$writable,
				$this->denylist->isSensitive( $key ),
				array(),
				'fail',
				array(
					'meta_key'          => $key,
					'registered_type'   => $type,
					'single'            => $single,
					'show_in_rest'      => false !== ( $registration['show_in_rest'] ?? false ),
					'default'           => $registration['default'] ?? null,
					'sanitize_callback' => $this->owners->callbackDescription( $registration['sanitize_callback'] ?? null ),
					'auth_callback'     => $this->owners->callbackDescription( $registration['auth_callback'] ?? null ),
					'boundary_status'   => 'read_only_discovery',
					'module_required'   => true,
					'rest_schema'       => $this->registered->schema( $registration ),
				),
				'edit_products',
				array( 'post_meta_update' ),
				array( 'post_meta', 'object_cache' ),
				$single ? array() : array( 'ordered' => true )
			);
		}

		return new FieldCollection( $fields );
	}

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

		return get_post_meta(
			(int) $context->targetId(),
			$key,
			(bool) ( $registration['single'] ?? false )
		);
	}

	/**
	 * @param mixed $value
	 */
	public function validate(
		ObjectContext $context,
		FieldDefinition $field,
		$value
	): ValidationResult {
		$base = parent::validate( $context, $field, $value );

		if ( ! $base->isValid() ) {
			return $base;
		}

		unset( $context, $value );

		return ValidationResult::invalid(
			'registered_metadata_read_only',
			'Registered metadata is read-only discovery in Scheduled Changes core. '
				. 'A compatibility extension is required to schedule plugin-owned metadata.',
			$field->id()
		);
	}

	/**
	 * @param mixed $value
	 */
	public function write(
		ObjectContext $context,
		FieldDefinition $field,
		$value,
		ExecutionContext $execution
	): OperationResult {
		unset( $context, $field, $value, $execution );

		return OperationResult::failure(
			'read_only_field',
			'Registered metadata is read-only discovery in Scheduled Changes core. '
				. 'A compatibility extension is required to schedule plugin-owned metadata.'
		);
	}

	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 );
	}

	public function rollback(
		ObjectContext $context,
		ProviderSnapshot $snapshot,
		RollbackContext $rollback
	): OperationResult {
		unset( $context, $rollback );

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

		return OperationResult::success( 'Registered metadata snapshot is read-only in core.', false );
	}

	private function subtype( ObjectContext $context ): string {
		return 'variation' === $context->targetType() ? 'product_variation' : 'product';
	}

	/**
	 * @return array<string,mixed>
	 */
	private function registration( ObjectContext $context, string $key ): array {
		$registration = $this->registered->get( $this->subtype( $context ), $key );

		if ( null === $registration ) {
			throw new \RuntimeException( 'Registered metadata schema is no longer available.' );
		}

		return $registration;
	}

	/**
	 * @param array<string,mixed> $registration Registered metadata arguments.
	 */
	private function description( array $registration ): string {
		$description = trim( (string) ( $registration['description'] ?? '' ) );

		if ( '' !== $description ) {
			return $description
				. ' Scheduled Changes core discovers this metadata as read-only; '
				. 'scheduling it requires a compatibility extension.';
		}

		return 'Registered WordPress product metadata discovered as read-only. '
			. 'Scheduling plugin-owned metadata requires a compatibility extension.';
	}

	private function dataType( string $type ): string {
		switch ( $type ) {
			case 'string':
				return FieldDataType::STRING;
			case 'boolean':
				return FieldDataType::BOOLEAN;
			case 'integer':
				return FieldDataType::INTEGER;
			case 'number':
				return FieldDataType::DECIMAL;
			case 'array':
				return FieldDataType::ARRAY;
			case 'object':
				return FieldDataType::OBJECT;
			default:
				return FieldDataType::MIXED;
		}
	}

	private function uiComponent( string $type, bool $single ): string {
		if ( ! $single || in_array( $type, array( 'array', 'object' ), true ) ) {
			return 'structured-data';
		}

		switch ( $type ) {
			case 'boolean':
				return 'toggle';
			case 'integer':
			case 'number':
				return 'number';
			default:
				return 'text';
		}
	}

	/**
	 * @param array<string,mixed> $registration Registered metadata arguments.
	 */
	private function label( string $key, array $registration ): string {
		$label = trim( (string) ( $registration['label'] ?? '' ) );

		return '' !== $label
			? $label
			: ucwords( str_replace( array( '_', '-' ), ' ', trim( $key, '_' ) ) );
	}

	private function fieldId( ObjectContext $context, string $key ): string {
		$prefix  = 'variation' === $context->targetType()
			? 'variation.{stable-key}.metadata.registered.'
			: 'product.metadata.registered.';
		$segment = sanitize_key( trim( $key, '_' ) );
		$segment = '' === $segment ? 'key' : $segment;

		return $prefix . $segment . '-' . substr( hash( 'sha256', $key ), 0, 8 );
	}

	private function metaKey( FieldDefinition $field ): string {
		$key = (string) ( $field->validationRules()['meta_key'] ?? '' );

		if ( '' === $key ) {
			throw new \RuntimeException( 'Registered metadata field is missing its storage key.' );
		}

		return $key;
	}
}
