<?php
/**
 * JJ\'s Post Scheduler WP-CLI commands.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\WpCli;

use ScheduledChanges\Application\LaunchLifecycleService;
use ScheduledChanges\Capabilities\CapabilityManager;
use ScheduledChanges\Capabilities\WordPressPermissionService;
use ScheduledChanges\Domain\Launch\Launch;
use ScheduledChanges\Domain\Launch\LaunchVersion;
use ScheduledChanges\Domain\Recording\RecordingSession;
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\Provider\ProviderAuthorization;
use ScheduledChanges\Provider\ProviderAuthorizationService;
use ScheduledChanges\Recording\RecordingService;
use ScheduledChanges\Repository\AdminDashboardRepository;
use ScheduledChanges\Repository\LaunchRepository;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Repository\RecordingSessionRepository;
use ScheduledChanges\Repository\RunRepository;
use ScheduledChanges\Rollback\LaunchRollbackService;
use ScheduledChanges\Scheduler\LaunchMutationScheduler;
use ScheduledChanges\Scheduler\QueueMonitor;

/**
 * Root `wp scheduled-changes` command.
 */
final class JpsCommand extends ModuleCommand {
	private RecordingSessionRepository $recordings;
	private ?RecordingService $recording_service;
	private AdminDashboardRepository $dashboard;
	private LaunchRepository $launches;
	private LaunchVersionRepository $versions;
	private LaunchLifecycleService $lifecycle;
	private WordPressPermissionService $permissions;
	private ProviderAuthorization $authorization;
	private InspectionCommands $inspection;
	/**
	 * Creates the CLI command handler.
	 *
	 * @param ProviderManager|null         $providers Provider manager.
	 * @param QueueMonitor|null            $queue Queue monitor.
	 * @param RecordingSessionRepository   $recordings Recording repository.
	 * @param RecordingService|null        $recording_service Recording service.
	 * @param AdminDashboardRepository     $dashboard Dashboard read model.
	 * @param LaunchRepository             $launches Launch repository.
	 * @param LaunchVersionRepository      $versions Launch version repository.
	 * @param RunRepository                $runs Run repository.
	 * @param LaunchMutationScheduler|null $scheduler Scheduler service.
	 * @param LaunchRollbackService|null   $rollback Rollback service.
	 */
	public function __construct( ?ProviderManager $providers, ?QueueMonitor $queue, RecordingSessionRepository $recordings, ?RecordingService $recording_service, AdminDashboardRepository $dashboard, LaunchRepository $launches, LaunchVersionRepository $versions, RunRepository $runs, ?LaunchMutationScheduler $scheduler, ?LaunchRollbackService $rollback, ?WordPressPermissionService $permissions = null, ?ProviderAuthorization $authorization = null, ?LaunchLifecycleService $lifecycle = null ) {
		$this->recordings        = $recordings;
		$this->recording_service = $recording_service;
		$this->dashboard         = $dashboard;
		$this->launches          = $launches;
		$this->versions          = $versions;
		$this->lifecycle         = $lifecycle ?? new LaunchLifecycleService( $launches, $versions, $scheduler, $providers, $runs, $rollback );
		$this->permissions       = $permissions ?? new WordPressPermissionService();
		$this->authorization     = $authorization ?? new ProviderAuthorizationService();
		$this->inspection        = new InspectionCommands( $providers, $queue, $recordings, $dashboard, $launches, $versions, $this->permissions, $this->authorization );
	}

