<?php
/**
 * WPDB snapshot repository.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Repository;

// phpcs:disable WordPress.DB -- Scheduler-owned custom tables require direct wpdb queries.
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are escaped at UI boundaries.

use ScheduledChanges\Database\TableNames;
use ScheduledChanges\Domain\Shared\UtcTimestamp;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Domain\Snapshot\SnapshotKind;
use ScheduledChanges\Domain\Snapshot\SnapshotRecord;
use ScheduledChanges\Support\Json;

/**
 * Stores checksummed canonical snapshots in the scheduler snapshot table.
 */
final class WpdbSnapshotRepository implements SnapshotRepository {
	private \wpdb $wpdb;

	private string $table;

	public function __construct( ?\wpdb $database = null, ?TableNames $tables = null ) {
		$database = \ScheduledChanges\Support\WpdbConnection::resolve( $database );

		$this->wpdb  = $database;
		$this->table = ( $tables ?? new TableNames( $database ) )->get( 'snapshots' );
	}

	public function create( SnapshotRecord $snapshot ): int {
		if ( null !== $snapshot->id() ) {
			throw new \InvalidArgumentException( 'Cannot create an already-persisted snapshot.' );
		}

		$retain_until = $snapshot->retainUntil();
		$inserted     = $this->wpdb->insert(
			$this->table,
			array(
				'uuid'              => $snapshot->uuid()->value(),
				'run_id'            => $snapshot->runId(),
				'launch_version_id' => $snapshot->launchVersionId(),
				'target_reference'  => $snapshot->targetReference(),
				'provider_id'       => $snapshot->providerId(),
				'provider_version'  => $snapshot->providerVersion(),
				'snapshot_kind'     => $snapshot->kind(),
				'data_json'         => Json::canonicalEncode( $snapshot->values() ),
				'checksum'          => $snapshot->checksum(),
				'redaction_json'    => Json::encode( $snapshot->redaction() ),
				'encryption_marker' => $snapshot->encryptionMarker(),
				'captured_at_utc'   => $snapshot->capturedAt()->toStorage(),
				'retain_until_utc'  => null === $retain_until ? null : $retain_until->toStorage(),
			),
			array( '%s', '%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
		);

		if ( false === $inserted ) {
			throw new RepositoryException( 'Unable to persist provider snapshot: ' . $this->wpdb->last_error );
		}

		return (int) $this->wpdb->insert_id;
	}

	public function findForRun( int $run_id, string $kind ): array {
		$sql  = $this->wpdb->prepare(
			"SELECT * FROM {$this->table}
				WHERE run_id = %d
					AND snapshot_kind = %s
				ORDER BY id ASC",
			$run_id,
			$kind
		);
		$rows = $this->wpdb->get_results( $sql, ARRAY_A );

		return array_map(
			fn ( array $row ): SnapshotRecord => $this->hydrate( $row ),
			is_array( $rows ) ? $rows : array()
		);
	}

	public function deleteByRun( int $run_id ): int {
		$deleted = $this->wpdb->delete( $this->table, array( 'run_id' => $run_id ), array( '%d' ) );

		if ( false === $deleted ) {
			throw new RepositoryException( 'Unable to delete run snapshots: ' . $this->wpdb->last_error );
		}

		return $deleted;
	}

	/**
	 * @param array<string,mixed> $row Snapshot row.
	 */
	private function hydrate( array $row ): SnapshotRecord {
		return new SnapshotRecord(
			(int) $row['id'],
			new Uuid( (string) $row['uuid'] ),
			null === $row['run_id'] ? null : (int) $row['run_id'],
			(int) $row['launch_version_id'],
			(string) $row['target_reference'],
			(string) $row['provider_id'],
			(string) $row['provider_version'],
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Snapshot\SnapshotKind::class, (string) $row['snapshot_kind'] ),
			Json::decodeObject( (string) $row['data_json'] ),
			(string) $row['checksum'],
			Json::decodeObject( (string) $row['redaction_json'] ),
			null === $row['encryption_marker'] ? null : (string) $row['encryption_marker'],
			UtcTimestamp::fromStorage( (string) $row['captured_at_utc'] ),
			$this->timestamp( $row['retain_until_utc'] )
		);
	}

	/**
	 * @param mixed $value
	 */
	private function timestamp( $value ): ?UtcTimestamp {
		return null === $value || '' === $value
			? null
			: UtcTimestamp::fromStorage( (string) $value );
	}
}
