<?php
/**
 * Launch REST API controller.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Rest;

use ScheduledChanges\Application\LaunchLifecycleException;
use ScheduledChanges\Application\LaunchLifecycleService;
use ScheduledChanges\Capabilities\CapabilityManager;
use ScheduledChanges\Domain\Launch\LaunchStatus;
use ScheduledChanges\Domain\Shared\NamedTimezone;
use ScheduledChanges\Domain\Shared\UtcTimestamp;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Repository\AdminDashboardRepository;
use ScheduledChanges\Repository\LaunchRepository;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Rollback\CreatedTargetRollbackPolicy;
use ScheduledChanges\Rollback\LaunchRollbackService;
use ScheduledChanges\Rollback\RollbackConflictPolicy;
use ScheduledChanges\Scheduler\LaunchMutationScheduler;
use ScheduledChanges\Support\PublicErrorMessages;

/**
 * Exposes launch calendar read and scheduling actions over REST.
 */
final class LaunchesController {
	use RegistersVersionedRoutes;

	private AdminDashboardRepository $dashboard;
	private ?ProviderManager $providers;
	private LaunchLifecycleService $lifecycle;
	private bool $rollback_route_enabled;
	private ?LaunchesRestSchemas $schemas = null;

	/**
	 * Creates the launch REST controller.
	 *
	 * @param AdminDashboardRepository     $dashboard Launch read model.
	 * @param LaunchRepository             $launches Launch repository.
	 * @param LaunchVersionRepository      $versions Launch version repository.
	 * @param LaunchMutationScheduler|null $scheduler Scheduler service.
	 * @param ProviderManager|null         $providers Provider manager.
	 * @param RunRepository|null           $runs Run repository.
	 * @param LaunchRollbackService|null   $rollback Rollback service.
	 * @param bool                         $rollback_route_enabled Whether rollback REST route should be registered.
	 * @param LaunchLifecycleService|null  $lifecycle Shared lifecycle service.
	 */
	public function __construct(
		AdminDashboardRepository $dashboard,
		LaunchRepository $launches,
		LaunchVersionRepository $versions,
		?LaunchMutationScheduler $scheduler,
		?ProviderManager $providers = null,
		?RunRepository $runs = null,
		?LaunchRollbackService $rollback = null,
		bool $rollback_route_enabled = true,
		?LaunchLifecycleService $lifecycle = null
	) {
		$this->dashboard              = $dashboard;
		$this->providers              = $providers;
		$this->lifecycle              = $lifecycle ?? new LaunchLifecycleService( $launches, $versions, $scheduler, $providers, $runs, $rollback );
		$this->rollback_route_enabled = $rollback_route_enabled;
		$this->schemas                = new LaunchesRestSchemas();
	}