	/**
	 * Manages launches.
	 *
	 * ## OPTIONS
	 *
	 * <subcommand>
	 * : One of list, show, validate, schedule, run, retry, cancel, or rollback.
	 *
	 * [<uuid>]
	 * : Launch UUID for item subcommands.
	 *
	 * [--format=<format>]
	 * : Output format. Supports table, json, and csv.
	 *
	 * [--limit=<limit>]
	 * : Maximum number of launch rows for list output.
	 *
	 * [--date=<date>]
	 * : Local schedule date in Y-m-d format.
	 *
	 * [--time=<time>]
	 * : Local schedule time in H:i format.
	 *
	 * [--yes]
	 * : Required for mutating commands.
	 *
	 * [--dry-run]
	 * : Show intended launch writes without executing them.
	 *
	 * [--run-id=<id>]
	 * : Source run ID for retry.
	 *
	 * [--conflict=<policy>]
	 * : Rollback conflict policy. Supports preserve, force, merge, or abort.
	 *
	 * [--created-targets=<policy>]
	 * : Rollback policy for created targets. Supports disable, keep, or remove.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function launch( array $args, array $assoc_args ): void {
		$subcommand = (string) ( $args[0] ?? '' );
		$capability = in_array( $subcommand, array( 'list', 'show', 'validate' ), true )
			? CapabilityManager::VIEW_SCHEDULES
			: ( in_array( $subcommand, array( 'run', 'retry' ), true )
				? CapabilityManager::EXECUTE_SCHEDULES
				: ( 'rollback' === $subcommand ? CapabilityManager::ROLLBACK_SCHEDULES : CapabilityManager::SCHEDULE_CHANGES ) );
		$this->requireCapability( $capability );

		switch ( $subcommand ) {
			case 'list':
				$this->launchList( $assoc_args );
				break;
			case 'show':
				$this->launchShow( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'validate':
				$this->launchValidate( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'schedule':
				$this->launchSchedule( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'run':
				$this->launchRun( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'retry':
				$this->launchRetry( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'cancel':
				$this->launchCancel( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'rollback':
				$this->launchRollback( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			default:
				$this->usage( 'Unknown launch subcommand. Use list, show, validate, schedule, run, retry, cancel, or rollback.' );
				break;
		}
	}

	/**
	 * Manages recording sessions.
	 *
	 * ## OPTIONS
	 *
	 * <subcommand>
	 * : Use list, start, capture, finalize, cancel, or cleanup.
	 *
	 * [<target-reference-or-uuid>]
	 * : Canonical target reference for start, or recording UUID for item commands.
	 *
	 * [--name=<name>]
	 * : Launch name used when finalizing.
	 *
	 * [--date=<date>]
	 * : Optional local schedule date in Y-m-d format for finalize.
	 *
	 * [--time=<time>]
	 * : Optional local schedule time in H:i format for finalize.
	 *
	 * [--acknowledge-unsupported]
	 * : Acknowledge selected unsupported metadata before finalizing.
	 *
	 * [--format=<format>]
	 * : Output format. Supports table, json, and csv.
	 *
	 * [--limit=<limit>]
	 * : Maximum number of recording rows.
	 *
	 * [--yes]
	 * : Required for cleanup.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function recording( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::CREATE_SCHEDULES );
		$subcommand = (string) ( $args[0] ?? '' );

		switch ( $subcommand ) {
			case 'list':
				$this->formatRows(
					$this->dashboard->recordings( $this->limit( $assoc_args ) ),
					array( 'uuid', 'sourceReference', 'sourceProviderId', 'sourceType', 'sourceTitle', 'workspaceReference', 'status', 'cleanupStatus', 'expiresAt' ),
					$assoc_args
				);
				break;
			case 'start':
				$this->recordingStart( (string) ( $args[1] ?? '' ), $assoc_args );
				break;
			case 'capture':
				$this->recordingCapture( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'finalize':
				$this->recordingFinalize( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'cancel':
				$this->recordingCancel( $this->requiredUuid( $args, 1 ), $assoc_args );
				break;
			case 'cleanup':
				$this->recordingCleanup( $assoc_args );
				break;
			default:
				$this->usage( 'Unknown recording subcommand. Use list, start, capture, finalize, cancel, or cleanup.' );
		}
	}

	/**
	 * Starts or resumes a provider-owned recording workspace.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function recordingStart( string $target_reference, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Starting a recording creates an isolated temporary workspace.' );

		if ( null === $this->recording_service || '' === $target_reference ) {
			\WP_CLI::error( 'Recording service and a canonical target reference are required.' );
		}

		$context = PublicHooks::cliFieldScanContext( null, $target_reference, $assoc_args );

		if ( null === $context || null === $context->targetId() ) {
			\WP_CLI::error( 'No provider could resolve the requested recording target.' );
		}

		if ( ! current_user_can( 'edit_post', $context->targetId() ) ) {
			\WP_CLI::error( 'The current actor cannot edit this recording target.' );
		}

		$session = $this->recording_service->start( $context->targetId(), $this->actorId() );

		$this->formatAssoc( $this->recordingSessionRow( $session ), $assoc_args );
	}

	/**
	 * Captures the current recording workspace diff.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function recordingCapture( Uuid $uuid, array $assoc_args ): void {
		if ( null === $this->recording_service ) {
			\WP_CLI::error( 'Recording service is unavailable.' );
		}

		$this->recordingSessionOrFail( $uuid );
		$this->formatAssoc( $this->recording_service->capture( $uuid ), $assoc_args );
	}

	/**
	 * Finalizes a recording and optionally schedules its immutable launch.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function recordingFinalize( Uuid $uuid, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Finalizing creates an immutable launch and may schedule it.' );

		if ( null === $this->recording_service ) {
			\WP_CLI::error( 'Recording service is unavailable.' );
		}

		$this->recordingSessionOrFail( $uuid );
		$has_date     = isset( $assoc_args['date'] ) && '' !== (string) $assoc_args['date'];
		$has_time     = isset( $assoc_args['time'] ) && '' !== (string) $assoc_args['time'];
		$timezone     = new NamedTimezone( $this->siteTimezoneName() );
		$scheduled_at = null;

		if ( $has_date || $has_time ) {
			$this->requireCapability( CapabilityManager::SCHEDULE_CHANGES );

			if ( ! $has_date || ! $has_time || ! $this->lifecycle->schedulerAvailable() ) {
				\WP_CLI::error( 'Scheduling finalization requires both --date and --time plus an available scheduler.' );
			}

			$scheduled_at = $this->scheduledAt( $assoc_args, $timezone );
		}

		$result = $this->recording_service->finalize(
			$uuid,
			array(),
			isset( $assoc_args['name'] ) ? (string) $assoc_args['name'] : null,
			$timezone,
			! isset( $assoc_args['retain-workspace'] ),
			isset( $assoc_args['acknowledge-unsupported'] )
		);
		$output = array(
			'launchUuid'          => $result->launchUuid()->value(),
			'launchId'            => $result->launchId(),
			'versionId'           => $result->versionId(),
			'selectedChangeCount' => $result->selectedChangeCount(),
		);
		if ( $scheduled_at instanceof UtcTimestamp ) {
			$lifecycle_result         = $this->lifecycle->scheduleVersion( $result->versionId(), $scheduled_at );
			$lifecycle_data           = $lifecycle_result->data();
			$output['actionId']       = $lifecycle_data['action_id'];
			$output['scheduledAtUtc'] = $lifecycle_data['scheduled_at_utc'];
		}

		$this->formatAssoc( $output, $assoc_args );
	}

	/**
	 * Cancels an active recording and removes its workspace.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function recordingCancel( Uuid $uuid, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Cancelling removes the isolated recording workspace.' );

		if ( null === $this->recording_service ) {
			\WP_CLI::error( 'Recording service is unavailable.' );
		}

		$this->recordingSessionOrFail( $uuid );
		$this->recording_service->abandon( $uuid, true );
		$this->formatAssoc(
			array(
				'uuid'   => $uuid->value(),
				'status' => 'abandoned',
			),
			$assoc_args
		);
	}

	/**
	 * Cleans expired recording sessions.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function recordingCleanup( array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Recording cleanup deletes expired temporary workspaces and releases locks.' );

		if ( null === $this->recording_service ) {
			\WP_CLI::error( 'Recording workspace cleanup is unavailable because no recording provider is active.' );
		}

		$this->formatAssoc(
			$this->recording_service->cleanupStale( $this->limit( $assoc_args ) ),
			$assoc_args
		);
	}

	/**
	 * Serializes a recording without provider-specific aliases.
	 *
	 * @return array<string,mixed>
	 */
	private function recordingSessionRow( RecordingSession $session ): array {
		return array(
			'uuid'                => $session->uuid()->value(),
			'sourceId'            => $session->sourceId(),
			'sourceProviderId'    => $session->sourceProviderId(),
			'sourceType'          => $session->sourceType(),
			'sourceReference'     => $session->sourceReference(),
			'workspaceId'         => $session->workspaceId(),
			'workspaceProviderId' => $session->workspaceProviderId(),
			'workspaceType'       => $session->workspaceType(),
			'workspaceReference'  => $session->workspaceReference(),
			'status'              => $session->status(),
			'cleanupStatus'       => $session->cleanupStatus(),
			'expiresAt'           => $session->expiresAt()->toStorage(),
		);
	}

