<?php
/**
 * Immutable schedulable operation.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Domain\Operation;

use ScheduledChanges\Domain\Launch\ConflictPolicy;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Support\Json;

/**
 * Smallest replayable unit in an operation graph.
 */
final class Operation {
	private Uuid $uuid;
	private string $type;
	private Uuid $targetUuid;
	private string $targetReference;
	private ?string $fieldPath;
	private string $providerId;
	/**
	 * @var mixed
	 */
	private $before;
	/**
	 * @var mixed
	 */
	private $after;
	private string $valueType;
	/**
	 * @var array<string, mixed>
	 */
	private array $schema;
	/**
	 * @var array<string, mixed>
	 */
	private array $validationRequirements;
	/**
	 * @var string[]
	 */
	private array $dependencies;
	private string $idempotencyKey;
	private string $reversibility;
	private string $conflictStrategy;
	/**
	 * @var array<string, mixed>
	 */
	private array $redactionRules;
	private string $executionHandler;
	private ?string $rollbackHandler;
	private string $summary;
	private int $executionOrder = 0;
	/**
	 * @param array<string,mixed> $schema Validation and value schema.
	 * @param array<string,mixed> $validationRequirements Runtime requirements.
	 * @param string[]            $dependencies Operation UUIDs.
	 * @param array<string,mixed> $redactionRules Redaction instructions.
	 * @param mixed               $before
	 * @param mixed               $after
	 */
	public function __construct(
		Uuid $uuid,
		string $type,
		Uuid $targetUuid,
		string $targetReference,
		?string $fieldPath,
		string $providerId,
		$before,
		$after,
		string $valueType,
		array $schema,
		array $validationRequirements,
		array $dependencies,
		string $idempotencyKey,
		string $reversibility,
		string $conflictStrategy,
		array $redactionRules,
		string $executionHandler,
		?string $rollbackHandler,
		string $summary,
		int $executionOrder = 0
	) {
		$this->uuid                   = $uuid;
		$this->type                   = $type;
		$this->targetUuid             = $targetUuid;
		$this->targetReference        = $targetReference;
		$this->fieldPath              = $fieldPath;
		$this->providerId             = $providerId;
		$this->before                 = $before;
		$this->after                  = $after;
		$this->valueType              = $valueType;
		$this->schema                 = $schema;
		$this->validationRequirements = $validationRequirements;
		$this->dependencies           = $dependencies;
		$this->idempotencyKey         = $idempotencyKey;
		$this->reversibility          = $reversibility;
		$this->conflictStrategy       = $conflictStrategy;
		$this->redactionRules         = $redactionRules;
		$this->executionHandler       = $executionHandler;
		$this->rollbackHandler        = $rollbackHandler;
		$this->summary                = $summary;
		$this->executionOrder         = $executionOrder;
		if ( '' === trim( $this->targetReference ) || '' === trim( $this->providerId ) ) {
			throw new \InvalidArgumentException( 'Operation target reference and provider are required.' );
		}

		if ( 1 !== preg_match( '/^[a-z][a-z0-9-]*$/', $this->providerId ) ) {
			throw new \InvalidArgumentException( 'Operation provider IDs must be lowercase slugs.' );
		}

		if ( \ScheduledChanges\Domain\Operation\OperationType::requiresFieldPath( $this->type ) && null === $this->fieldPath ) {
			throw new \InvalidArgumentException( 'This operation type requires a canonical field path.' );
		}

		if ( null !== $this->fieldPath && 1 !== preg_match( '/^[a-z][a-z0-9_]*(?:\.[a-z0-9_{}-]+)+$/', $this->fieldPath ) ) {
			throw new \InvalidArgumentException( 'Operation field paths must be canonical lowercase paths.' );
		}

		if ( 1 !== preg_match( '/^[0-9a-f]{64}$/', $this->idempotencyKey ) ) {
			throw new \InvalidArgumentException( 'Operation idempotency keys must be SHA-256 hashes.' );
		}

		if (
			1 !== preg_match( '/^[a-z][a-z0-9_.-]*$/', $this->executionHandler )
			|| (
				null !== $this->rollbackHandler
				&& 1 !== preg_match( '/^[a-z][a-z0-9_.-]*$/', $this->rollbackHandler )
			)
		) {
			throw new \InvalidArgumentException( 'Operation handler IDs must be stable lowercase identifiers.' );
		}

		if ( '' === trim( $this->valueType ) || '' === trim( $this->summary ) ) {
			throw new \InvalidArgumentException( 'Operation value type and summary are required.' );
		}

		$dependencies = array_values( array_unique( $this->dependencies ) );
		sort( $dependencies );
		if ( $dependencies !== $this->dependencies ) {
			throw new \InvalidArgumentException( 'Operation dependencies must be unique and canonically ordered.' );
		}

		foreach ( $this->dependencies as $dependency ) {
			$uuid = new Uuid( $dependency );

			if ( $uuid->value() === $this->uuid->value() ) {
				throw new \InvalidArgumentException( 'Operations cannot depend on themselves.' );
			}
		}
	}

