<?php
/**
 * Operation graph builder.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Domain\Operation;

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

use ScheduledChanges\Domain\Shared\Uuid;

/**
 * Adds declared and semantic dependencies before freezing a graph.
 */
final class OperationGraphBuilder {
	/**
	 * @var array<string,TargetDefinition>
	 */
	private array $targets = array();

	/**
	 * @var array<string,Operation>
	 */
	private array $operations = array();

	public function addTarget( TargetDefinition $target ): self {
		$uuid = $target->uuid()->value();

		if ( isset( $this->targets[ $uuid ] ) ) {
			throw new \InvalidArgumentException( 'Target already exists in the graph builder: ' . $uuid );
		}

		$this->targets[ $uuid ] = $target;

		return $this;
	}

	public function addOperation( Operation $operation ): self {
		$uuid = $operation->uuid()->value();

		if ( isset( $this->operations[ $uuid ] ) ) {
			throw new \InvalidArgumentException( 'Operation already exists in the graph builder: ' . $uuid );
		}

		$this->operations[ $uuid ] = $operation;

		return $this;
	}

	public function build(): OperationGraph {
		$writers   = $this->fieldWriters();
		$snapshots = $this->snapshotOperations();
		$resolved  = array();

		foreach ( $this->operations as $operation ) {
			$dependencies = array_map(
				static fn ( string $uuid ): Uuid => new Uuid( $uuid ),
				$operation->dependencies()
			);
			$target       = $this->targets[ $operation->targetUuid()->value() ] ?? null;

			if ( null !== $target && $target->future() ) {
				$creator = $target->creationOperationUuid();

				if ( null !== $creator && $creator->value() !== $operation->uuid()->value() ) {
					$dependencies[] = $creator;
				}
			}

			foreach ( $this->declaredFieldDependencies( $operation ) as $fieldPath ) {
				$key = $operation->targetUuid()->value() . '|' . $fieldPath;

				if ( isset( $writers[ $key ] ) ) {
					$dependencies[] = $writers[ $key ];
				}
			}

			if ( \ScheduledChanges\Domain\Operation\OperationType::destructive( $operation->type() ) ) {
				foreach ( $snapshots[ $operation->targetUuid()->value() ] ?? array() as $snapshot ) {
					$dependencies[] = $snapshot;
				}
			}

			if ( OperationType::CLEAR_CACHE === $operation->type() ) {
				foreach ( $this->operations as $candidate ) {
					if (
						\ScheduledChanges\Domain\Operation\OperationType::mutatesTarget( $candidate->type() )
						&& $candidate->targetUuid()->value() === $operation->targetUuid()->value()
					) {
						$dependencies[] = $candidate->uuid();
					}
				}
			}

			$dependencies = $this->uniqueDependencies( $dependencies, $operation->uuid() );
			$resolved[]   = $operation->withDependencies( $dependencies );
		}

		return new OperationGraph( array_values( $this->targets ), $resolved );
	}

	/**
	 * @return array<string,Uuid>
	 */
	private function fieldWriters(): array {
		$writers = array();

		foreach ( $this->operations as $operation ) {
			if ( null !== $operation->fieldPath() && \ScheduledChanges\Domain\Operation\OperationType::writesField( $operation->type() ) ) {
				$writers[ $operation->targetUuid()->value() . '|' . $operation->fieldPath() ] = $operation->uuid();
			}
		}

		return $writers;
	}

	/**
	 * @return array<string,Uuid[]>
	 */
	private function snapshotOperations(): array {
		$snapshots = array();

		foreach ( $this->operations as $operation ) {
			if ( OperationType::CAPTURE_SNAPSHOT === $operation->type() ) {
				$snapshots[ $operation->targetUuid()->value() ][] = $operation->uuid();
			}
		}

		return $snapshots;
	}

	/**
	 * @return string[]
	 */
	private function declaredFieldDependencies( Operation $operation ): array {
		$dependencies = $operation->schema()['dependencies'] ?? array();

		if ( ! is_array( $dependencies ) ) {
			return array();
		}

		return array_values(
			array_map(
				static fn ( $dependency ): string => (string) $dependency,
				$dependencies
			)
		);
	}

	/**
	 * @param Uuid[] $dependencies Candidate dependencies.
	 * @return Uuid[]
	 */
	private function uniqueDependencies( array $dependencies, Uuid $operationUuid ): array {
		$unique = array();

		foreach ( $dependencies as $dependency ) {
			if ( $dependency->value() !== $operationUuid->value() ) {
				$unique[ $dependency->value() ] = $dependency;
			}
		}

		ksort( $unique );

		return array_values( $unique );
	}
}
