<?php
/**
 * Launch lifecycle application service.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Application;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Application exceptions are escaped or mapped at delivery boundaries.

use ScheduledChanges\Domain\Launch\Launch;
use ScheduledChanges\Domain\Launch\LaunchVersion;
use ScheduledChanges\Domain\Shared\NamedTimezone;
use ScheduledChanges\Domain\Shared\UtcTimestamp;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Provider\ProviderManager;
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\EnumValue;
use ScheduledChanges\Validation\ValidationModule;

/**
 * Coordinates launch lifecycle mutations for REST, admin, and WP-CLI delivery.
 */
final class LaunchLifecycleService {
	public const CODE_VALIDATED         = 'launch.validated';
	public const CODE_DRY_RUN           = 'launch.dry_run';
	public const CODE_SCHEDULED         = 'launch.scheduled';
	public const CODE_RUN_QUEUED        = 'launch.run_queued';
	public const CODE_RETRY_QUEUED      = 'launch.retry_queued';
	public const CODE_CANCELLED         = 'launch.cancelled';
	public const CODE_ROLLBACK_COMPLETE = 'launch.rollback_complete';

	public const FAILURE_LAUNCH_NOT_FOUND      = 'launch.not_found';
	public const FAILURE_VERSION_MISSING       = 'launch.version_missing';
	public const FAILURE_PROVIDER_UNAVAILABLE  = 'provider_manager.unavailable';
	public const FAILURE_SCHEDULER_UNAVAILABLE = 'scheduler.unavailable';
	public const FAILURE_ROLLBACK_UNAVAILABLE  = 'rollback.unavailable';
	public const FAILURE_ROLLBACK_RUN_MISSING  = 'rollback.run_missing';
	public const FAILURE_ROLLBACK_RUN_MISMATCH = 'rollback.run_mismatch';
	public const FAILURE_ROLLBACK_POLICY       = 'rollback.policy_invalid';

	private LaunchRepository $launches;
	private LaunchVersionRepository $versions;
	private ?LaunchMutationScheduler $scheduler;
	private ?ProviderManager $providers;
	private ?RunRepository $runs;
	private ?LaunchRollbackService $rollback;

	/**
	 * Creates the lifecycle application service.
	 */
	public function __construct(
		LaunchRepository $launches,
		LaunchVersionRepository $versions,
		?LaunchMutationScheduler $scheduler,
		?ProviderManager $providers = null,
		?RunRepository $runs = null,
		?LaunchRollbackService $rollback = null
	) {
		$this->launches  = $launches;
		$this->versions  = $versions;
		$this->scheduler = $scheduler;
		$this->providers = $providers;
		$this->runs      = $runs;
		$this->rollback  = $rollback;
	}

	/**
	 * Returns the timezone for a launch resolved through the shared lifecycle lookup.
	 */
	public function launchTimezone( Uuid $uuid ): NamedTimezone {
		return $this->launchByUuid( $uuid )->timezone();
	}

	/**
	 * Validates the current immutable version.
	 */
	public function validate( Uuid $uuid, int $actor_id ): LaunchLifecycleResult {
		$version = $this->versionForLaunch( $this->launchByUuid( $uuid ) );
		$manager = $this->providerManager();
		$report  = ( new ValidationModule() )->createServices(
			$manager->providers(),
			$manager->fields()
		)->validator()->validate( $version->plan()->graph(), $version->plan()->requiredProviders(), $actor_id );

		return LaunchLifecycleResult::success(
			self::CODE_VALIDATED,
			array(
				'version_id' => $version->id(),
				'report'     => $report->toArray(),
			)
		);
	}

	/**
	 * Builds the dry-run projection for the current immutable version.
	 */
	public function dryRun( Uuid $uuid, int $actor_id ): LaunchLifecycleResult {
		$version = $this->versionForLaunch( $this->launchByUuid( $uuid ) );
		$manager = $this->providerManager();
		$result  = ( new ValidationModule() )->createServices(
			$manager->providers(),
			$manager->fields()
		)->dryRun()->run( $version->plan()->graph(), $version->plan()->requiredProviders(), $actor_id );

		return LaunchLifecycleResult::success(
			self::CODE_DRY_RUN,
			array(
				'version_id' => $version->id(),
				'report'     => $result->toArray(),
			)
		);
	}

	/**
	 * Schedules the current immutable version for a launch UUID.
	 */
	public function schedule( Uuid $uuid, UtcTimestamp $scheduled_at ): LaunchLifecycleResult {
		$launch  = $this->launchByUuid( $uuid );
		$version = $this->versionForLaunch( $launch );

		return $this->scheduleVersionResult( $launch, $version, $scheduled_at );
	}

	/**
	 * Schedules a known immutable version, used after recording finalization.
	 */
	public function scheduleVersion( int $version_id, UtcTimestamp $scheduled_at ): LaunchLifecycleResult {
		$version = $this->versionById( $version_id );
		$launch  = $this->launchById( $version->launchId() );

		return $this->scheduleVersionResult( $launch, $version, $scheduled_at );
	}

