<?php
/**
 * Just-in-time field conflict detector.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Execution;

use ScheduledChanges\Domain\Launch\ConflictPolicy;
use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Domain\Run\FailureClassification;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\MergeCapableFieldProvider;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Support\Json;

/**
 * Compares planned, current, and desired canonical values immediately before a write.
 */
final class ConflictDetector {
	private ProviderRegistry $providers;
	private FieldRegistry $fields;
	public function __construct( ProviderRegistry $providers, FieldRegistry $fields ) {
		$this->providers = $providers;
		$this->fields    = $fields;
	}

	public function decide(
		Operation $operation,
		TargetDefinition $target,
		ExecutionState $state
	): ConflictDecision {
		if ( 'provider.write' !== $operation->executionHandler() || null === $operation->fieldPath() ) {
			return ConflictDecision::proceed( $operation->after(), array( 'conflict' => false ) );
		}

		$provider = $this->providers->get( $operation->providerId() );
		$objectId = $state->resolve( $target->reference() );

		if ( null === $provider || null === $objectId ) {
			return ConflictDecision::stop(
				OperationExecutionResult::failure(
					'conflict_context_unavailable',
					'Conflict detection could not resolve the provider or target.',
					FailureClassification::PERMANENT_VALIDATION,
					false
				),
				array( 'conflict' => true )
			);
		}

		$context = new ObjectContext( $target->targetType(), $objectId, $target->subtype() );
		$catalog = $this->fields->discover( $context );
		$field   = $catalog->fields()->get( $operation->fieldPath() );

		if ( null === $field ) {
			return ConflictDecision::stop(
				OperationExecutionResult::failure(
					'conflict_field_unavailable',
					'Conflict detection could not discover the planned field.',
					FailureClassification::PERMANENT_VALIDATION,
					false
				),
				array( 'conflict' => true )
			);
		}

		$planned = $provider->normalize( $field, $operation->before() );
		$current = $provider->normalize( $field, $provider->read( $context, $field ) );
		$desired = $provider->normalize( $field, $operation->after() );
		$details = array(
			'conflict'            => $current !== $planned,
			'policy'              => $operation->conflictStrategy(),
			'planned_fingerprint' => $this->fingerprint( $planned ),
			'current_fingerprint' => $this->fingerprint( $current ),
			'desired_fingerprint' => $this->fingerprint( $desired ),
		);

		if ( $current === $planned || $current === $desired ) {
			return ConflictDecision::proceed( $desired, $details );
		}

		switch ( $operation->conflictStrategy() ) {
			case ConflictPolicy::OVERWRITE:
				return ConflictDecision::proceed( $desired, $details );
			case ConflictPolicy::SKIP:
				return ConflictDecision::stop(
					OperationExecutionResult::skipped(
						'conflict_skipped',
						'The current value changed after planning and was preserved.',
						$details
					),
					$details
				);
			case ConflictPolicy::MERGE:
				return $this->merge( $provider, $context, $field, $planned, $current, $desired, $details );
			case ConflictPolicy::FAIL:
				return ConflictDecision::stop(
					OperationExecutionResult::failure(
						'conflict_detected',
						'The current value changed after planning.',
						FailureClassification::PERMANENT_VALIDATION,
						false,
						$details
					),
					$details
				);
			default:
				return ConflictDecision::stop(
					OperationExecutionResult::failure(
						'conflict_policy_invalid',
						'The operation contains an unsupported conflict policy.',
						FailureClassification::PERMANENT_VALIDATION,
						false,
						$details
					),
					$details
				);
		}
	}

	/**
	 * @param array<string,mixed> $details Conflict details.
	 * @param mixed               $planned
	 * @param mixed               $current
	 * @param mixed               $desired
	 */
	private function merge(
		object $provider,
		ObjectContext $context,
		\ScheduledChanges\Field\FieldDefinition $field,
		$planned,
		$current,
		$desired,
		array $details
	): ConflictDecision {
		if ( ! $provider instanceof MergeCapableFieldProvider ) {
			return ConflictDecision::stop(
				OperationExecutionResult::failure(
					'merge_not_supported',
					'The owning provider does not support conflict merging.',
					FailureClassification::PERMANENT_VALIDATION,
					false,
					$details
				),
				$details
			);
		}

		$merged                        = $provider->mergeForExecution( $context, $field, $planned, $current, $desired );
		$details['merged_fingerprint'] = $this->fingerprint( $merged );

		return ConflictDecision::proceed( $merged, $details );
	}

	/**
	 * @param mixed $value
	 */
	private function fingerprint( $value ): string {
		return hash( 'sha256', Json::canonicalEncode( $value ) );
	}
}