	/**
	 * Registers launch routes.
	 */
	public function registerRoutes(): void {
		$this->registerVersionedRoute(
			'/targets',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'listTargets' ),
					'permission_callback' => array( $this, 'canView' ),
					'args'                => array_merge(
						$this->paginationArgs(),
						array(
							'context' => array(
								'type'              => 'string',
								'default'           => 'scheduled',
								'enum'              => array( 'scheduled', 'recordable' ),
								'sanitize_callback' => 'sanitize_key',
							),
						)
					),
				),
				'schema' => array( $this, 'targetSchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/providers',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'listProviders' ),
					'permission_callback' => array( $this, 'canManageProviders' ),
				),
				'schema' => array( $this, 'providerSchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'listLaunches' ),
					'permission_callback' => array( $this, 'canView' ),
					'args'                => $this->paginationArgs(),
				),
				'schema' => array( $this, 'schema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'getLaunch' ),
					'permission_callback' => array( $this, 'canView' ),
					'args'                => $this->uuidArgs(),
				),
				'schema' => array( $this, 'detailSchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})/validate',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'validateLaunch' ),
					'permission_callback' => array( $this, 'canView' ),
					'args'                => $this->uuidArgs(),
				),
				'schema' => array( $this, 'validationSchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})/schedule',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'scheduleLaunch' ),
					'permission_callback' => array( $this, 'canSchedule' ),
					'args'                => array_merge(
						$this->uuidArgs(),
						array(
							'go_live_date' => array(
								'description'       => __( 'Local go-live date in Y-m-d format.', 'jjs-post-scheduler' ),
								'type'              => 'string',
								'required'          => true,
								'validate_callback' => array( $this, 'isDateString' ),
							),
							'go_live_time' => array(
								'description'       => __( 'Local go-live time in H:i format.', 'jjs-post-scheduler' ),
								'type'              => 'string',
								'required'          => true,
								'validate_callback' => array( $this, 'isTimeString' ),
							),
						),
					),
				),
				'schema' => array( $this, 'scheduleSchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})/run',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'runLaunch' ),
					'permission_callback' => array( $this, 'canExecute' ),
					'args'                => array_merge(
						$this->uuidArgs(),
						array(
							'dry_run' => array(
								'type'              => 'boolean',
								'default'           => true,
								'sanitize_callback' => 'rest_sanitize_boolean',
							),
						)
					),
				),
				'schema' => array( $this, 'runSchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})/runs/(?P<run_id>\d+)/retry',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'retryLaunch' ),
					'permission_callback' => array( $this, 'canExecute' ),
					'args'                => array_merge(
						$this->uuidArgs(),
						array(
							'run_id' => array(
								'type'              => 'integer',
								'required'          => true,
								'minimum'           => 1,
								'sanitize_callback' => 'absint',
							),
						)
					),
				),
				'schema' => array( $this, 'retrySchema' ),
			)
		);
		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})/cancel',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'cancelLaunch' ),
					'permission_callback' => array( $this, 'canSchedule' ),
					'args'                => $this->uuidArgs(),
				),
				'schema' => array( $this, 'cancelSchema' ),
			)
		);
		if ( ! $this->rollback_route_enabled || ! $this->lifecycle->rollbackAvailable() ) {
			return;
		}

		$this->registerVersionedRoute(
			'/launches/(?P<uuid>[0-9a-fA-F-]{36})/rollback',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'rollbackLaunch' ),
					'permission_callback' => array( $this, 'canRollback' ),
					'args'                => array_merge(
						$this->uuidArgs(),
						array(
							'conflict_policy' => array(
								'type'    => 'string',
								'default' => RollbackConflictPolicy::PRESERVE,
								'enum'    => array( RollbackConflictPolicy::PRESERVE, RollbackConflictPolicy::FORCE, RollbackConflictPolicy::MERGE, RollbackConflictPolicy::ABORT ),
							),
							'created_targets' => array(
								'type'    => 'string',
								'default' => CreatedTargetRollbackPolicy::DISABLE,
								'enum'    => array( CreatedTargetRollbackPolicy::DISABLE, CreatedTargetRollbackPolicy::KEEP, CreatedTargetRollbackPolicy::REMOVE ),
							),
						)
					),
				),
				'schema' => array( $this, 'rollbackSchema' ),
			)
		);
	}

	/**
	 * Lists permission-filtered targets for schedules or recording discovery.
	 *
	 * @param \WP_REST_Request $request REST request.
	 */
	public function listTargets( \WP_REST_Request $request ): \WP_REST_Response {
		$per_page = $this->perPage( $request );
		$page     = max( 1, (int) $request['page'] );
		$rows     = 'recordable' === (string) $request['context']
			? $this->dashboard->recordableTargets( 100 )
			: $this->dashboard->targetSummaries( 100 );
		$rows     = array_values( array_map( array( $this, 'publicTargetRow' ), $rows ) );
		$total    = count( $rows );
		$response = rest_ensure_response( array_slice( $rows, ( $page - 1 ) * $per_page, $per_page ) );

		$response->header( 'X-WP-Total', (string) $total );
		$response->header( 'X-WP-TotalPages', (string) (int) ceil( $total / $per_page ) );

		return $response;
	}

	/**
	 * Lists active provider contracts without exposing provider internals.
	 */
	public function listProviders(): \WP_REST_Response {
		if ( null === $this->providers || ! $this->providers->loaded() ) {
			return rest_ensure_response( array() );
		}

		$errors = $this->providers->errors();
		$rows   = array();

		foreach ( $this->providers->providers()->versions() as $id => $version ) {
			$rows[] = array(
				'id'      => (string) $id,
				'version' => (string) $version,
				'status'  => array() === $errors ? 'loaded' : 'loaded_with_errors',
			);
		}

		return rest_ensure_response( $rows );
	}

	/**
	 * Lists launches with bounded pagination.
	 *
	 * @param \WP_REST_Request $request REST request.
	 */
	public function listLaunches( \WP_REST_Request $request ): \WP_REST_Response {
		$per_page = $this->perPage( $request );
		$page     = max( 1, (int) $request['page'] );
		$total    = $this->dashboard->launchCount();
		$response = rest_ensure_response(
			array_map(
				array( $this, 'publicLaunchRow' ),
				$this->dashboard->launches( $per_page, ( $page - 1 ) * $per_page )
			)
		);

		$response->header( 'X-WP-Total', (string) $total );
		$response->header( 'X-WP-TotalPages', (string) (int) ceil( $total / $per_page ) );

		return $response;
	}

	/**
	 * Returns one launch by UUID.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function getLaunch( \WP_REST_Request $request ) {
		$detail = $this->dashboard->launchDetailByUuid( (string) $request['uuid'] );

		if ( null === $detail ) {
			return $this->notFound();
		}

		return rest_ensure_response(
			$this->detailResponse( $detail, current_user_can( CapabilityManager::VIEW_LOGS ) )
		);
	}

	/**
	 * Builds a launch detail response with log-like data gated separately.
	 *
	 * @param array<string,mixed> $detail Dashboard detail row.
	 * @return array<string,mixed>
	 */
	private function detailResponse( array $detail, bool $can_view_logs ): array {
		$response            = $this->publicLaunchRow( is_array( $detail['launch'] ?? null ) ? $detail['launch'] : array() );
		$response['targets'] = array_map(
			array( $this, 'publicTargetRow' ),
			is_array( $detail['targets'] ?? null ) ? $detail['targets'] : array()
		);

		if ( $can_view_logs ) {
			$response['operations'] = is_array( $detail['operations'] ?? null ) ? $detail['operations'] : array();
			$response['runs']       = is_array( $detail['runs'] ?? null ) ? $detail['runs'] : array();
			$response['timeline']   = is_array( $detail['timeline'] ?? null ) ? $detail['timeline'] : array();
		}

		return $response;
	}

	/**
	 * Validates the current immutable version of one launch.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function validateLaunch( \WP_REST_Request $request ) {
		try {
			$result = $this->lifecycle->validate( new Uuid( (string) $request['uuid'] ), get_current_user_id() );
			$data   = $result->data();

			return rest_ensure_response( $data['report'] );
		} catch ( LaunchLifecycleException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_validation_failed', __( 'Launch validation could not be completed.', 'jjs-post-scheduler' ), 400, $exception );
		} catch ( \RuntimeException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_validation_failed', __( 'Launch validation could not be completed.', 'jjs-post-scheduler' ), 400, $exception );
		}
	}

	/**
	 * Schedules or reschedules the launch's current immutable version.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function scheduleLaunch( \WP_REST_Request $request ) {
		try {
			$uuid         = new Uuid( (string) $request['uuid'] );
			$scheduled_at = $this->scheduledTimestamp(
				(string) $request['go_live_date'],
				(string) $request['go_live_time'],
				$this->lifecycle->launchTimezone( $uuid )
			);
			$result       = $this->lifecycle->schedule( $uuid, $scheduled_at );
			$data         = $result->data();
			$row          = $this->dashboard->launchByUuid( (string) $data['launch_uuid'] );

			return rest_ensure_response(
				array(
					'actionId'       => $data['action_id'],
					'versionId'      => $data['version_id'],
					'scheduledAtUtc' => $data['scheduled_at_utc'],
					'launch'         => null === $row ? null : $this->publicLaunchRow( $row ),
				)
			);
		} catch ( \InvalidArgumentException $exception ) {
			return $this->exceptionError( 'scheduled_changes_invalid_schedule_time', __( 'Choose a valid future go-live date and time.', 'jjs-post-scheduler' ), 400, $exception );
		} catch ( LaunchLifecycleException $exception ) {
			return $this->lifecycleError( $exception, 'scheduled_changes_launch_schedule_failed', __( 'Scheduled change could not be scheduled.', 'jjs-post-scheduler' ), 400 );
		} catch ( \RuntimeException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_schedule_failed', __( 'Scheduled change could not be scheduled.', 'jjs-post-scheduler' ), 400, $exception );
		}
	}

	/**
	 * Returns a dry-run projection or executes one launch immediately.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function runLaunch( \WP_REST_Request $request ) {
		try {
			$uuid = new Uuid( (string) $request['uuid'] );

			if ( false !== $request['dry_run'] ) {
				$result = $this->lifecycle->dryRun( $uuid, get_current_user_id() );
				$data   = $result->data();

				return rest_ensure_response( $data['report'] );
			}

			$result = $this->lifecycle->runNow( $uuid, UtcTimestamp::now() );
			$data   = $result->data();

			return rest_ensure_response(
				array(
					'uuid'           => $data['launch_uuid'],
					'versionId'      => $data['version_id'],
					'actionId'       => $data['action_id'],
					'scheduledAtUtc' => $data['scheduled_at_utc'],
					'status'         => $data['status'],
				)
			);
		} catch ( \InvalidArgumentException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_run_invalid', __( 'Scheduled change could not run now.', 'jjs-post-scheduler' ), 400, $exception );
		} catch ( LaunchLifecycleException $exception ) {
			return $this->lifecycleError( $exception, 'scheduled_changes_launch_run_failed', __( 'Scheduled change could not run now.', 'jjs-post-scheduler' ), 400 );
		} catch ( \RuntimeException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_run_failed', __( 'Scheduled change could not run now.', 'jjs-post-scheduler' ), 400, $exception );
		}
	}

	/**
	 * Schedules a manual retry for a terminal source run.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function retryLaunch( \WP_REST_Request $request ) {
		try {
			$result = $this->lifecycle->retry(
				new Uuid( (string) $request['uuid'] ),
				(int) $request['run_id'],
				get_current_user_id()
			);
			$data   = $result->data();

			return rest_ensure_response(
				array(
					'uuid'            => $data['launch_uuid'],
					'sourceRunId'     => $data['source_run_id'],
					'versionId'       => $data['version_id'],
					'actionId'        => $data['action_id'],
					'attemptNumber'   => $data['attempt_number'],
					'occurrenceAtUtc' => $data['occurrence_at_utc'],
					'status'          => $data['status'],
				)
			);
		} catch ( \InvalidArgumentException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_retry_invalid', __( 'Scheduled change could not be retried.', 'jjs-post-scheduler' ), 400, $exception );
		} catch ( LaunchLifecycleException $exception ) {
			return $this->lifecycleError( $exception, 'scheduled_changes_launch_retry_failed', __( 'Scheduled change could not be retried.', 'jjs-post-scheduler' ), 400 );
		} catch ( \RuntimeException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_retry_failed', __( 'Scheduled change could not be retried.', 'jjs-post-scheduler' ), 400, $exception );
		}
	}

	/**
	 * Cancels a scheduled launch.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function cancelLaunch( \WP_REST_Request $request ) {
		try {
			$result = $this->lifecycle->cancel( new Uuid( (string) $request['uuid'] ), get_current_user_id() );
			$data   = $result->data();
			$row    = $this->dashboard->launchByUuid( (string) $data['launch_uuid'] );

			return rest_ensure_response(
				array(
					'cancelledActionCount' => $data['cancelled_action_count'],
					'versionId'            => $data['version_id'],
					'launch'               => null === $row ? null : $this->publicLaunchRow( $row ),
				)
			);
		} catch ( \InvalidArgumentException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_cancel_invalid_state', __( 'Scheduled change could not be canceled.', 'jjs-post-scheduler' ), 400, $exception );
		} catch ( LaunchLifecycleException $exception ) {
			return $this->lifecycleError( $exception, 'scheduled_changes_launch_cancel_failed', __( 'Scheduled change could not be canceled.', 'jjs-post-scheduler' ), 400 );
		} catch ( \RuntimeException $exception ) {
			return $this->exceptionError( 'scheduled_changes_launch_cancel_failed', __( 'Scheduled change could not be canceled.', 'jjs-post-scheduler' ), 400, $exception );
		}
	}

	/**
	 * Rolls back the latest successful run for one launch.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function rollbackLaunch( \WP_REST_Request $request ) {
		try {
			$result = $this->lifecycle->rollbackLatest(
				new Uuid( (string) $request['uuid'] ),
				(string) $request['conflict_policy'],
				(string) $request['created_targets'],
				get_current_user_id()
			);
			$data   = $result->data();

			return rest_ensure_response(
				array(
					'uuid'             => $data['launch_uuid'],
					'sourceRunId'      => $data['source_run_id'],
					'rollbackRunId'    => $data['rollback_run_id'],
					'status'           => $data['status'],
					'conflictPolicy'   => $data['conflict_policy'],
					'createdTargets'   => $data['created_targets'],
					'operationSummary' => $data['operation_summary'],
				)
			);
		} catch ( \InvalidArgumentException $exception ) {
			return $this->exceptionError( 'scheduled_changes_rollback_invalid', __( 'Rollback could not be completed.', 'jjs-post-scheduler' ), 400, $exception );
		} catch ( LaunchLifecycleException $exception ) {
			return $this->lifecycleError( $exception, 'scheduled_changes_rollback_failed', __( 'Rollback could not be completed.', 'jjs-post-scheduler' ), 409 );
		} catch ( \RuntimeException $exception ) {
			return $this->exceptionError( 'scheduled_changes_rollback_failed', __( 'Rollback could not be completed.', 'jjs-post-scheduler' ), 409, $exception );
		}
	}

	/**
	 * Checks read capability.
	 */
	public function canView( ?\WP_REST_Request $request = null ): bool {
		if ( ! current_user_can( CapabilityManager::VIEW_SCHEDULES ) ) {
			return false;
		}

		$uuid = null === $request ? '' : (string) $request['uuid'];

		return '' === $uuid || null !== $this->dashboard->launchByUuid( $uuid );
	}

	/**
	 * Checks schedule-management capability.
	 */
	public function canSchedule( ?\WP_REST_Request $request = null ): bool {
		if ( ! current_user_can( CapabilityManager::SCHEDULE_CHANGES ) ) {
			return false;
		}

		$uuid = null === $request ? '' : (string) $request['uuid'];

		return '' !== $uuid && null !== $this->dashboard->launchByUuid( $uuid );
	}

	/**
	 * Checks provider-management capability.
	 */
	public function canManageProviders(): bool {
		return current_user_can( CapabilityManager::MANAGE_SETTINGS );
	}

	/**
	 * Checks immediate-execution capability and launch visibility.
	 */
	public function canExecute( ?\WP_REST_Request $request = null ): bool {
		return $this->canAccessLaunch( CapabilityManager::EXECUTE_SCHEDULES, $request );
	}

	/**
	 * Checks rollback capability and launch visibility.
	 */
	public function canRollback( ?\WP_REST_Request $request = null ): bool {
		return $this->canAccessLaunch( CapabilityManager::ROLLBACK_SCHEDULES, $request );
	}

	/**
	 * Returns the launch item schema.
	 *
	 * @return array<string,mixed>
	 */
	public function schema(): array {
		return $this->schemas()->schema();
	}

	/**
	 * Returns the expanded launch detail schema.
	 *
	 * @return array<string,mixed>
	 */
	public function detailSchema(): array {
		return $this->schemas()->detailSchema();
	}

	/**
	 * Returns the target discovery item schema.
	 *
	 * @return array<string,mixed>
	 */
	public function targetSchema(): array {
		return $this->schemas()->targetSchema();
	}

	/**
	 * Returns the provider item schema.
	 *
	 * @return array<string,mixed>
	 */
	public function providerSchema(): array {
		return $this->schemas()->providerSchema();
	}

	/**
	 * Returns the validation report schema.
	 *
	 * @return array<string,mixed>
	 */
	public function validationSchema(): array {
		return $this->schemas()->validationSchema();
	}

	/**
	 * Returns the dry-run or execution response schema.
	 *
	 * @return array<string,mixed>
	 */
	public function runSchema(): array {
		return $this->schemas()->runSchema();
	}

	/**
	 * Returns the manual retry response schema.
	 *
	 * @return array<string,mixed>
	 */
	public function retrySchema(): array {
		return $this->schemas()->retrySchema();
	}

	/**
	 * Returns the rollback response schema.
	 *
	 * @return array<string,mixed>
	 */
	public function rollbackSchema(): array {
		return $this->schemas()->rollbackSchema();
	}

	/**
	 * Returns the schedule response schema.
	 *
	 * @return array<string,mixed>
	 */
	public function scheduleSchema(): array {
		return $this->schemas()->scheduleSchema();
	}

	/**
	 * Returns the cancel response schema.
	 *
	 * @return array<string,mixed>
	 */
	public function cancelSchema(): array {
		return $this->schemas()->cancelSchema();
	}

	/**
	 * Validates a Y-m-d date string.
	 *
	 * @param mixed $value Request value.
	 */
	public function isDateString( $value ): bool {
		return $this->schemas()->isDateString( $value );
	}

	/**
	 * Validates an H:i time string.
	 *
	 * @param mixed $value Request value.
	 */
	public function isTimeString( $value ): bool {
		return $this->schemas()->isTimeString( $value );
	}

	/**
	 * Returns the schema helper, including reflection-built test instances.
	 */
	private function schemas(): LaunchesRestSchemas {
		if ( null === $this->schemas ) {
			$this->schemas = new LaunchesRestSchemas();
		}

		return $this->schemas;
	}

	/**
	 * Returns pagination route arguments.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	private function paginationArgs(): array {
		return $this->schemas()->paginationArgs();
	}

	/**
	 * Returns UUID route arguments.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	private function uuidArgs(): array {
		return $this->schemas()->uuidArgs();
	}

	/**
	 * Returns a bounded per-page value.
	 *
	 * @param \WP_REST_Request $request REST request.
	 */
	private function perPage( \WP_REST_Request $request ): int {
		return max( 1, min( 100, (int) $request['per_page'] ) );
	}

	/**
	 * Converts dashboard launch rows to public REST shape.
	 *
	 * @param array<string,mixed> $row Dashboard row.
	 * @return array<string,mixed>
	 */
	private function publicLaunchRow( array $row ): array {
		$status            = (string) ( $row['status'] ?? '' );
		$available_actions = is_array( $row['availableActions'] ?? null ) ? array_values( $row['availableActions'] ) : array();
		$diagnostics       = is_array( $row['diagnostics'] ?? null ) ? array_values( $row['diagnostics'] ) : array();

		$public = array(
			'uuid'             => (string) ( $row['uuid'] ?? '' ),
			'name'             => (string) ( $row['name'] ?? '' ),
			'status'           => $status,
			'health'           => (string) ( $row['health'] ?? '' ),
			'healthReason'     => (string) ( $row['healthReason'] ?? '' ),
			'availableActions' => $available_actions,
			'diagnostics'      => $diagnostics,
			'targetCount'      => (int) ( $row['targetCount'] ?? $row['productCount'] ?? 0 ),
			'startAt'          => (string) ( $row['startAt'] ?? '' ),
			'startDate'        => (string) ( $row['startDate'] ?? '' ),
			'startTime'        => (string) ( $row['startTime'] ?? '' ),
			'endAt'            => (string) ( $row['endAt'] ?? '' ),
			'timezone'         => (string) ( $row['timezone'] ?? '' ),
			'validationState'  => (string) ( $row['validationState'] ?? '' ),
			'lastRunStatus'    => (string) ( $row['lastRunStatus'] ?? '' ),
			'owner'            => (string) ( $row['owner'] ?? '' ),
			'creationSource'   => (string) ( $row['creationSource'] ?? '' ),
			'updatedAt'        => (string) ( $row['updatedAt'] ?? '' ),
			'queued'           => (bool) ( $row['queued'] ?? ( LaunchStatus::SCHEDULED === $status && '' === (string) ( $row['lastRunStatus'] ?? '' ) ) ),
			'canCancel'        => (bool) ( $row['canCancel'] ?? in_array( 'cancel', $available_actions, true ) ),
			'canReschedule'    => (bool) ( $row['canReschedule'] ?? in_array( 'reschedule', $available_actions, true ) ),
		);

		return PublicHooks::restLaunchRow( $public );
	}

	/**
	 * Converts a dashboard target row to a stable, provider-neutral shape.
	 *
	 * @param array<string,mixed> $row Dashboard target row.
	 * @return array<string,mixed>
	 */
	private function publicTargetRow( array $row ): array {
		return array(
			'targetReference' => (string) ( $row['targetReference'] ?? $row['reference'] ?? '' ),
			'targetId'        => (int) ( $row['id'] ?? $row['targetId'] ?? 0 ),
			'targetType'      => (string) ( $row['targetType'] ?? $row['type'] ?? '' ),
			'subtype'         => (string) ( $row['subtype'] ?? '' ),
			'name'            => (string) ( $row['name'] ?? '' ),
			'providerId'      => (string) ( $row['providerId'] ?? '' ),
			'providerLabel'   => (string) ( $row['providerLabel'] ?? $row['providerId'] ?? '' ),
			'typeLabel'       => (string) ( $row['typeLabel'] ?? $row['targetType'] ?? '' ),
			'canEdit'         => (bool) ( $row['canEdit'] ?? false ),
			'upcomingCount'   => (int) ( $row['upcomingCount'] ?? 0 ),
			'failedCount'     => (int) ( $row['failedCount'] ?? 0 ),
			'nextChange'      => (string) ( $row['nextChange'] ?? '' ),
		);
	}

	/**
	 * Checks a mutating capability without revealing inaccessible launches.
	 */
	private function canAccessLaunch( string $capability, ?\WP_REST_Request $request ): bool {
		if ( ! current_user_can( $capability ) ) {
			return false;
		}

		$uuid = null === $request ? '' : (string) $request['uuid'];

		return '' !== $uuid && null !== $this->dashboard->launchByUuid( $uuid );
	}

	/**
	 * Converts local date and time input to a UTC timestamp.
	 *
	 * @param string        $date Date in Y-m-d format.
	 * @param string        $time Time in H:i format.
	 * @param NamedTimezone $timezone Launch timezone.
	 */
	private function scheduledTimestamp( string $date, string $time, NamedTimezone $timezone ): UtcTimestamp {
		$timezone_name = $timezone->value();

		if ( ! NamedTimezone::isValid( $timezone_name ) ) {
			$timezone_name = 'UTC';
		}

		$local  = \DateTimeImmutable::createFromFormat(
			'!Y-m-d H:i',
			$date . ' ' . $time,
			new \DateTimeZone( $timezone_name )
		);
		$errors = \DateTimeImmutable::getLastErrors();

		if (
			false === $local
			|| ( is_array( $errors ) && ( $errors['warning_count'] > 0 || $errors['error_count'] > 0 ) )
		) {
			throw new \InvalidArgumentException( 'Choose a valid go-live date and time.' );
		}

		$scheduled_at = new UtcTimestamp( $local->setTimezone( new \DateTimeZone( 'UTC' ) ) );

		if ( $scheduled_at->native() <= UtcTimestamp::now()->native() ) {
			throw new \InvalidArgumentException( 'Choose a go-live time in the future.' );
		}

		return $scheduled_at;
	}

	/**
	 * Returns a launch not-found REST error.
	 */
	private function notFound(): \WP_Error {
		return $this->error( 'scheduled_changes_launch_not_found', __( 'Launch was not found.', 'jjs-post-scheduler' ), 404 );
	}

	/**
	 * Creates a REST error with a response status.
	 *
	 * @param string $code Error code.
	 * @param string $message Error message.
	 * @param int    $status HTTP status.
	 */
	private function error( string $code, string $message, int $status ): \WP_Error {
		return new \WP_Error(
			$code,
			$message,
			array( 'status' => $status )
		);
	}

	/**
	 * Creates a REST error without exposing raw exception details.
	 */
	private function exceptionError( string $code, string $message, int $status, \Throwable $exception ): \WP_Error {
		return $this->error(
			$code,
			PublicErrorMessages::message( $code, $message, $exception, array( 'surface' => 'rest.launches' ) ),
			$status
		);
	}

	/**
	 * Maps stable lifecycle failure codes to existing REST errors.
	 */
	private function lifecycleError( LaunchLifecycleException $exception, string $fallback_code, string $fallback_message, int $fallback_status ): \WP_Error {
		if ( LaunchLifecycleService::FAILURE_SCHEDULER_UNAVAILABLE === $exception->reasonCode() ) {
			return $this->exceptionError( 'scheduled_changes_scheduler_unavailable', __( 'Scheduler service is unavailable.', 'jjs-post-scheduler' ), $exception->status(), $exception );
		}

		if ( LaunchLifecycleService::FAILURE_ROLLBACK_UNAVAILABLE === $exception->reasonCode() ) {
			return $this->exceptionError( 'scheduled_changes_rollback_unavailable', __( 'Rollback service is unavailable.', 'jjs-post-scheduler' ), $exception->status(), $exception );
		}

		if ( LaunchLifecycleService::FAILURE_ROLLBACK_RUN_MISSING === $exception->reasonCode() ) {
			return $this->exceptionError( 'scheduled_changes_rollback_run_not_found', __( 'Launch has no successful run to roll back.', 'jjs-post-scheduler' ), $exception->status(), $exception );
		}

		if ( LaunchLifecycleService::FAILURE_ROLLBACK_POLICY === $exception->reasonCode() ) {
			return $this->exceptionError( 'scheduled_changes_rollback_policy_invalid', __( 'Rollback policy is invalid.', 'jjs-post-scheduler' ), $exception->status(), $exception );
		}

		unset( $fallback_status );

		return $this->exceptionError( $fallback_code, $fallback_message, $exception->status(), $exception );
	}
}