	/**
	 * Creates an operation and derives its stable idempotency key.
	 *
	 * @param array<string,mixed> $schema Value schema.
	 * @param array<string,mixed> $validationRequirements Runtime requirements.
	 * @param Uuid[]              $dependencies Dependency UUIDs.
	 * @param array<string,mixed> $redactionRules Redaction instructions.
	 * @param mixed               $before
	 * @param mixed               $after
	 */
	public static function create(
		string $type,
		TargetDefinition $target,
		?string $fieldPath,
		string $providerId,
		$before,
		$after,
		string $valueType,
		array $schema,
		array $validationRequirements,
		array $dependencies,
		string $reversibility,
		string $conflictStrategy,
		array $redactionRules,
		string $executionHandler,
		?string $rollbackHandler,
		string $summary,
		?Uuid $uuid = null
	): self {
		$uuid              = $uuid ?? Uuid::generate();
		$dependencyStrings = array_map(
			static fn ( Uuid $dependency ): string => $dependency->value(),
			$dependencies
		);
		sort( $dependencyStrings );
		$idempotencyKey = hash(
			'sha256',
			Json::canonicalEncode(
				array(
					'uuid'             => $uuid->value(),
					'type'             => $type,
					'target_reference' => $target->reference(),
					'field_path'       => $fieldPath,
					'provider_id'      => $providerId,
					'after'            => $after,
				)
			)
		);

		return new self(
			$uuid,
			$type,
			$target->uuid(),
			$target->reference(),
			$fieldPath,
			$providerId,
			$before,
			$after,
			$valueType,
			$schema,
			$validationRequirements,
			$dependencyStrings,
			$idempotencyKey,
			$reversibility,
			$conflictStrategy,
			$redactionRules,
			$executionHandler,
			$rollbackHandler,
			$summary
		);
	}

	public function uuid(): Uuid {
		return $this->uuid;
	}

	public function type(): string {
		return $this->type;
	}

	public function targetUuid(): Uuid {
		return $this->targetUuid;
	}

	public function targetReference(): string {
		return $this->targetReference;
	}

	public function fieldPath(): ?string {
		return $this->fieldPath;
	}

	public function providerId(): string {
		return $this->providerId;
	}

	/**
	 * @return mixed
	 */
	public function before() {
		return $this->before;
	}

	/**
	 * @return mixed
	 */
	public function after() {
		return $this->after;
	}

	public function valueType(): string {
		return $this->valueType;
	}

	/**
	 * @return array<string,mixed>
	 */
	public function schema(): array {
		return $this->schema;
	}

	/**
	 * @return array<string,mixed>
	 */
	public function validationRequirements(): array {
		return $this->validationRequirements;
	}

	/**
	 * @return string[]
	 */
	public function dependencies(): array {
		return $this->dependencies;
	}

	public function idempotencyKey(): string {
		return $this->idempotencyKey;
	}

	public function reversibility(): string {
		return $this->reversibility;
	}

	public function conflictStrategy(): string {
		return $this->conflictStrategy;
	}