	/**
	 * Returns a recording only when its source is visible to the current actor.
	 */
	private function recordingSessionOrFail( Uuid $uuid ): RecordingSession {
		$session = $this->recordings->findByUuid( $uuid );

		if ( null === $session || ! current_user_can( 'edit_post', $session->sourceId() ) ) {
			\WP_CLI::error( 'Recording was not found or is not available to the current actor.' );
		}

		return $session;
	}

	/**
	 * Lists loaded providers.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function providers( array $args, array $assoc_args ): void {
		$this->inspection->providers( $args, $assoc_args );
	}

	/**
	 * Scans fields for a provider-owned target.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function fields( array $args, array $assoc_args ): void {
		$this->inspection->fields( $args, $assoc_args );
	}

	/**
	 * Shows queue diagnostics.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function queue( array $args, array $assoc_args ): void {
		$this->inspection->queue( $args, $assoc_args );
	}

	/**
	 * Lists or shows permission-filtered execution logs.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function logs( array $args, array $assoc_args ): void {
		$this->inspection->logs( $args, $assoc_args );
	}

	/**
	 * Lists current warning and blocking validation conflicts.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function conflicts( array $args, array $assoc_args ): void {
		$this->inspection->conflicts( $args, $assoc_args );
	}

	/**
	 * Shows migration status or runs pending migrations.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function migrations( array $args, array $assoc_args ): void {
		$this->inspection->migrations( $args, $assoc_args );
	}

	/**
	 * Runs broad scheduler diagnostics.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function doctor( array $args, array $assoc_args ): void {
		$this->inspection->doctor( $args, $assoc_args );
	}

	/**
	 * Lists launches.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchList( array $assoc_args ): void {
		$rows = array_values(
			array_filter(
				$this->dashboard->launches( $this->limit( $assoc_args ) ),
				fn ( array $row ): bool => $this->canInspectLaunchRow( $row )
			)
		);

		$this->formatRows(
			$rows,
			array( 'uuid', 'name', 'status', 'health', 'targetCount', 'startAt', 'timezone', 'validationState', 'lastRunStatus' ),
			$assoc_args
		);
	}

	/**
	 * Shows one launch.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchShow( Uuid $uuid, array $assoc_args ): void {
		$this->versionForLaunch( $this->launchByUuid( $uuid ) );
		$row = $this->dashboard->launchByUuid( $uuid->value() );

		if ( null === $row ) {
			\WP_CLI::error( 'Launch was not found or is not available to the current actor.' );
		}

		$this->formatAssoc( $row, $assoc_args );
	}

	/**
	 * Validates one launch.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchValidate( Uuid $uuid, array $assoc_args ): void {
		$result = $this->lifecycle->validate( $uuid, $this->actorId() );
		$data   = $result->data();

		$this->formatAssoc( $data['report'], $assoc_args );
	}

	/**
	 * Schedules one launch.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchSchedule( Uuid $uuid, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Scheduling mutates launch and queue state.' );

		$scheduled_at = $this->scheduledAt( $assoc_args, $this->lifecycle->launchTimezone( $uuid ) );
		$result       = $this->lifecycle->schedule( $uuid, $scheduled_at );
		$data         = $result->data();

		$this->formatAssoc(
			array(
				'uuid'             => $uuid->value(),
				'version_id'       => $data['version_id'],
				'action_id'        => $data['action_id'],
				'scheduled_at_utc' => $data['scheduled_at_utc'],
			),
			$assoc_args
		);
	}

	/**
	 * Runs a dry-run projection or immediate execution for one launch.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchRun( Uuid $uuid, array $assoc_args ): void {
		if ( isset( $assoc_args['dry-run'] ) ) {
			$result = $this->lifecycle->dryRun( $uuid, $this->actorId() );
			$data   = $result->data();

			$this->formatAssoc( $data['report'], $assoc_args );
			return;
		}

		$this->requireYes( $assoc_args, 'Running a launch mutates target data.' );

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

		$this->formatAssoc(
			array(
				'uuid'             => $uuid->value(),
				'version_id'       => $data['version_id'],
				'action_id'        => $data['action_id'],
				'scheduled_at_utc' => $data['scheduled_at_utc'],
				'status'           => $data['status'],
			),
			$assoc_args
		);
	}

	/**
	 * Cancels one launch.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchCancel( Uuid $uuid, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Cancelling mutates launch and queue state.' );

		$result = $this->lifecycle->cancel( $uuid, $this->actorId() );
		$data   = $result->data();

		$this->formatAssoc(
			array(
				'uuid'                   => $uuid->value(),
				'version_id'             => $data['version_id'],
				'cancelled_action_count' => $data['cancelled_action_count'],
			),
			$assoc_args
		);
	}

	/**
	 * Retries a terminal source run as a new queued attempt.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchRetry( Uuid $uuid, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Retrying queues a new launch execution attempt.' );

		$source_run_id = absint( $assoc_args['run-id'] ?? 0 );
		if ( $source_run_id <= 0 ) {
			\WP_CLI::error( 'A positive --run-id value is required for retry.' );
		}

		$result = $this->lifecycle->retry( $uuid, $source_run_id, $this->actorId() );
		$data   = $result->data();

		$this->formatAssoc(
			array(
				'uuid'              => $uuid->value(),
				'source_run_id'     => $data['source_run_id'],
				'version_id'        => $data['version_id'],
				'action_id'         => $data['action_id'],
				'attempt_number'    => $data['attempt_number'],
				'occurrence_at_utc' => $data['occurrence_at_utc'],
				'status'            => $data['status'],
			),
			$assoc_args
		);
	}

	/**
	 * Rolls back the latest successful run for one launch.
	 *
	 * @param Uuid                $uuid Launch UUID.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function launchRollback( Uuid $uuid, array $assoc_args ): void {
		$this->requireYes( $assoc_args, 'Rolling back mutates target data.' );

		$result = $this->lifecycle->rollbackLatest(
			$uuid,
			(string) ( $assoc_args['conflict'] ?? 'preserve' ),
			(string) ( $assoc_args['created-targets'] ?? 'disable' ),
			$this->actorId()
		);
		$data   = $result->data();

		$this->formatAssoc(
			array(
				'uuid'                  => $uuid->value(),
				'source_run_id'         => $data['source_run_id'],
				'rollback_run_id'       => $data['rollback_run_id'],
				'status'                => $data['status'],
				'conflict_policy'       => $data['conflict_policy'],
				'created_targets'       => $data['created_targets'],
				'successful_operations' => (int) ( $data['operation_summary']['successful_operations'] ?? 0 ),
				'skipped_operations'    => (int) ( $data['operation_summary']['skipped_operations'] ?? 0 ),
				'failed_operations'     => (int) ( $data['operation_summary']['failed_operations'] ?? 0 ),
			),
			$assoc_args
		);
	}

	/**
	 * Finds a launch by UUID.
	 *
	 * @param Uuid $uuid Launch UUID.
	 */
	private function launchByUuid( Uuid $uuid ): Launch {
		$launch = $this->launches->findByUuid( $uuid );

		if ( null === $launch ) {
			\WP_CLI::error( 'Launch was not found.' );
		}

		return $launch;
	}

