<?php
/**
 * Derived launch health projector.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Domain\Launch;

use ScheduledChanges\Domain\Run\RunStatus;

/**
 * Derives shared launch health and actions from durable lifecycle state.
 */
final class LaunchHealthProjector {
	public const ACTION_VALIDATE   = 'validate';
	public const ACTION_SCHEDULE   = 'schedule';
	public const ACTION_RUN_NOW    = 'run_now';
	public const ACTION_CANCEL     = 'cancel';
	public const ACTION_RESCHEDULE = 'reschedule';
	public const ACTION_RETRY      = 'retry';
	public const ACTION_ROLLBACK   = 'rollback';

	/**
	 * Projects one durable launch state row into shared health.
	 *
	 * @param array<string,mixed> $state Durable state row.
	 */
	public static function project( array $state ): LaunchHealthProjection {
		$launch_status             = self::stringValue( $state, 'launch_status', 'status' );
		$version_status            = self::stringValue( $state, 'version_status' );
		$validation_state          = self::stringValue( $state, 'validation_state' );
		$last_run_status           = self::stringValue( $state, 'last_run_status' );
		$version_id                = (int) ( $state['current_version_id'] ?? $state['version_id'] ?? 0 );
		$validation_blocking_count = (int) ( $state['validation_blocking_count'] ?? 0 );
		$diagnostics               = self::diagnostics( $state );

		if ( $version_id <= 0 && ! in_array( $launch_status, array( '', LaunchStatus::DRAFT, LaunchStatus::CANCELLED ), true ) ) {
			return new LaunchHealthProjection(
				LaunchHealth::BLOCKED,
				'version.missing',
				array(),
				array() === $diagnostics
					? self::withDiagnostic( $diagnostics, 'version.missing', 'Launch has no readable immutable version.' )
					: $diagnostics
			);
		}

		if ( array() !== $diagnostics ) {
			return new LaunchHealthProjection( LaunchHealth::LEGACY_CONFLICT, $diagnostics[0]['code'], array(), $diagnostics );
		}

		if ( in_array( $launch_status, array( LaunchStatus::ROLLING_BACK, LaunchStatus::ROLLED_BACK, LaunchStatus::ROLLBACK_FAILED ), true ) ) {
			return self::rollbackProjection( $launch_status );
		}

		if ( in_array( $launch_status, array( LaunchStatus::SUPERSEDED, LaunchStatus::CANCELLED ), true ) ) {
			return new LaunchHealthProjection( $launch_status, 'launch.' . $launch_status );
		}

		if ( self::failedStatus( $launch_status ) || self::failedStatus( $version_status ) || self::failedStatus( $last_run_status ) ) {
			return new LaunchHealthProjection( LaunchHealth::FAILED, 'execution.failed', array( self::ACTION_RETRY ), $diagnostics );
		}

		if ( RunStatus::SUCCESSFUL_WITH_WARNINGS === $last_run_status || LaunchStatus::SUCCESSFUL_WITH_WARNINGS === $launch_status ) {
			return new LaunchHealthProjection( LaunchHealth::WARNING, 'execution.successful_with_warnings', array( self::ACTION_ROLLBACK ), $diagnostics );
		}

		if ( RunStatus::SUCCESSFUL === $last_run_status || LaunchStatus::SUCCESSFUL === $launch_status ) {
			return new LaunchHealthProjection( LaunchHealth::SUCCESSFUL, 'execution.successful', array( self::ACTION_ROLLBACK ), $diagnostics );
		}

		if ( in_array( $launch_status, array( LaunchStatus::RUNNING, LaunchStatus::VALIDATING ), true ) || LaunchVersionStatus::RUNNING === $version_status || RunStatus::RUNNING === $last_run_status ) {
			return new LaunchHealthProjection( LaunchHealth::RUNNING, 'execution.running' );
		}

		if ( $validation_blocking_count > 0 ) {
			return new LaunchHealthProjection(
				LaunchHealth::BLOCKED,
				'validation.blocked',
				array( self::ACTION_VALIDATE ),
				self::validationDiagnostics( $state )
			);
		}

		if ( LaunchStatus::SCHEDULED === $launch_status || LaunchVersionStatus::SCHEDULED === $version_status ) {
			return new LaunchHealthProjection(
				LaunchHealth::SCHEDULED,
				'queue.scheduled',
				array( self::ACTION_CANCEL, self::ACTION_RESCHEDULE, self::ACTION_RUN_NOW )
			);
		}

		if ( 'valid' !== $validation_state ) {
			return new LaunchHealthProjection( LaunchHealth::NEEDS_VALIDATION, 'validation.required', array( self::ACTION_VALIDATE ) );
		}

		if ( in_array( $launch_status, array( LaunchStatus::READY, LaunchStatus::PENDING ), true ) ) {
			return new LaunchHealthProjection(
				LaunchHealth::READY,
				'launch.ready',
				array( self::ACTION_SCHEDULE, self::ACTION_RUN_NOW )
			);
		}

		if ( LaunchStatus::DRAFT === $launch_status || '' === $launch_status ) {
			return new LaunchHealthProjection( LaunchHealth::DRAFT, 'launch.draft', array( self::ACTION_VALIDATE ) );
		}

		return new LaunchHealthProjection( LaunchHealth::UNKNOWN, 'launch.unknown' );
	}

