<?php
/**
 * Immutable scheduled end-version compiler.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Rollback;

use ScheduledChanges\Domain\Launch\ConflictPolicy;
use ScheduledChanges\Domain\Launch\LaunchVersion;
use ScheduledChanges\Domain\Launch\LaunchVersionRole;
use ScheduledChanges\Domain\Launch\OperationPlan;
use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Domain\Operation\OperationGraph;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Validation\ValidationReport;

/**
 * Compiles explicit end choices into a linked, checksummed immutable plan.
 */
final class EndVersionCompiler {
	private LaunchVersionRepository $versions;
	public function __construct( LaunchVersionRepository $versions ) {
		$this->versions = $versions;
	}

	/**
	 * @param array<string,EndBehavior> $behaviors Behaviors keyed by start operation UUID.
	 */
	public function compile( int $start_version_id, array $behaviors, int $creator_id ): int {
		$start = $this->versions->findById( $start_version_id );

		if ( null === $start || null === $start->id() || LaunchVersionRole::START !== $start->role() ) {
			throw new \InvalidArgumentException( 'A persisted start version is required.' );
		}

		if ( null !== $this->versions->findLinkedEndVersion( $start_version_id ) ) {
			throw new \RuntimeException( 'This start version already has a linked end version.' );
		}

		$operations = array();
		$targets    = array();

		foreach ( $start->plan()->graph()->topologicalOrder() as $operation ) {
			$behavior = $behaviors[ $operation->uuid()->value() ] ?? EndBehavior::keep();

			if ( EndBehaviorType::KEEP_LAUNCH_VALUE === $behavior->type() ) {
				continue;
			}

			$endOperation = $this->compileOperation( $start, $operation, $behavior );

			$target = $start->plan()->graph()->targetByUuid( $endOperation->targetUuid()->value() );
			if ( null === $target ) {
				throw new \RuntimeException( 'Compiled end operation target is missing.' );
			}

			$targets[ $target->uuid()->value() ] = $target;
			$operations[]                        = $endOperation;
		}

		if ( array() === $operations ) {
			throw new \InvalidArgumentException( 'An end version must contain at least one explicit behavior.' );
		}

		$graph         = new OperationGraph( array_values( $targets ), $operations );
		$fingerprints  = array_intersect_key(
			$start->plan()->fingerprints(),
			array_fill_keys(
				array_map(
					static fn ( TargetDefinition $target ): string => $target->reference(),
					array_values( $targets )
				),
				true
			)
		);
		$plan          = OperationPlan::create(
			$graph,
			ValidationReport::valid(),
			$start->plan()->compatibility(),
			$start->plan()->requiredProviders(),
			'Apply scheduled end behavior for launch version ' . $start_version_id . '.',
			$fingerprints
		);
		$latest        = $this->versions->findLatestForLaunch( $start->launchId() );
		$versionNumber = ( null === $latest ? $start->versionNumber() : $latest->versionNumber() ) + 1;

		return $this->versions->create(
			LaunchVersion::createEnd(
				$start->launchId(),
				$versionNumber,
				$plan,
				$creator_id,
				$start_version_id
			)
		);
	}

	private function compileOperation(
		LaunchVersion $start,
		Operation $operation,
		EndBehavior $behavior
	): Operation {
		if ( null === $operation->fieldPath() ) {
			throw new \InvalidArgumentException( 'This end behavior is not supported for a non-field operation.' );
		}

		$target = $start->plan()->graph()->targetByUuid( $operation->targetUuid()->value() );
		if ( null === $target ) {
			throw new \RuntimeException( 'Start operation target is missing.' );
		}

		switch ( $behavior->type() ) {
			case EndBehaviorType::RESTORE_PRE_LAUNCH:
				$after = $operation->before();
				break;
			case EndBehaviorType::APPLY_SPECIFIC_VALUE:
				$after = $behavior->value();
				break;
			default:
				throw new \InvalidArgumentException( 'The selected end behavior is invalid for a field operation.' );
		}

		return Operation::create(
			$operation->type(),
			$target,
			$operation->fieldPath(),
			$operation->providerId(),
			$operation->after(),
			$after,
			$operation->valueType(),
			$operation->schema(),
			$operation->validationRequirements(),
			array(),
			$operation->reversibility(),
			ConflictPolicy::FAIL,
			$operation->redactionRules(),
			$operation->executionHandler(),
			$operation->rollbackHandler(),
			'Apply end behavior for ' . $operation->summary()
		);
	}
}
