<?php
/**
 * Queue diagnostics.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Scheduler;

use ScheduledChanges\Lock\LockManager;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Repository\RunRepository;

/**
 * Combines Action Scheduler, run delay, cron, and lock diagnostics.
 */
final class QueueMonitor {
	private ActionSchedulerGateway $actions;
	private RunRepository $runs;
	private LockManager $locks;
	private ?LaunchVersionRepository $versions;
	private ?LifecycleRepairDiagnostics $lifecycleRepair;
	public const LAST_ACTIVITY_OPTION = 'scheduled_changes_last_queue_activity_utc';

	public function __construct( ActionSchedulerGateway $actions, RunRepository $runs, LockManager $locks, ?LaunchVersionRepository $versions = null, ?LifecycleRepairDiagnostics $lifecycleRepair = null ) {
		$this->actions         = $actions;
		$this->runs            = $runs;
		$this->locks           = $locks;
		$this->versions        = $versions;
		$this->lifecycleRepair = $lifecycleRepair;
	}

	public function register(): void {
		add_action( 'action_scheduler_before_process_queue', array( self::class, 'recordActivity' ), 10, 0 );
	}

	public function status(): QueueStatus {
		$counts  = $this->actions->counts();
		$metrics = $this->runs->delayMetrics();
		$last    = get_option( self::LAST_ACTIVITY_OPTION, null );

		return new QueueStatus(
			$this->actions->available(),
			defined( 'DISABLE_WP_CRON' ) && true === DISABLE_WP_CRON,
			is_string( $last ) && '' !== $last ? $last : null,
			$counts['pending'],
			$counts['running'],
			$counts['failed'],
			$counts['complete'],
			$counts['overdue'],
			$metrics['average_delay_seconds'],
			$metrics['maximum_delay_seconds'],
			$metrics['sample_size'],
			$this->locks->staleCount(),
			$this->repairDiagnostics()
		);
	}

	/**
	 * @return array<string,mixed>
	 */
	private function repairDiagnostics(): array {
		$lifecycle = $this->lifecycleDiagnostics();

		if ( null === $this->versions ) {
			return array(
				'orphaned_action_count'         => 0,
				'missing_action_count'          => 0,
				'mismatched_pointer_count'      => 0,
				'lifecycle_inconsistency_count' => 0,
				'orphaned_actions'              => array(),
				'missing_actions'               => array(),
				'mismatched_pointers'           => array(),
				'lifecycle'                     => $lifecycle,
				'repair_actions'                => $this->repairActions(),
				'stuck_running_repair_safe'     => false,
			);
		}

		$actions      = $this->actions->scheduledLaunchActions();
		$pointers     = $this->versions->queuedCallbacks();
		$actionsById  = array();
		$pointersById = array();
		$orphaned     = array();
		$missing      = array();
		$mismatched   = array();

		foreach ( $actions as $action ) {
			$actionsById[ (int) $action['action_scheduler_id'] ] = $action;
		}

		foreach ( $pointers as $pointer ) {
			$pointersById[ (int) $pointer['action_scheduler_id'] ] = $pointer;
		}

		foreach ( $actionsById as $action_id => $action ) {
			if ( ! isset( $pointersById[ $action_id ] ) ) {
				$orphaned[] = $action;
				continue;
			}

			$pointer = $pointersById[ $action_id ];
			if (
				(int) $action['version_id'] !== (int) $pointer['id']
				|| (string) $action['occurrence_at_utc'] !== (string) $pointer['occurrence_at_utc']
				|| (int) $action['attempt_number'] !== (int) $pointer['attempt_number']
				|| (string) $action['trigger_source'] !== (string) $pointer['trigger_source']
			) {
				$mismatched[] = array(
					'action'  => $action,
					'pointer' => $pointer,
				);
			}
		}

		foreach ( $pointersById as $action_id => $pointer ) {
			if ( ! isset( $actionsById[ $action_id ] ) ) {
				$missing[] = $pointer;
			}
		}

		return array(
			'orphaned_action_count'         => count( $orphaned ),
			'missing_action_count'          => count( $missing ),
			'mismatched_pointer_count'      => count( $mismatched ),
			'lifecycle_inconsistency_count' => (int) ( $lifecycle['total_count'] ?? 0 ),
			'orphaned_actions'              => $orphaned,
			'missing_actions'               => $missing,
			'mismatched_pointers'           => $mismatched,
			'lifecycle'                     => $lifecycle,
			'repair_actions'                => $this->repairActions(),
			'stuck_running_repair_safe'     => false,
		);
	}

	/**
	 * @return array<string,mixed>
	 */
	private function lifecycleDiagnostics(): array {
		if ( null === $this->lifecycleRepair ) {
			return array(
				'mode'                                     => 'dry_run',
				'sample_limit'                             => 0,
				'total_count'                              => 0,
				'claimed_version_without_active_run_count' => 0,
				'stale_active_run_count'                   => 0,
				'terminal_run_state_drift_count'           => 0,
				'incomplete_rollback_link_count'           => 0,
				'claimed_versions_without_active_run'      => array(),
				'stale_active_runs'                        => array(),
				'terminal_run_state_drift'                 => array(),
				'incomplete_rollback_links'                => array(),
				'repair_actions'                           => array(),
				'stuck_running_repair_safe_without_readback' => false,
			);
		}

		return $this->lifecycleRepair->dryRun();
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	private function repairActions(): array {
		return array(
			array(
				'state'          => 'orphaned_action',
				'actions'        => array( 'cancel_action', 'attach_pointer' ),
				'automatic_safe' => false,
			),
			array(
				'state'          => 'missing_action',
				'actions'        => array( 'reschedule_from_pointer', 'mark_stale_after_readback' ),
				'automatic_safe' => false,
			),
			array(
				'state'          => 'mismatched_pointer',
				'actions'        => array( 'cancel_orphaned_action', 'restore_canonical_pointer' ),
				'automatic_safe' => false,
			),
		);
	}

	public static function recordActivity(): void {
		update_option( self::LAST_ACTIVITY_OPTION, gmdate( 'c' ), false );
	}
}