	/**
	 * Queues the current immutable version for immediate execution.
	 */
	public function runNow( Uuid $uuid, ?UtcTimestamp $occurrence_at = null ): LaunchLifecycleResult {
		$launch       = $this->launchByUuid( $uuid );
		$version      = $this->versionForLaunch( $launch );
		$scheduled_at = $occurrence_at ?? UtcTimestamp::now();
		$action_id    = $this->scheduler()->runNow( (int) $version->id(), $scheduled_at );
		$updated      = $this->launches->findByUuid( $uuid );

		return LaunchLifecycleResult::success(
			self::CODE_RUN_QUEUED,
			array(
				'launch_uuid'      => $uuid->value(),
				'version_id'       => $version->id(),
				'action_id'        => $action_id,
				'scheduled_at_utc' => $scheduled_at->toStorage(),
				'status'           => null === $updated ? $launch->status() : $updated->status(),
			)
		);
	}

	/**
	 * Queues a manual retry for one terminal run.
	 */
	public function retry( Uuid $uuid, int $source_run_id, int $actor_id ): LaunchLifecycleResult {
		$launch    = $this->launchByUuid( $uuid );
		$launch_id = $this->launchId( $launch );
		$result    = $this->scheduler()->manualRetry( $source_run_id, $actor_id, $launch_id );
		$latest    = $this->launches->findByUuid( $uuid );

		return LaunchLifecycleResult::success(
			self::CODE_RETRY_QUEUED,
			array(
				'launch_uuid'       => $uuid->value(),
				'source_run_id'     => $result['source_run_id'],
				'version_id'        => $result['launch_version_id'],
				'action_id'         => $result['action_scheduler_id'],
				'attempt_number'    => $result['attempt_number'],
				'occurrence_at_utc' => $result['occurrence_at_utc'],
				'status'            => null === $latest ? $launch->status() : $latest->status(),
			)
		);
	}

	/**
	 * Cancels the current immutable version for one launch.
	 */
	public function cancel( Uuid $uuid, int $actor_id ): LaunchLifecycleResult {
		$launch    = $this->launchByUuid( $uuid );
		$version   = $this->versionForLaunch( $launch );
		$cancelled = $this->scheduler()->cancel( (int) $version->id(), $actor_id );

		return LaunchLifecycleResult::success(
			self::CODE_CANCELLED,
			array(
				'launch_uuid'            => $uuid->value(),
				'version_id'             => $version->id(),
				'cancelled_action_count' => $cancelled,
			)
		);
	}

	/**
	 * Rolls back the latest successful run for one launch.
	 */
	public function rollbackLatest( Uuid $uuid, string $conflict_policy, string $created_targets, int $actor_id ): LaunchLifecycleResult {
		$launch     = $this->launchByUuid( $uuid );
		$launch_id  = $this->launchId( $launch );
		$version    = $this->versionForLaunch( $launch );
		$version_id = $this->versionId( $version );
		$run        = $this->runs()->findLatestSuccessfulForVersion( $version_id );

		if ( null === $run || null === $run->id() ) {
			throw new LaunchLifecycleException( self::FAILURE_ROLLBACK_RUN_MISSING, 'Launch has no successful run to roll back.', 409 );
		}

		if ( $run->launchId() !== $launch_id || $run->launchVersionId() !== $version_id ) {
			throw new LaunchLifecycleException( self::FAILURE_ROLLBACK_RUN_MISMATCH, 'Rollback source run does not belong to this launch.', 409 );
		}

		$conflict = EnumValue::tryFrom( RollbackConflictPolicy::class, $conflict_policy );
		$created  = EnumValue::tryFrom( CreatedTargetRollbackPolicy::class, $created_targets );

		if ( null === $conflict || null === $created ) {
			throw new LaunchLifecycleException( self::FAILURE_ROLLBACK_POLICY, 'Rollback policy is invalid.', 400 );
		}

		$outcome = $this->rollback()->rollback( $run->id(), $conflict, $created, $actor_id );

		return LaunchLifecycleResult::success(
			self::CODE_ROLLBACK_COMPLETE,
			array(
				'launch_uuid'       => $uuid->value(),
				'source_run_id'     => $run->id(),
				'rollback_run_id'   => $outcome->runId(),
				'status'            => $outcome->status(),
				'conflict_policy'   => $conflict,
				'created_targets'   => $created,
				'operation_summary' => $outcome->summary(),
			)
		);
	}

	/**
	 * Returns whether rollback dependencies are available.
	 */
	public function rollbackAvailable(): bool {
		return null !== $this->rollback && null !== $this->runs;
	}

	/**
	 * Returns whether scheduling dependencies are available.
	 */
	public function schedulerAvailable(): bool {
		return null !== $this->scheduler;
	}

