<?php
/**
 * Native WordPress recording launch compiler.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\WordPressContent\Recording;

use ScheduledChanges\Application\RecordingOperationGraphCompiler;
use ScheduledChanges\Application\RecordingPlanCompilationRequest;
use ScheduledChanges\Application\RecordingPlanCompilationResult;
use ScheduledChanges\Application\RecordingPlanCompilationService;
use ScheduledChanges\Application\RecordingPlanDraft;
use ScheduledChanges\Domain\Launch\ConflictPolicy;
use ScheduledChanges\Domain\Operation\Operation;
use ScheduledChanges\Domain\Operation\OperationFactory;
use ScheduledChanges\Domain\Operation\OperationGraphBuilder;
use ScheduledChanges\Domain\Operation\OperationType;
use ScheduledChanges\Domain\Operation\Reversibility;
use ScheduledChanges\Domain\Operation\TargetDefinition;
use ScheduledChanges\Domain\Recording\RecordingSession;
use ScheduledChanges\Domain\Shared\NamedTimezone;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Field\FieldRegistry;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Recording\RecordingFinalizationResult;
use ScheduledChanges\Recording\RecordingLaunchCompilerContract;
use ScheduledChanges\Repository\LaunchRepository;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Validation\ValidationEnvironment;

/**
 * Compiles selected native recording changes through the application boundary.
 */
final class NativeWordPressRecordingLaunchCompiler implements RecordingLaunchCompilerContract, RecordingOperationGraphCompiler {
	private FieldRegistry $fields;
	private RecordingPlanCompilationService $compilation;

	/**
	 * Builds the compiler from provider, field, and persistence services.
	 */
	public function __construct(
		ProviderRegistry $providers,
		FieldRegistry $fields,
		LaunchRepository $launches,
		LaunchVersionRepository $versions,
		?ValidationEnvironment $environment = null
	) {
		$this->fields      = $fields;
		$this->compilation = new RecordingPlanCompilationService( $providers, $fields, $launches, $versions, $environment );
	}

	/**
	 * Compiles selected native changes into a validated draft launch.
	 *
	 * @param array<string,mixed> $diff Structured recording diff.
	 * @param string[]            $selected_change_ids Selected change IDs.
	 */
	public function compile(
		RecordingSession $session,
		array $diff,
		array $selected_change_ids,
		string $launch_name,
		string $launch_description,
		NamedTimezone $timezone,
		int $actor_id
	): RecordingFinalizationResult {
		$changes = $this->selectedChanges( $diff, $selected_change_ids );

		if ( array() === $changes ) {
			throw new \RuntimeException( 'At least one recorded change must be selected.' );
		}

		$request = new RecordingPlanCompilationRequest(
			$session,
			$changes,
			$launch_name,
			$launch_description,
			$timezone,
			$actor_id
		);
		$result  = $this->compilation->compile( $request, $this );

		return $this->finalizationResult( $result );
	}

	/**
	 * Compiles normalized native selected intent into an operation graph draft.
	 */
	public function compileGraph( RecordingPlanCompilationRequest $request ): RecordingPlanDraft {
		$session    = $request->session();
		$changes    = $request->selectedChanges();
		$target     = $this->target( $session );
		$context    = new ObjectContext( $session->sourceType(), $session->sourceId(), $session->sourceType() );
		$catalog    = $this->fields->discover( $context )->fields();
		$factory    = new OperationFactory();
		$builder    = new OperationGraphBuilder();
		$snapshot   = $this->snapshotOperation( $target );
		$depends_on = array( $snapshot->uuid() );
		$builder->addTarget( $target );
		$builder->addOperation( $snapshot );
		$this->sortChangesForExecution( $changes );

		foreach ( $changes as $change ) {
			$change_target_id = (int) ( $change['target_id'] ?? 0 );

			if (
				$session->sourceReference() !== (string) ( $change['source_reference'] ?? '' )
				|| $session->sourceId() !== $change_target_id
			) {
				throw new \RuntimeException( 'Recorded change does not belong to this source target.' );
			}

			$field = $catalog->get( (string) ( $change['field_id'] ?? '' ) );

			if ( null === $field || ! $field->writable() || ! $field->schedulable() ) {
				throw new \RuntimeException( 'Recorded field is no longer schedulable.' );
			}

			$operation = $factory->fieldChange(
				$target,
				$field,
				$change['before'] ?? null,
				$change['after'] ?? null,
				$depends_on
			);
			$builder->addOperation( $operation );
			$depends_on = array( $operation->uuid() );
		}

		return new RecordingPlanDraft(
			$builder->build(),
			$request->selectedChangeCount(),
			array(
				'wordpress' => get_bloginfo( 'version' ),
				'php'       => PHP_VERSION,
			),
			array()
		);
	}