	/**
	 * Finds the current or latest launch version.
	 *
	 * @param Launch $launch Launch aggregate.
	 */
	private function versionForLaunch( Launch $launch ): LaunchVersion {
		$version = $this->resolveVersionForLaunch( $launch );

		if ( null === $version || null === $version->id() ) {
			\WP_CLI::error( 'Launch has no immutable version.' );
		}

		if ( ! $this->canInspectVersion( $version ) ) {
			\WP_CLI::error( 'Launch was not found or is not available to the current actor.' );
		}

		return $version;
	}

	/**
	 * Checks whether one dashboard launch row is visible to the current actor.
	 *
	 * @param array<string,mixed> $row Launch row.
	 */
	private function canInspectLaunchRow( array $row ): bool {
		$uuid = (string) ( $row['uuid'] ?? '' );

		if ( '' === $uuid ) {
			return false;
		}

		try {
			$launch = $this->launches->findByUuid( new Uuid( $uuid ) );
		} catch ( \Throwable $throwable ) {
			unset( $throwable );

			return false;
		}

		$version = null === $launch ? null : $this->resolveVersionForLaunch( $launch );

		return null !== $version && null !== $version->id() && $this->canInspectVersion( $version );
	}

	/**
	 * Resolves the current or latest immutable version without emitting CLI output.
	 */
	private function resolveVersionForLaunch( Launch $launch ): ?LaunchVersion {
		return null === $launch->currentVersionId()
			? ( null === $launch->id() ? null : $this->versions->findLatestForLaunch( $launch->id() ) )
			: $this->versions->findById( $launch->currentVersionId() );
	}