	/**
	 * @return array<string,mixed>
	 */
	public function redactionRules(): array {
		return $this->redactionRules;
	}

	public function executionHandler(): string {
		return $this->executionHandler;
	}

	public function rollbackHandler(): ?string {
		return $this->rollbackHandler;
	}

	public function summary(): string {
		return $this->summary;
	}

	public function executionOrder(): int {
		return $this->executionOrder;
	}

	/**
	 * @param Uuid[] $dependencies Replacement dependencies.
	 */
	public function withDependencies( array $dependencies ): self {
		$dependencyStrings = array_map(
			static fn ( Uuid $dependency ): string => $dependency->value(),
			$dependencies
		);

		sort( $dependencyStrings );

		return new self(
			$this->uuid,
			$this->type,
			$this->targetUuid,
			$this->targetReference,
			$this->fieldPath,
			$this->providerId,
			$this->before,
			$this->after,
			$this->valueType,
			$this->schema,
			$this->validationRequirements,
			$dependencyStrings,
			$this->idempotencyKey,
			$this->reversibility,
			$this->conflictStrategy,
			$this->redactionRules,
			$this->executionHandler,
			$this->rollbackHandler,
			$this->summary,
			$this->executionOrder
		);
	}

	/**
	 * @return array<string,mixed>
	 */
	public function toArray(): array {
		return array(
			'uuid'                    => $this->uuid->value(),
			'type'                    => $this->type,
			'target_uuid'             => $this->targetUuid->value(),
			'target_reference'        => $this->targetReference,
			'field_path'              => $this->fieldPath,
			'provider_id'             => $this->providerId,
			'before'                  => $this->before,
			'after'                   => $this->after,
			'value_type'              => $this->valueType,
			'schema'                  => $this->schema,
			'validation_requirements' => $this->validationRequirements,
			'dependencies'            => $this->dependencies,
			'idempotency_key'         => $this->idempotencyKey,
			'reversibility'           => $this->reversibility,
			'conflict_strategy'       => $this->conflictStrategy,
			'redaction_rules'         => $this->redactionRules,
			'execution_handler'       => $this->executionHandler,
			'rollback_handler'        => $this->rollbackHandler,
			'summary'                 => $this->summary,
			'execution_order'         => $this->executionOrder,
		);
	}

	/**
	 * @param array<string,mixed> $data Serialized operation.
	 */
	public static function fromArray( array $data ): self {
		$dependencies = array_values(
			array_map(
				static fn ( $dependency ): string => (string) $dependency,
				is_array( $data['dependencies'] ?? null ) ? $data['dependencies'] : array()
			)
		);

		return new self(
			new Uuid( (string) ( $data['uuid'] ?? '' ) ),
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Operation\OperationType::class, (string) ( $data['type'] ?? '' ) ),
			new Uuid( (string) ( $data['target_uuid'] ?? '' ) ),
			(string) ( $data['target_reference'] ?? '' ),
			null === ( $data['field_path'] ?? null ) ? null : (string) $data['field_path'],
			(string) ( $data['provider_id'] ?? '' ),
			$data['before'] ?? null,
			$data['after'] ?? null,
			(string) ( $data['value_type'] ?? 'mixed' ),
			is_array( $data['schema'] ?? null ) ? $data['schema'] : array(),
			is_array( $data['validation_requirements'] ?? null ) ? $data['validation_requirements'] : array(),
			$dependencies,
			(string) ( $data['idempotency_key'] ?? '' ),
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Operation\Reversibility::class, (string) ( $data['reversibility'] ?? Reversibility::IRREVERSIBLE ) ),
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Launch\ConflictPolicy::class, (string) ( $data['conflict_strategy'] ?? ConflictPolicy::FAIL ) ),
			is_array( $data['redaction_rules'] ?? null ) ? $data['redaction_rules'] : array(),
			(string) ( $data['execution_handler'] ?? '' ),
			null === ( $data['rollback_handler'] ?? null ) ? null : (string) $data['rollback_handler'],
			(string) ( $data['summary'] ?? '' ),
			(int) ( $data['execution_order'] ?? 0 )
		);
	}
}