	/**
	 * Returns the machine-testable invariant rule matrix.
	 *
	 * @return array<int,array{id:string,level:string,message:string,when:callable(array<string,mixed>):bool}>
	 */
	public static function invariantRules(): array {
		return array(
			array(
				'id'      => 'version.missing',
				'level'   => 'blocking',
				'message' => 'Non-draft launches must point at a readable immutable version.',
				'when'    => static fn ( array $state ): bool => (int) ( $state['current_version_id'] ?? $state['version_id'] ?? 0 ) <= 0
					&& ! in_array( self::stringValue( $state, 'launch_status', 'status' ), array( '', LaunchStatus::DRAFT, LaunchStatus::CANCELLED ), true ),
			),
			array(
				'id'      => 'queue.overrides_terminal',
				'level'   => 'blocking',
				'message' => 'Queued version state cannot override terminal launch truth.',
				'when'    => static fn ( array $state ): bool => LaunchVersionStatus::SCHEDULED === self::stringValue( $state, 'version_status' )
					&& in_array( self::stringValue( $state, 'launch_status', 'status' ), array( LaunchStatus::SUCCESSFUL, LaunchStatus::FAILED, LaunchStatus::PARTIALLY_FAILED, LaunchStatus::ROLLED_BACK, LaunchStatus::ROLLBACK_FAILED, LaunchStatus::CANCELLED, LaunchStatus::SUPERSEDED ), true ),
			),
			array(
				'id'      => 'execution.success_with_failed_launch',
				'level'   => 'blocking',
				'message' => 'Successful run evidence cannot be presented as a failed launch without diagnostics.',
				'when'    => static fn ( array $state ): bool => RunStatus::SUCCESSFUL === self::stringValue( $state, 'last_run_status' )
					&& in_array( self::stringValue( $state, 'launch_status', 'status' ), array( LaunchStatus::FAILED, LaunchStatus::PARTIALLY_FAILED, LaunchStatus::DEAD_LETTER ), true ),
			),
			array(
				'id'      => 'version.failed_with_scheduled_launch',
				'level'   => 'blocking',
				'message' => 'Failed immutable version state cannot be shown as an active schedule.',
				'when'    => static fn ( array $state ): bool => LaunchStatus::SCHEDULED === self::stringValue( $state, 'launch_status', 'status' )
					&& in_array( self::stringValue( $state, 'version_status' ), array( LaunchVersionStatus::FAILED, LaunchVersionStatus::STALE, LaunchVersionStatus::DEAD_LETTER ), true ),
			),
		);
	}

	/**
	 * @param array<string,mixed> $state Durable state row.
	 * @return array<int,array<string,string>>
	 */
	public static function diagnostics( array $state ): array {
		$diagnostics = array();

		foreach ( self::invariantRules() as $rule ) {
			if ( $rule['when']( $state ) ) {
				$diagnostics[] = array(
					'code'    => $rule['id'],
					'level'   => $rule['level'],
					'message' => $rule['message'],
				);
			}
		}

		return $diagnostics;
	}

	/**
	 * Returns a rollback-specific projection.
	 */
	private static function rollbackProjection( string $launch_status ): LaunchHealthProjection {
		if ( LaunchStatus::ROLLBACK_FAILED === $launch_status ) {
			return new LaunchHealthProjection( LaunchHealth::ROLLBACK_FAILED, 'rollback.failed', array( self::ACTION_RETRY ) );
		}

		if ( LaunchStatus::ROLLED_BACK === $launch_status ) {
			return new LaunchHealthProjection( LaunchHealth::ROLLED_BACK, 'rollback.successful' );
		}

		return new LaunchHealthProjection( LaunchHealth::ROLLING_BACK, 'rollback.running' );
	}

	/**
	 * Returns whether a status is a failed terminal state.
	 */
	private static function failedStatus( string $status ): bool {
		return in_array(
			$status,
			array(
				LaunchStatus::FAILED,
				LaunchStatus::PARTIALLY_FAILED,
				LaunchStatus::STALE,
				LaunchStatus::DEAD_LETTER,
				LaunchVersionStatus::FAILED,
				LaunchVersionStatus::STALE,
				LaunchVersionStatus::DEAD_LETTER,
				RunStatus::FAILED,
				RunStatus::PARTIALLY_FAILED,
				RunStatus::STALE,
				RunStatus::DEAD_LETTER,
			),
			true
		);
	}

	/**
	 * @param array<string,mixed> $state Durable state row.
	 */
	private static function stringValue( array $state, string $primary, string $fallback = '' ): string {
		$value = (string) ( $state[ $primary ] ?? '' );

		return '' !== $value || '' === $fallback ? $value : (string) ( $state[ $fallback ] ?? '' );
	}

	/**
	 * @param array<int,array<string,string>> $diagnostics Existing diagnostics.
	 * @return array<int,array<string,string>>
	 */
	private static function withDiagnostic( array $diagnostics, string $code, string $message ): array {
		$diagnostics[] = array(
			'code'    => $code,
			'level'   => 'blocking',
			'message' => $message,
		);

		return $diagnostics;
	}

	/**
	 * @param array<string,mixed> $state Durable state row.
	 * @return array<int,array<string,string>>
	 */
	private static function validationDiagnostics( array $state ): array {
		$diagnostics = array();

		foreach ( is_array( $state['validation_diagnostics'] ?? null ) ? $state['validation_diagnostics'] : array() as $diagnostic ) {
			if ( is_array( $diagnostic ) ) {
				$diagnostics[] = array(
					'code'    => (string) ( $diagnostic['code'] ?? 'validation.blocked' ),
					'level'   => (string) ( $diagnostic['level'] ?? 'blocking' ),
					'message' => (string) ( $diagnostic['message'] ?? 'Launch validation is blocked.' ),
				);
			}
		}

		return array() === $diagnostics
			? self::withDiagnostic( array(), 'validation.blocked', 'Launch validation is blocked.' )
			: $diagnostics;
	}
}