	/**
	 * Schedules a concrete immutable version and returns a shared result.
	 */
	private function scheduleVersionResult( Launch $launch, LaunchVersion $version, UtcTimestamp $scheduled_at ): LaunchLifecycleResult {
		$version_id = $this->versionId( $version );
		$action_id  = $this->scheduler()->schedule( $version_id, $scheduled_at );

		return LaunchLifecycleResult::success(
			self::CODE_SCHEDULED,
			array(
				'launch_uuid'      => $launch->uuid()->value(),
				'version_id'       => $version_id,
				'action_id'        => $action_id,
				'scheduled_at_utc' => $scheduled_at->toStorage(),
			)
		);
	}

	/**
	 * Returns a launch by UUID or a stable application failure.
	 */
	private function launchByUuid( Uuid $uuid ): Launch {
		$launch = $this->launches->findByUuid( $uuid );

		if ( null === $launch ) {
			throw new LaunchLifecycleException( self::FAILURE_LAUNCH_NOT_FOUND, 'Launch was not found.', 404 );
		}

		return $launch;
	}

	/**
	 * Returns a launch by ID or a stable application failure.
	 */
	private function launchById( int $id ): Launch {
		$launch = $this->launches->findById( $id );

		if ( null === $launch || null === $launch->id() ) {
			throw new LaunchLifecycleException( self::FAILURE_LAUNCH_NOT_FOUND, 'Launch was not found for the immutable version.', 404 );
		}

		return $launch;
	}

	/**
	 * Resolves the current or latest immutable version for a launch.
	 */
	private function versionForLaunch( Launch $launch ): LaunchVersion {
		$current_version_id = $launch->currentVersionId();
		$launch_id          = $launch->id();

		if ( null !== $current_version_id ) {
			return $this->versionById( $current_version_id );
		}

		if ( null === $launch_id ) {
			throw new LaunchLifecycleException( self::FAILURE_VERSION_MISSING, 'Launch has no immutable version.', 409 );
		}

		$version = $this->versions->findLatestForLaunch( $launch_id );

		if ( null === $version || null === $version->id() ) {
			throw new LaunchLifecycleException( self::FAILURE_VERSION_MISSING, 'Launch has no immutable version.', 409 );
		}

		return $version;
	}

	/**
	 * Resolves one immutable launch version by ID.
	 */
	private function versionById( int $version_id ): LaunchVersion {
		$version = $this->versions->findById( $version_id );

		if ( null === $version || null === $version->id() ) {
			throw new LaunchLifecycleException( self::FAILURE_VERSION_MISSING, 'Immutable launch version was not found.', 409 );
		}

		return $version;
	}

	/**
	 * Returns a persisted launch ID or a stable lifecycle failure.
	 */
	private function launchId( Launch $launch ): int {
		$launch_id = $launch->id();

		if ( null === $launch_id ) {
			throw new LaunchLifecycleException( self::FAILURE_LAUNCH_NOT_FOUND, 'Launch is not persisted.', 404 );
		}

		return $launch_id;
	}

	/**
	 * Returns a persisted version ID or a stable lifecycle failure.
	 */
	private function versionId( LaunchVersion $version ): int {
		$version_id = $version->id();

		if ( null === $version_id ) {
			throw new LaunchLifecycleException( self::FAILURE_VERSION_MISSING, 'Immutable launch version is not persisted.', 409 );
		}

		return $version_id;
	}

	/**
	 * Returns the selected scheduler.
	 */
	private function scheduler(): LaunchMutationScheduler {
		if ( null === $this->scheduler ) {
			throw new LaunchLifecycleException( self::FAILURE_SCHEDULER_UNAVAILABLE, 'Scheduler service is unavailable.', 503 );
		}

		return $this->scheduler;
	}

	/**
	 * Returns the selected rollback service.
	 */
	private function rollback(): LaunchRollbackService {
		if ( null === $this->rollback ) {
			throw new LaunchLifecycleException( self::FAILURE_ROLLBACK_UNAVAILABLE, 'Rollback service is unavailable.', 503 );
		}

		return $this->rollback;
	}

	/**
	 * Returns the run repository needed by rollback.
	 */
	private function runs(): RunRepository {
		if ( null === $this->runs ) {
			throw new LaunchLifecycleException( self::FAILURE_ROLLBACK_UNAVAILABLE, 'Rollback service is unavailable.', 503 );
		}

		return $this->runs;
	}

	/**
	 * Returns the provider manager needed by validation and dry-run.
	 */
	private function providerManager(): ProviderManager {
		if ( null === $this->providers || ! $this->providers->loaded() ) {
			throw new LaunchLifecycleException( self::FAILURE_PROVIDER_UNAVAILABLE, 'Provider manager is unavailable or not loaded.', 503 );
		}

		return $this->providers;
	}
}
