<?php
/**
 * Durable canonical provider snapshot.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Domain\Snapshot;

use ScheduledChanges\Domain\Shared\UtcTimestamp;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Provider\ProviderSnapshot;
use ScheduledChanges\Support\Json;

/**
 * Stores only the canonical values needed for conflict checks and compensation.
 */
final class SnapshotRecord {
	private ?int $id;
	private Uuid $uuid;
	private ?int $run_id;
	private int $launchVersionId;
	private string $targetReference;
	private string $providerId;
	private string $providerVersion;
	private string $kind;
	/**
	 * @var array<string, mixed>
	 */
	private array $values;
	private string $checksum;
	/**
	 * @var array<string, mixed>
	 */
	private array $redaction;
	private ?string $encryptionMarker;
	private UtcTimestamp $capturedAt;
	private ?UtcTimestamp $retainUntil;
	/**
	 * @param array<string,mixed> $values Canonical values keyed by field path.
	 * @param array<string,mixed> $redaction Redaction markers.
	 */
	public function __construct(
		?int $id,
		Uuid $uuid,
		?int $run_id,
		int $launchVersionId,
		string $targetReference,
		string $providerId,
		string $providerVersion,
		string $kind,
		array $values,
		string $checksum,
		array $redaction,
		?string $encryptionMarker,
		UtcTimestamp $capturedAt,
		?UtcTimestamp $retainUntil
	) {
		$this->id               = $id;
		$this->uuid             = $uuid;
		$this->run_id           = $run_id;
		$this->launchVersionId  = $launchVersionId;
		$this->targetReference  = $targetReference;
		$this->providerId       = $providerId;
		$this->providerVersion  = $providerVersion;
		$this->kind             = $kind;
		$this->values           = $values;
		$this->checksum         = $checksum;
		$this->redaction        = $redaction;
		$this->encryptionMarker = $encryptionMarker;
		$this->capturedAt       = $capturedAt;
		$this->retainUntil      = $retainUntil;
		if ( null !== $this->id && $this->id <= 0 ) {
			throw new \InvalidArgumentException( 'Snapshot IDs must be positive.' );
		}

		if ( null !== $this->run_id && $this->run_id <= 0 ) {
			throw new \InvalidArgumentException( 'Snapshot run IDs must be positive.' );
		}

		if ( $this->launchVersionId <= 0 || '' === trim( $this->targetReference ) ) {
			throw new \InvalidArgumentException( 'Snapshot version and target identity are required.' );
		}

		if ( '' === trim( $this->providerId ) || '' === trim( $this->providerVersion ) ) {
			throw new \InvalidArgumentException( 'Snapshot provider identity is required.' );
		}

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

		if ( ! hash_equals( $this->checksum, self::checksumFor( $this->values ) ) ) {
			throw new \UnexpectedValueException( 'Snapshot checksum does not match canonical values.' );
		}
	}

	/**
	 * @param array<string,mixed> $redaction Redaction markers.
	 */
	public static function capture(
		?int $run_id,
		int $launchVersionId,
		string $targetReference,
		ProviderSnapshot $snapshot,
		string $kind,
		array $redaction = array(),
		?string $encryptionMarker = null,
		?UtcTimestamp $retainUntil = null
	): self {
		return new self(
			null,
			Uuid::generate(),
			$run_id,
			$launchVersionId,
			$targetReference,
			$snapshot->providerId(),
			$snapshot->providerVersion(),
			$kind,
			$snapshot->values(),
			self::checksumFor( $snapshot->values() ),
			$redaction,
			$encryptionMarker,
			UtcTimestamp::now(),
			$retainUntil
		);
	}

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

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

	public function runId(): ?int {
		return $this->run_id;
	}

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

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

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

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

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

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

	/**
	 * @return mixed
	 */
	public function value( string $fieldPath ) {
		return $this->values[ $fieldPath ] ?? null;
	}

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

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

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

	public function capturedAt(): UtcTimestamp {
		return $this->capturedAt;
	}

	public function retainUntil(): ?UtcTimestamp {
		return $this->retainUntil;
	}

	public function providerSnapshot(): ProviderSnapshot {
		return new ProviderSnapshot( $this->providerId, $this->providerVersion, $this->values );
	}

	/**
	 * @param array<string,mixed> $values Canonical snapshot values.
	 */
	private static function checksumFor( array $values ): string {
		return hash( 'sha256', Json::canonicalEncode( $values ) );
	}
}
