<?php
/**
 * Advanced raw metadata field provider.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Discovery\Provider;

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

use ScheduledChanges\Discovery\MetadataClassification;
use ScheduledChanges\Discovery\MetadataDescriptor;
use ScheduledChanges\Discovery\MetadataValueShape;
use ScheduledChanges\Discovery\RawMetadataExplorer;
use ScheduledChanges\Discovery\RawMetadataDiagnosticsModule;
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;

/**
 * Exposes confirmed non-denied raw metadata as read-only canonical fields.
 */
final class RawMetadataFieldProvider extends AbstractFieldProvider {
	private RawMetadataExplorer $metadata;

	/** Creates the provider over the restricted diagnostics module. */
	public function __construct( ?RawMetadataExplorer $metadata = null ) {
		$metadata     ??= ( new RawMetadataDiagnosticsModule() )->createExplorer();
		$this->metadata = $metadata;
		parent::__construct();
	}

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

	/** Returns the diagnostic schema version. */
	public function version(): string {
		return '1.0.0';
	}

	/** Returns the lowest-priority raw metadata category. */
	public function category(): string {
		return ProviderCategory::RAW_METADATA;
	}

	/** Enforces module, toggle, actor, capability, and confirmation gates. */
	public function supports( ObjectContext $context ): bool {
		$actor_id  = (int) $context->attribute( 'actor_id', 0 );
		$confirmed = true === $context->attribute( 'raw_metadata_confirmed', false );

		return true === $context->attribute( 'advanced_raw_metadata', false )
			&& in_array( $context->targetType(), array( 'product', 'variation' ), true )
			&& null !== $context->targetId()
			&& $this->metadata->accessAllowed( $confirmed, $actor_id );
	}

	/** Discovers only confirmed, non-denied, read-only descriptors. */
	public function discover( ObjectContext $context ): FieldCollection {
		$result = $this->metadata->explore(
			(int) $context->targetId(),
			true === $context->attribute( 'raw_metadata_confirmed', false ),
			(int) $context->attribute( 'actor_id', 0 )
		);
		$fields = array();

		foreach ( $result->descriptors() as $descriptor ) {
			if (
				! in_array(
					$descriptor->classification(),
					array( MetadataClassification::RAW, MetadataClassification::DISCOVERED ),
					true
				)
			) {
				continue;
			}

			$fields[] = $this->definition( $context, $descriptor );
		}

		return new FieldCollection( $fields );
	}

	/**
	 * @return mixed
	 */
	public function read( ObjectContext $context, FieldDefinition $field ) {
		$key = $this->metaKey( $field );
		$this->metadata->auditRead(
			(int) $context->targetId(),
			$key,
			true === $context->attribute( 'raw_metadata_confirmed', false ),
			(int) $context->attribute( 'actor_id', 0 )
		);

		return $this->readValue( $context, $field );
	}

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

		return OperationResult::failure(
			'raw_metadata_read_only',
			'Unknown raw metadata is never writable by automatic discovery.'
		);
	}

	/** Captures a read-only diagnostic snapshot. */
	public function snapshot( ObjectContext $context ): ProviderSnapshot {
		$values = array();

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

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

	/** Rejects rollback because the provider never owns writes. */
	public function rollback(
		ObjectContext $context,
		ProviderSnapshot $snapshot,
		RollbackContext $rollback
	): OperationResult {
		unset( $context, $snapshot, $rollback );

		return OperationResult::failure(
			'raw_metadata_read_only',
			'Read-only raw metadata snapshots cannot be rolled back automatically.'
		);
	}

	/** Builds one explicitly read-only diagnostic field. */
	private function definition( ObjectContext $context, MetadataDescriptor $descriptor ): FieldDefinition {
		$prefix  = 'variation' === $context->targetType()
			? 'variation.{stable-key}.metadata.raw.'
			: 'product.metadata.raw.';
		$segment = sanitize_key( trim( $descriptor->key(), '_' ) );
		$segment = '' === $segment ? 'key' : $segment;

		return new FieldDefinition(
			$prefix . $segment . '-' . substr( hash( 'sha256', $descriptor->key() ), 0, 8 ),
			ucwords( str_replace( array( '_', '-' ), ' ', trim( $descriptor->key(), '_' ) ) ),
			'Advanced raw metadata discovered without an explicit write contract.',
			$descriptor->owner(),
			$this->id(),
			array( $context->targetType() ),
			array( $context->subtype() ),
			$this->dataType( $descriptor ),
			'raw-metadata',
			true,
			false,
			false,
			false,
			$descriptor->sensitive(),
			array(),
			'fail',
			array_merge(
				$descriptor->toArray(),
				array(
					'meta_key' => $descriptor->key(),
					'multiple' => $descriptor->multiple(),
				)
			),
			'scheduled_changes_use_raw_metadata'
		);
	}

	/** Maps a discovered value shape to its read-only field type. */
	private function dataType( MetadataDescriptor $descriptor ): string {
		if ( $descriptor->multiple() ) {
			return FieldDataType::ARRAY;
		}

		switch ( $descriptor->shape() ) {
			case MetadataValueShape::LIST:
				return FieldDataType::ARRAY;
			case MetadataValueShape::OBJECT:
				return FieldDataType::OBJECT;
			default:
				return FieldDataType::MIXED;
		}
	}

	/** Returns the audited storage key for one diagnostic field. */
	private function metaKey( FieldDefinition $field ): string {
		$key = (string) ( $field->validationRules()['meta_key'] ?? '' );

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

		return $key;
	}

	/**
	 * @return mixed
	 */
	private function readValue( ObjectContext $context, FieldDefinition $field ) {
		$multiple = (bool) ( $field->validationRules()['multiple'] ?? false );

		return get_post_meta( (int) $context->targetId(), $this->metaKey( $field ), ! $multiple );
	}
}
