<?php
/**
 * Scheduler lifecycle repair diagnostics.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Scheduler;

// phpcs:disable WordPress.DB -- Scheduler-owned custom tables require direct wpdb diagnostics.

use ScheduledChanges\Database\TableNames;
use ScheduledChanges\Domain\Launch\LaunchStatus;
use ScheduledChanges\Domain\Launch\LaunchVersionStatus;
use ScheduledChanges\Domain\Run\RunStatus;
use ScheduledChanges\Domain\Run\TriggerSource;

/**
 * Produces bounded dry-run diagnostics for inconsistent scheduler lifecycle rows.
 */
final class LifecycleRepairDiagnostics {
	private \wpdb $wpdb;
	/**
	 * @var array<string,string>
	 */
	private array $tables;
	private const DEFAULT_LIMIT     = 20;
	private const MAX_LIMIT         = 50;
	private const STALE_RUN_SECONDS = 3600;

	public function __construct( ?\wpdb $database = null, ?TableNames $tables = null ) {
		$database     = \ScheduledChanges\Support\WpdbConnection::resolve( $database );
		$this->wpdb   = $database;
		$this->tables = ( $tables ?? new TableNames( $database ) )->all();
	}

	/**
	 * Returns bounded dry-run repair diagnostics.
	 *
	 * @return array<string,mixed>
	 */
	public function dryRun( int $limit = self::DEFAULT_LIMIT ): array {
		$limit  = max( 1, min( self::MAX_LIMIT, $limit ) );
		$cutoff = gmdate( 'Y-m-d H:i:s', time() - self::STALE_RUN_SECONDS );

		$claimed_without_run = $this->claimedVersionsWithoutActiveRun( $limit );
		$stale_active_runs   = $this->staleActiveRuns( $cutoff, $limit );
		$terminal_drift      = $this->terminalRunStateDrift( $limit );
		$rollback_links      = $this->incompleteRollbackLinks( $limit );
		$total               = count( $claimed_without_run )
			+ count( $stale_active_runs )
			+ count( $terminal_drift )
			+ count( $rollback_links );

		return array(
			'mode'                                       => 'dry_run',
			'sample_limit'                               => $limit,
			'stale_run_cutoff_utc'                       => $cutoff,
			'total_count'                                => $total,
			'claimed_version_without_active_run_count'   => count( $claimed_without_run ),
			'stale_active_run_count'                     => count( $stale_active_runs ),
			'terminal_run_state_drift_count'             => count( $terminal_drift ),
			'incomplete_rollback_link_count'             => count( $rollback_links ),
			'claimed_versions_without_active_run'        => $claimed_without_run,
			'stale_active_runs'                          => $stale_active_runs,
			'terminal_run_state_drift'                   => $terminal_drift,
			'incomplete_rollback_links'                  => $rollback_links,
			'repair_actions'                             => $this->repairActions(),
			'stuck_running_repair_safe_without_readback' => false,
		);
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private function claimedVersionsWithoutActiveRun( int $limit ): array {
		$rows = $this->wpdb->get_results(
			$this->wpdb->prepare(
				'/* scheduled-changes:claimed_version_without_active_run */
				SELECT v.id AS launch_version_id,
					v.launch_id,
					v.status AS version_status,
					v.action_scheduler_id,
					v.occurrence_at_utc,
					v.attempt_number,
					v.trigger_source
				FROM %i v
				LEFT JOIN %i r
					ON r.launch_version_id = v.id
					AND r.status IN (%s, %s, %s)
				WHERE v.status = %s
					AND r.id IS NULL
				ORDER BY v.updated_at_utc ASC, v.id ASC
				LIMIT %d',
				$this->tables['launch_versions'],
				$this->tables['runs'],
				RunStatus::PENDING,
				RunStatus::VALIDATING,
				RunStatus::RUNNING,
				LaunchVersionStatus::RUNNING,
				$limit
			),
			'ARRAY_A'
		);

		return $this->rows( $rows, $limit );
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private function staleActiveRuns( string $cutoff, int $limit ): array {
		$rows = $this->wpdb->get_results(
			$this->wpdb->prepare(
				'/* scheduled-changes:stale_active_run */
				SELECT id AS run_id,
					launch_id,
					launch_version_id,
					action_scheduler_id,
					status AS run_status,
					scheduled_at_utc,
					started_at_utc
				FROM %i
				WHERE status IN (%s, %s, %s)
					AND COALESCE(started_at_utc, scheduled_at_utc) < %s
				ORDER BY COALESCE(started_at_utc, scheduled_at_utc) ASC, id ASC
				LIMIT %d',
				$this->tables['runs'],
				RunStatus::PENDING,
				RunStatus::VALIDATING,
				RunStatus::RUNNING,
				$cutoff,
				$limit
			),
			'ARRAY_A'
		);

		return $this->rows( $rows, $limit );
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private function terminalRunStateDrift( int $limit ): array {
		$rows = $this->wpdb->get_results(
			$this->wpdb->prepare(
				'/* scheduled-changes:terminal_run_state_drift */
				SELECT r.id AS run_id,
					r.launch_id,
					r.launch_version_id,
					r.status AS run_status,
					l.status AS launch_status,
					v.status AS version_status,
					r.completed_at_utc
				FROM %i r
				INNER JOIN %i l ON l.id = r.launch_id
				INNER JOIN %i v ON v.id = r.launch_version_id
				WHERE r.trigger_source <> %s
					AND r.status IN (%s, %s, %s, %s, %s, %s, %s)
					AND (
						l.status IN (%s, %s, %s, %s, %s)
						OR v.status IN (%s, %s, %s)
					)
				ORDER BY r.completed_at_utc ASC, r.id ASC
				LIMIT %d',
				$this->tables['runs'],
				$this->tables['launches'],
				$this->tables['launch_versions'],
				TriggerSource::MANUAL_ROLLBACK,
				RunStatus::SUCCESSFUL,
				RunStatus::SUCCESSFUL_WITH_WARNINGS,
				RunStatus::PARTIALLY_FAILED,
				RunStatus::FAILED,
				RunStatus::STALE,
				RunStatus::DEAD_LETTER,
				RunStatus::CANCELLED,
				LaunchStatus::PENDING,
				LaunchStatus::VALIDATING,
				LaunchStatus::READY,
				LaunchStatus::SCHEDULED,
				LaunchStatus::RUNNING,
				LaunchVersionStatus::DRAFT,
				LaunchVersionStatus::SCHEDULED,
				LaunchVersionStatus::RUNNING,
				$limit
			),
			'ARRAY_A'
		);

		return $this->rows( $rows, $limit );
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private function incompleteRollbackLinks( int $limit ): array {
		$rows = $this->wpdb->get_results(
			$this->wpdb->prepare(
				'/* scheduled-changes:incomplete_rollback_link */
				SELECT source.id AS source_run_id,
					source.launch_id,
					source.launch_version_id,
					source.rollback_run_id,
					rollback.status AS rollback_status
				FROM %i source
				LEFT JOIN %i rollback ON rollback.id = source.rollback_run_id
				WHERE source.rollback_run_id IS NOT NULL
					AND (
						rollback.id IS NULL
						OR rollback.status IN (%s, %s, %s)
					)
				ORDER BY source.completed_at_utc ASC, source.id ASC
				LIMIT %d',
				$this->tables['runs'],
				$this->tables['runs'],
				RunStatus::PENDING,
				RunStatus::VALIDATING,
				RunStatus::RUNNING,
				$limit
			),
			'ARRAY_A'
		);

		return $this->rows( $rows, $limit );
	}

	/**
	 * @param mixed $rows Query rows.
	 * @return array<int,array<string,mixed>>
	 */
	private function rows( $rows, int $limit ): array {
		if ( ! is_array( $rows ) ) {
			return array();
		}

		return array_map(
			array( $this, 'sanitizeRow' ),
			array_slice( $rows, 0, $limit )
		);
	}

	/**
	 * @param array<string,mixed> $row Database row.
	 * @return array<string,mixed>
	 */
	private function sanitizeRow( array $row ): array {
		$sanitized = array();

		foreach ( $row as $key => $value ) {
			if ( null === $value ) {
				$sanitized[ (string) $key ] = null;
				continue;
			}

			$sanitized[ (string) $key ] = preg_match( '/(^id$|_id$|_count$|attempt_number$)/', (string) $key )
				? (int) $value
				: (string) $value;
		}

		return $sanitized;
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private function repairActions(): array {
		return array(
			array(
				'state'          => 'claimed_version_without_active_run',
				'action'         => 'mark_stale_after_readback_or_attach_active_run',
				'automatic_safe' => false,
			),
			array(
				'state'          => 'stale_active_run',
				'action'         => 'inspect_mutation_readback_before_finalizing_stale',
				'automatic_safe' => false,
			),
			array(
				'state'          => 'terminal_run_state_drift',
				'action'         => 'roll_forward_launch_and_version_status_from_terminal_run',
				'automatic_safe' => false,
			),
			array(
				'state'          => 'incomplete_rollback_link',
				'action'         => 'relink_or_mark_rollback_failed_after_run_inspection',
				'automatic_safe' => false,
			),
		);
	}
}
