<?php
/**
 * Provider-owned operation applier.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Execution;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are escaped at UI boundaries.

use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Domain\Operation\OperationType;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Domain\Run\FailureClassification;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\ExecutionContext;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\OperationResult;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Support\ThrowableDiagnostic;

/**
 * Dispatches field writes and provider-owned operation handlers.
 */
final class ProviderOperationApplier implements OperationApplier {
	private ProviderRegistry $providers;
	private FieldRegistry $fields;
	/**
	 * @var ProviderOperationHandler[]
	 */
	private array $handlers;
	private ?RunRepository $runs;

	/**
	 * @param ProviderOperationHandler[] $handlers Provider-owned handlers.
	 */
	public function __construct( ProviderRegistry $providers, FieldRegistry $fields, array $handlers = array(), ?RunRepository $runs = null ) {
		$this->providers = $providers;
		$this->fields    = $fields;
		$this->handlers  = $handlers;
		$this->runs      = $runs;
	}

	public function apply(
		Operation $operation,
		TargetDefinition $target,
		ExecutionContext $context,
		ExecutionState $state
	): OperationExecutionResult {
		try {
			if ( 'provider.write' === strtolower( $operation->executionHandler() ) ) {
				if ( ! $this->handlerMatchesProviderWrite( $operation ) ) {
					return OperationExecutionResult::failure(
						'handler_type_mismatch',
						'Execution handler is not compatible with the immutable operation type.',
						FailureClassification::PERMANENT_VALIDATION,
						false
					);
				}

				return $this->providerWrite( $operation, $target, $context, $state );
			}

			if ( 'system.capture_snapshot' === strtolower( $operation->executionHandler() ) ) {
				return $this->captureSnapshotToken();
			}

			if ( 'provider.execute' === strtolower( $operation->executionHandler() ) ) {
				return $this->providerDefined( $operation, $context, $state );
			}

			foreach ( PublicHooks::providerOperationHandlers( $this->handlers, $this->providers, $this->fields, $this->runs ) as $handler ) {
				if ( ! $handler instanceof ProviderOperationHandler || ! $handler->supports( $operation ) ) {
					continue;
				}

				return $handler->apply( $operation, $target, $context, $state );
			}

			return OperationExecutionResult::failure(
				'unsupported_handler',
				'No provider operation handler is registered for ' . $operation->executionHandler() . '.',
				FailureClassification::UNSUPPORTED_HANDLER,
				false
			);
		} catch ( \Throwable $throwable ) {
			$classification = $this->classification( $operation, $throwable->getMessage() );

			return OperationExecutionResult::failure(
				'handler_exception',
				'Provider operation handler failed.',
				$classification,
				in_array(
					$classification,
					array(
						FailureClassification::TEMPORARY_PROVIDER,
						FailureClassification::EXTERNAL_TIMEOUT,
						FailureClassification::CACHE_PURGE,
					),
					true
				),
				array( 'exception' => ThrowableDiagnostic::from( $throwable ) )
			);
		}
	}

	private function handlerMatchesProviderWrite( Operation $operation ): bool {
		return OperationType::writesField( $operation->type() ) && null !== $operation->fieldPath();
	}

	private function providerWrite(
		Operation $operation,
		TargetDefinition $target,
		ExecutionContext $execution,
		ExecutionState $state
	): OperationExecutionResult {
		$provider = $this->providers->get( $operation->providerId() );
		$objectId = $state->resolve( $target->reference() );

		if ( null === $provider || null === $objectId || null === $operation->fieldPath() ) {
			return OperationExecutionResult::failure(
				'provider_context_unavailable',
				'Provider, target, or field context is unavailable.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

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

		if ( null === $field || $catalog->writableOwner( $field->id() )->id() !== $provider->id() ) {
			return OperationExecutionResult::failure(
				'provider_ownership_unavailable',
				'Canonical field ownership is unavailable at execution time.',
				FailureClassification::PERMANENT_VALIDATION,
				false
			);
		}

		$desiredValue = $state->desiredValue( $operation );
		$result       = $provider->write( $objectContext, $field, $desiredValue, $execution );
		if ( ! $result->successful() ) {
			return $this->providerFailure( $operation, $result );
		}

		$desired = $provider->normalize( $field, $desiredValue );
		$current = $provider->normalize( $field, $provider->read( $objectContext, $field ) );

		if ( $current !== $desired ) {
			return OperationExecutionResult::failure(
				'verification_failed',
				'Provider readback did not match the requested canonical value.',
				FailureClassification::PERMANENT_VALIDATION,
				false,
				array( 'provider_code' => $result->code() )
			);
		}

		return OperationExecutionResult::success(
			$result->message(),
			$result->changed(),
			true,
			$result->details()
		);
	}

	private function captureSnapshotToken(): OperationExecutionResult {
		return OperationExecutionResult::success(
			'Execution guard captured the required target snapshot.',
			false
		);
	}

	private function providerDefined(
		Operation $operation,
		ExecutionContext $context,
		ExecutionState $state
	): OperationExecutionResult {
		$result = PublicHooks::executeProviderOperation( $operation, $context, $state );

		return $result ?? OperationExecutionResult::failure(
			'provider_handler_missing',
			'No extension handler accepted the provider-defined operation.',
			FailureClassification::UNSUPPORTED_HANDLER,
			false
		);
	}

	private function providerFailure(
		Operation $operation,
		OperationResult $result
	): OperationExecutionResult {
		$classification = $this->classification( $operation, $result->code() . ' ' . $result->message() );
		$retryable      = in_array(
			$classification,
			array(
				FailureClassification::TEMPORARY_PROVIDER,
				FailureClassification::EXTERNAL_TIMEOUT,
				FailureClassification::CACHE_PURGE,
			),
			true
		);

		return OperationExecutionResult::failure(
			$result->code(),
			$result->message(),
			$classification,
			$retryable,
			$result->details()
		);
	}

	private function classification( Operation $operation, string $message ): string {
		$haystack = strtolower( $message );

		if ( $operation->type() === OperationType::CLEAR_CACHE ) {
			return FailureClassification::CACHE_PURGE;
		}

		if ( strpos( $haystack, 'timeout' ) !== false ) {
			return FailureClassification::EXTERNAL_TIMEOUT;
		}

		if (
			strpos( $haystack, 'temporary' ) !== false
			|| strpos( $haystack, 'temporarily' ) !== false
			|| strpos( $haystack, 'rate limit' ) !== false
			|| strpos( $haystack, 'unavailable' ) !== false
		) {
			return FailureClassification::TEMPORARY_PROVIDER;
		}

		return FailureClassification::PERMANENT_VALIDATION;
	}
}