	/**
	 * Checks every provider-owned target in a version.
	 */
	private function canInspectVersion( LaunchVersion $version ): bool {
		$actor_id = $this->actorId();

		foreach ( $version->plan()->graph()->targets() as $target ) {
			if ( ! $this->authorization->canInspect( $actor_id, $target ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Parses schedule date/time arguments.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 * @param NamedTimezone       $timezone Launch timezone.
	 */
	private function scheduledAt( array $assoc_args, NamedTimezone $timezone ): UtcTimestamp {
		$date = (string) ( $assoc_args['date'] ?? '' );
		$time = (string) ( $assoc_args['time'] ?? '' );

		if ( '' === $date || '' === $time ) {
			\WP_CLI::error( 'Schedule requires --date=Y-m-d and --time=H:i.' );
		}

		$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 ) )
		) {
			\WP_CLI::error( 'Choose a valid schedule date and time.' );
		}

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

		if ( $scheduled_at->native() <= UtcTimestamp::now()->native() ) {
			\WP_CLI::error( 'Choose a schedule time in the future.' );
		}

		return $scheduled_at;
	}

	/**
	 * Returns the site's timezone with a UTC fallback.
	 */
	private function siteTimezoneName(): string {
		$timezone = wp_timezone_string();

		return NamedTimezone::isValid( $timezone )
			? $timezone
			: 'UTC';
	}

	/**
	 * Returns a bounded row limit.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function limit( array $assoc_args ): int {
		return max( 1, min( 100, (int) ( $assoc_args['limit'] ?? 25 ) ) );
	}

	/**
	 * Returns a UUID argument.
	 *
	 * @param string[] $args Positional arguments.
	 * @param int      $offset Argument offset.
	 */
	private function requiredUuid( array $args, int $offset ): Uuid {
		$value = (string) ( $args[ $offset ] ?? '' );

		if ( '' === $value ) {
			\WP_CLI::error( 'Launch UUID is required.' );
		}

		return new Uuid( $value );
	}

	/**
	 * Emits a usage error.
	 *
	 * @param string $message Error message.
	 * @return void
	 */
	private function usage( string $message ) {
		\WP_CLI::error( $message );
	}
}
