<?php
/**
 * Snapshot capture coordinator.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Snapshot;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are escaped at UI boundaries.
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals -- scheduled_changes_ is the stable plugin prefix.

use ScheduledChanges\Domain\Operation\OperationGraph;
use ScheduledChanges\Domain\Shared\UtcTimestamp;
use ScheduledChanges\Domain\Snapshot\SnapshotKind;
use ScheduledChanges\Domain\Snapshot\SnapshotRecord;
use ScheduledChanges\Execution\ExecutionState;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderRegistry;
use ScheduledChanges\Provider\ProviderSnapshot;
use ScheduledChanges\Repository\SnapshotRepository;

/**
 * Captures only fields referenced by the immutable plan.
 */
final class SnapshotCoordinator {
	private ProviderRegistry $providers;
	private SnapshotRepository $snapshots;

	/**
	 * @param ProviderRegistry   $providers Provider registry.
	 * @param SnapshotRepository $snapshots Snapshot repository.
	 */
	public function __construct( ProviderRegistry $providers, SnapshotRepository $snapshots ) {
		$this->providers = $providers;
		$this->snapshots = $snapshots;
	}

	/**
	 * Captures field-level provider snapshots for one run.
	 */
	public function capture(
		int $run_id,
		int $version_id,
		OperationGraph $graph,
		ExecutionState $state,
		string $kind
	): void {
		foreach ( $this->fieldMap( $graph ) as $target_reference => $providers ) {
			$target    = $graph->targetByReference( $target_reference );
			$object_id = $state->resolve( $target_reference );

			if ( null === $target || null === $object_id ) {
				continue;
			}

			$context = new ObjectContext( $target->targetType(), $object_id, $target->subtype() );

			foreach ( $providers as $provider_id => $field_data ) {
				$provider = $this->providers->get( $provider_id );

				if ( null === $provider || ! $provider->supports( $context ) ) {
					throw new \RuntimeException( 'Snapshot provider is unavailable for ' . $target_reference . '.' );
				}

				$provider_snapshot = $provider->snapshot( $context );
				$values            = array_intersect_key(
					$provider_snapshot->values(),
					array_fill_keys( $field_data['paths'], true )
				);
				$values            = array_diff_key(
					$values,
					array_fill_keys( $field_data['sensitive'], true )
				);
				$snapshot          = new ProviderSnapshot(
					$provider_snapshot->providerId(),
					$provider_snapshot->providerVersion(),
					$values
				);

				$this->snapshots->create(
					SnapshotRecord::capture(
						$run_id,
						$version_id,
						$target_reference,
						$snapshot,
						$kind,
						array( 'omitted_sensitive_fields' => $field_data['sensitive'] ),
						null,
						$this->retentionDate()
					)
				);
			}
		}
	}

	/**
	 * @return array<string,array<string,array{paths:string[],sensitive:string[]}>>
	 */
	private function fieldMap( OperationGraph $graph ): array {
		$map = array();

		foreach ( $graph->operations() as $operation ) {
			$field_path = $operation->fieldPath();

			if ( null === $field_path ) {
				continue;
			}

			$target_reference                           = $operation->targetReference();
			$provider_id                                = $operation->providerId();
			$map[ $target_reference ][ $provider_id ] ??= array(
				'paths'     => array(),
				'sensitive' => array(),
			);
			$map[ $target_reference ][ $provider_id ]['paths'][] = $field_path;

			if ( (bool) ( $operation->redactionRules()['sensitive'] ?? false ) ) {
				$map[ $target_reference ][ $provider_id ]['sensitive'][] = $field_path;
			}
		}

		foreach ( $map as &$providers ) {
			foreach ( $providers as &$field_data ) {
				$field_data['paths']     = array_values( array_unique( $field_data['paths'] ) );
				$field_data['sensitive'] = array_values( array_unique( $field_data['sensitive'] ) );
				sort( $field_data['paths'] );
				sort( $field_data['sensitive'] );
			}
			unset( $field_data );
		}
		unset( $providers );

		return $map;
	}

	/**
	 * Returns the configured snapshot retention timestamp.
	 */
	private function retentionDate(): UtcTimestamp {
		$days = max( 1, (int) apply_filters( 'scheduled_changes_snapshot_retention_days', 90 ) );

		return new UtcTimestamp(
			( new \DateTimeImmutable( 'now', new \DateTimeZone( 'UTC' ) ) )
				->modify( '+' . $days . ' days' )
		);
	}
}