	/**
	 * Maps application compilation output to the existing recording result.
	 */
	private function finalizationResult( RecordingPlanCompilationResult $result ): RecordingFinalizationResult {
		return new RecordingFinalizationResult(
			$result->launchUuid(),
			$result->launchId(),
			$result->versionId(),
			$result->selectedChangeCount()
		);
	}

	/**
	 * @param array<string,mixed> $diff Structured diff.
	 * @param string[]            $selected_change_ids Selected change IDs.
	 * @return array<int,array<string,mixed>>
	 */
	private function selectedChanges( array $diff, array $selected_change_ids ): array {
		$selected = array_fill_keys( $selected_change_ids, true );
		$changes  = array();

		foreach ( is_array( $diff['changes'] ?? null ) ? $diff['changes'] : array() as $change ) {
			if ( is_array( $change ) && isset( $selected[ (string) ( $change['change_id'] ?? '' ) ] ) ) {
				$changes[] = $change;
			}
		}

		return $changes;
	}

	/**
	 * Orders native writes so publication date is applied before status.
	 *
	 * @param array<int,array<string,mixed>> $changes Selected changes.
	 */
	private function sortChangesForExecution( array &$changes ): void {
		$priority = array(
			'wordpress.publish_date' => 0,
			'wordpress.status'       => 1000,
		);

		usort(
			$changes,
			static function ( array $left, array $right ) use ( $priority ): int {
				$left_id  = (string) ( $left['field_id'] ?? '' );
				$right_id = (string) ( $right['field_id'] ?? '' );
				$order    = ( $priority[ $left_id ] ?? 100 ) <=> ( $priority[ $right_id ] ?? 100 );

				return 0 !== $order ? $order : strcmp( $left_id, $right_id );
			}
		);
	}

	/**
	 * Builds the existing native source target for field-level conflict checks.
	 */
	private function target( RecordingSession $session ): TargetDefinition {
		$post = get_post( $session->sourceId() );

		if ( ! $post instanceof \WP_Post || $post->post_type !== $session->sourceType() ) {
			throw new \RuntimeException( 'Recorded WordPress source no longer exists.' );
		}

		return new TargetDefinition(
			Uuid::generate(),
			$session->sourceReference(),
			$post->ID,
			$post->post_type,
			$post->post_type,
			null,
			'wordpress-core',
			null
		);
	}

	/**
	 * Builds the just-in-time snapshot operation required before writes.
	 */
	private function snapshotOperation( TargetDefinition $target ): Operation {
		return Operation::create(
			OperationType::CAPTURE_SNAPSHOT,
			$target,
			null,
			$target->providerId(),
			null,
			null,
			'object',
			array(),
			array( 'capability' => 'edit_post' ),
			array(),
			Reversibility::REVERSIBLE,
			ConflictPolicy::FAIL,
			array(),
			'system.capture_snapshot',
			null,
			'Capture a just-in-time snapshot for ' . $target->reference() . '.'
		);
	}
}
