<?php
/**
 * WPDB launch 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\Launch\ConflictPolicy;
use ScheduledChanges\Domain\Launch\CreationSource;
use ScheduledChanges\Domain\Launch\FailurePolicy;
use ScheduledChanges\Domain\Launch\Launch;
use ScheduledChanges\Domain\Launch\LaunchStatus;
use ScheduledChanges\Domain\Shared\NamedTimezone;
use ScheduledChanges\Domain\Shared\UtcTimestamp;
use ScheduledChanges\Domain\Shared\Uuid;

/**
 * Stores launches in scheduler-owned custom tables.
 */
final class WpdbLaunchRepository implements LaunchRepository {
	/**
	 * WordPress database connection.
	 *
	 * @var \wpdb
	 */
	private \wpdb $wpdb;

	/**
	 * Launch table.
	 *
	 * @var string
	 */
	private string $table;

	/**
	 * Creates the repository.
	 *
	 * @param \wpdb|null      $database WordPress database connection.
	 * @param TableNames|null $tables Table resolver.
	 */
	public function __construct( ?\wpdb $database = null, ?TableNames $tables = null ) {
		$database = \ScheduledChanges\Support\WpdbConnection::resolve( $database );

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

	/**
	 * {@inheritDoc}
	 */
	public function create( Launch $launch ): int {
		if ( null !== $launch->id() ) {
			throw new \InvalidArgumentException( 'Cannot create a launch that already has an internal ID.' );
		}

		$start_at     = $launch->startAt();
		$end_at       = $launch->endAt();
		$scheduled_at = $launch->scheduledAt();
		$completed_at = $launch->completedAt();
		$inserted     = $this->wpdb->insert(
			$this->table,
			array(
				'uuid'               => $launch->uuid()->value(),
				'name'               => $launch->name(),
				'description'        => $launch->description(),
				'owner_id'           => $launch->ownerId(),
				'status'             => $launch->status(),
				'timezone'           => $launch->timezone()->value(),
				'start_at_utc'       => null === $start_at ? null : $start_at->toStorage(),
				'end_at_utc'         => null === $end_at ? null : $end_at->toStorage(),
				'recurrence_rule'    => $launch->recurrenceRule(),
				'current_version_id' => $launch->currentVersionId(),
				'conflict_policy'    => $launch->conflictPolicy(),
				'failure_policy'     => $launch->failurePolicy(),
				'creation_source'    => $launch->creationSource(),
				'created_at_utc'     => $launch->createdAt()->toStorage(),
				'updated_at_utc'     => $launch->updatedAt()->toStorage(),
				'scheduled_at_utc'   => null === $scheduled_at ? null : $scheduled_at->toStorage(),
				'completed_at_utc'   => null === $completed_at ? null : $completed_at->toStorage(),
			),
			array(
				'%s',
				'%s',
				'%s',
				'%d',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%d',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
				'%s',
			)
		);

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

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

	/**
	 * {@inheritDoc}
	 */
	public function findByUuid( Uuid $uuid ): ?Launch {
		$sql = $this->wpdb->prepare(
			"SELECT * FROM {$this->table} WHERE uuid = %s LIMIT 1",
			$uuid->value()
		);
		$row = $this->wpdb->get_row( $sql, ARRAY_A );

		if ( null === $row ) {
			return null;
		}

		return $this->hydrate( $row );
	}

	/**
	 * {@inheritDoc}
	 */
	public function findById( int $id ): ?Launch {
		$sql = $this->wpdb->prepare(
			"SELECT * FROM {$this->table} WHERE id = %d LIMIT 1",
			$id
		);
		$row = $this->wpdb->get_row( $sql, ARRAY_A );

		return null === $row ? null : $this->hydrate( $row );
	}

	/**
	 * {@inheritDoc}
	 */
	public function markScheduled( int $id, int $version_id, UtcTimestamp $start_at ): void {
		$updated = $this->wpdb->update(
			$this->table,
			array(
				'status'             => LaunchStatus::SCHEDULED,
				'current_version_id' => $version_id,
				'start_at_utc'       => $start_at->toStorage(),
				'scheduled_at_utc'   => $start_at->toStorage(),
				'updated_at_utc'     => UtcTimestamp::now()->toStorage(),
				'completed_at_utc'   => null,
			),
			array( 'id' => $id ),
			array( '%s', '%d', '%s', '%s', '%s', '%s' ),
			array( '%d' )
		);

		if ( false === $updated ) {
			throw new RepositoryException( 'Unable to mark launch scheduled: ' . $this->wpdb->last_error );
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function setCurrentVersion( int $id, int $version_id ): void {
		$updated = $this->wpdb->update(
			$this->table,
			array(
				'current_version_id' => $version_id,
				'updated_at_utc'     => UtcTimestamp::now()->toStorage(),
			),
			array( 'id' => $id ),
			array( '%d', '%s' ),
			array( '%d' )
		);

		if ( false === $updated ) {
			throw new RepositoryException( 'Unable to set launch current version: ' . $this->wpdb->last_error );
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public function updateStatus( int $id, string $status, ?UtcTimestamp $completed_at = null ): void {
		$updated = $this->wpdb->update(
			$this->table,
			array(
				'status'           => $status,
				'updated_at_utc'   => UtcTimestamp::now()->toStorage(),
				'completed_at_utc' => null === $completed_at ? null : $completed_at->toStorage(),
			),
			array( 'id' => $id ),
			array( '%s', '%s', '%s' ),
			array( '%d' )
		);

		if ( false === $updated ) {
			throw new RepositoryException( 'Unable to update launch status: ' . $this->wpdb->last_error );
		}
	}

	/**
	 * Deletes a launch by UUID.
	 */
	public function deleteByUuid( Uuid $uuid ): bool {
		$deleted = $this->wpdb->delete(
			$this->table,
			array( 'uuid' => $uuid->value() ),
			array( '%s' )
		);

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

		return $deleted > 0;
	}

	/**
	 * Reconstitutes a launch from a database row.
	 *
	 * @param array<string,mixed> $row Database row.
	 */
	private function hydrate( array $row ): Launch {
		return new Launch(
			(int) $row['id'],
			new Uuid( (string) $row['uuid'] ),
			(string) $row['name'],
			(string) $row['description'],
			(int) $row['owner_id'],
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Launch\LaunchStatus::class, (string) $row['status'] ),
			new NamedTimezone( (string) $row['timezone'] ),
			$this->timestamp( $row['start_at_utc'] ),
			$this->timestamp( $row['end_at_utc'] ),
			null === $row['recurrence_rule'] ? null : (string) $row['recurrence_rule'],
			null === $row['current_version_id'] ? null : (int) $row['current_version_id'],
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Launch\ConflictPolicy::class, (string) $row['conflict_policy'] ),
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Launch\FailurePolicy::class, (string) $row['failure_policy'] ),
			\ScheduledChanges\Support\EnumValue::from( \ScheduledChanges\Domain\Launch\CreationSource::class, (string) $row['creation_source'] ),
			UtcTimestamp::fromStorage( (string) $row['created_at_utc'] ),
			UtcTimestamp::fromStorage( (string) $row['updated_at_utc'] ),
			$this->timestamp( $row['scheduled_at_utc'] ),
			$this->timestamp( $row['completed_at_utc'] )
		);
	}

	/**
	 * Converts a nullable database datetime.
	 *
	 * @param mixed $value Database value.
	 */
	private function timestamp( $value ): ?UtcTimestamp {
		return null === $value || '' === $value
			? null
			: UtcTimestamp::fromStorage( (string) $value );
	}
}
