<?php
/**
 * Diagnostic read models for the WordPress administration application.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Admin;

use ScheduledChanges\Database\Migrator;
use ScheduledChanges\Database\TableHealth;
use ScheduledChanges\Domain\Launch\OperationPlan;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Repository\AdminDashboardRepository;
use ScheduledChanges\Repository\RecordingSessionRepository;
use ScheduledChanges\Scheduler\QueueMonitor;
use ScheduledChanges\Support\ThrowableDiagnostic;
use ScheduledChanges\Support\VersionPolicy;
use ScheduledChanges\Validation\WordPressValidationEnvironment;

/**
 * Builds support, system, and scheduler diagnostics for admin screens.
 */
final class AdminAppDiagnostics {
	private ?ProviderManager $providers;
	private ?QueueMonitor $queue;
	private RecordingSessionRepository $recordings;
	private AdminDashboardRepository $dashboard;

	/**
	 * Creates the diagnostics read-model builder.
	 */
	public function __construct(
		?ProviderManager $providers,
		?QueueMonitor $queue,
		RecordingSessionRepository $recordings,
		AdminDashboardRepository $dashboard
	) {
		$this->providers  = $providers;
		$this->queue      = $queue;
		$this->recordings = $recordings;
		$this->dashboard  = $dashboard;
	}

	/**
	 * Returns support-report data for download.
	 *
	 * @return array<string,mixed>
	 */
	public function supportReportData(): array {
		return array(
			'generated_at_utc'    => gmdate( 'c' ),
			'system'              => $this->systemRows(),
			'launch_count'        => count( $this->dashboard->launches( 100 ) ),
			'active_recordings'   => $this->recordings->activeCount(),
			'stale_recordings'    => $this->recordings->staleCount(),
			'orphaned_workspaces' => $this->recordings->orphanedWorkspaceCount(),
			'scheduler'           => $this->schedulerDiagnostics(),
		);
	}

	/**
	 * Returns system status rows.
	 *
	 * @return array<int,array{label:string,value:string}>
	 */
	public function systemRows(): array {
		global $wp_version;

		$action_scheduler_version = class_exists( 'ActionScheduler_Versions' )
			? \ActionScheduler_Versions::instance()->latest_version()
			: __( 'Unavailable', 'jjs-post-scheduler' );
		$migrator                 = new Migrator();
		$table_health             = ( new TableHealth() )->summary();
		$migration_status         = $migrator->status();
		$provider_versions        = null !== $this->providers
			? $this->providers->providers()->versions()
			: array();
		$provider_label           = array() === $provider_versions
			? __( 'None registered', 'jjs-post-scheduler' )
			: implode(
				', ',
				array_map(
					static fn ( string $id, string $version ): string => $id . ' ' . $version,
					array_keys( $provider_versions ),
					$provider_versions
				)
			);
		$provider_errors          = null !== $this->providers ? $this->redactedProviderErrors( $this->providers->errors() ) : array();
		$action_scheduler_healthy = ( new WordPressValidationEnvironment() )->actionSchedulerHealthy();
		$queue_status             = null === $this->queue ? array() : $this->queue->status()->toArray();

		$rows = array(
			__( 'Plugin version', 'jjs-post-scheduler' )   => SCHEDULED_CHANGES_VERSION,
			__( 'Schema version', 'jjs-post-scheduler' )   => SCHEDULED_CHANGES_SCHEMA_VERSION,
			__( 'WordPress', 'jjs-post-scheduler' )        => (string) $wp_version,
			__( 'PHP', 'jjs-post-scheduler' )              => PHP_VERSION,
			__( 'Action Scheduler', 'jjs-post-scheduler' ) => $action_scheduler_version,
			__( 'Action Scheduler API health', 'jjs-post-scheduler' ) => $action_scheduler_healthy ? __( 'Ready', 'jjs-post-scheduler' ) : __( 'Unavailable', 'jjs-post-scheduler' ),
			__( 'Operation plan schema', 'jjs-post-scheduler' ) => (string) OperationPlan::SCHEMA_VERSION,
			__( 'Queue health', 'jjs-post-scheduler' )     => (bool) ( $queue_status['healthy'] ?? false ) ? __( 'Healthy', 'jjs-post-scheduler' ) : __( 'Needs attention', 'jjs-post-scheduler' ),
			__( 'WP-Cron disabled', 'jjs-post-scheduler' ) => (bool) ( $queue_status['wp_cron_disabled'] ?? false ) ? __( 'Yes', 'jjs-post-scheduler' ) : __( 'No', 'jjs-post-scheduler' ),
			__( 'JJ\'s Post Scheduler pending actions', 'jjs-post-scheduler' ) => (string) ( $queue_status['pending'] ?? 0 ),
			__( 'JJ\'s Post Scheduler running actions', 'jjs-post-scheduler' ) => (string) ( $queue_status['running'] ?? 0 ),
			__( 'JJ\'s Post Scheduler failed actions', 'jjs-post-scheduler' ) => (string) ( $queue_status['failed'] ?? 0 ),
			__( 'JJ\'s Post Scheduler overdue actions', 'jjs-post-scheduler' ) => (string) ( $queue_status['overdue'] ?? 0 ),
			__( 'Stale target locks', 'jjs-post-scheduler' ) => (string) ( $queue_status['stale_locks'] ?? 0 ),
			__( 'Minimum PHP', 'jjs-post-scheduler' )      => VersionPolicy::MIN_PHP,
			__( 'Minimum WordPress', 'jjs-post-scheduler' ) => VersionPolicy::MIN_WORDPRESS,
			__( 'Installed schema', 'jjs-post-scheduler' ) => (string) $migrator->currentVersion(),
			__( 'Required schema', 'jjs-post-scheduler' )  => (string) $migrator->latestVersion(),
			__( 'Last migration', 'jjs-post-scheduler' )   => (string) ( $migration_status['status'] ?? 'not_run' ),
			__( 'Database tables', 'jjs-post-scheduler' )  => sprintf( '%1$d of %2$d present', $table_health['existing'], $table_health['expected'] ),
			__( 'Missing tables', 'jjs-post-scheduler' )   => array() === $table_health['missing'] ? __( 'None', 'jjs-post-scheduler' ) : implode( ', ', $table_health['missing'] ),
			__( 'Provider lifecycle', 'jjs-post-scheduler' ) => null !== $this->providers && $this->providers->loaded() ? __( 'Loaded', 'jjs-post-scheduler' ) : __( 'Pending provider initialization', 'jjs-post-scheduler' ),
			__( 'Registered providers', 'jjs-post-scheduler' ) => $provider_label,
			__( 'Provider errors', 'jjs-post-scheduler' )  => array() === $provider_errors ? __( 'None', 'jjs-post-scheduler' ) : implode( '; ', $provider_errors ),
			__( 'Active recording sessions', 'jjs-post-scheduler' ) => (string) $this->recordings->activeCount(),
			__( 'Stale recording sessions', 'jjs-post-scheduler' ) => (string) $this->recordings->staleCount(),
			__( 'Orphaned recording workspaces', 'jjs-post-scheduler' ) => (string) $this->recordings->orphanedWorkspaceCount(),
		);

		$out = array();
		foreach ( $rows as $label => $value ) {
			$out[] = array(
				'label' => (string) $label,
				'value' => (string) $value,
			);
		}

		return PublicHooks::adminSystemRows( $out, $this->providers );
	}

	/**
	 * Returns scheduler and queue diagnostics for support panels.
	 *
	 * @return array<string,mixed>
	 */
	public function schedulerDiagnostics(): array {
		$status = null === $this->queue ? array() : $this->queue->status()->toArray();
		$repair = is_array( $status['repair_diagnostics'] ?? null ) ? $status['repair_diagnostics'] : array();

		return array(
			'status'        => $this->schedulerStatusLabel( $status ),
			'nextAction'    => $this->schedulerNextAction( $status ),
			'counts'        => array(
				'pending'    => (int) ( $status['pending'] ?? 0 ),
				'running'    => (int) ( $status['running'] ?? 0 ),
				'failed'     => (int) ( $status['failed'] ?? 0 ),
				'complete'   => (int) ( $status['complete'] ?? 0 ),
				'overdue'    => (int) ( $status['overdue'] ?? 0 ),
				'staleLocks' => (int) ( $status['stale_locks'] ?? 0 ),
			),
			'timing'        => array(
				'wpCronDisabled'      => (bool) ( $status['wp_cron_disabled'] ?? false ),
				'lastActivityUtc'     => (string) ( $status['last_activity_utc'] ?? '' ),
				'averageDelaySeconds' => (float) ( $status['average_delay_seconds'] ?? 0 ),
				'maximumDelaySeconds' => (int) ( $status['maximum_delay_seconds'] ?? 0 ),
				'delaySampleSize'     => (int) ( $status['delay_sample_size'] ?? 0 ),
			),
			'repairCounts'  => array(
				'orphanedActions'        => (int) ( $repair['orphaned_action_count'] ?? 0 ),
				'missingActions'         => (int) ( $repair['missing_action_count'] ?? 0 ),
				'mismatchedPointers'     => (int) ( $repair['mismatched_pointer_count'] ?? 0 ),
				'lifecycleInconsistency' => (int) ( $repair['lifecycle_inconsistency_count'] ?? 0 ),
			),
			'tasks'         => $this->schedulerTaskRows( $repair ),
			'repairActions' => $this->schedulerRepairActions( $repair ),
		);
	}

	/**
	 * @param array<string,mixed> $status Queue status payload.
	 */
	private function schedulerStatusLabel( array $status ): string {
		if ( false === (bool) ( $status['available'] ?? false ) ) {
			return __( 'Unavailable', 'jjs-post-scheduler' );
		}

		if ( true === (bool) ( $status['healthy'] ?? false ) ) {
			return __( 'Working', 'jjs-post-scheduler' );
		}

		return __( 'Delayed or needs review', 'jjs-post-scheduler' );
	}

	/**
	 * @param array<string,mixed> $status Queue status payload.
	 */
	private function schedulerNextAction( array $status ): string {
		if ( false === (bool) ( $status['available'] ?? false ) ) {
			return __( 'Confirm Action Scheduler is available, then refresh this check.', 'jjs-post-scheduler' );
		}

		if ( (int) ( $status['failed'] ?? 0 ) > 0 || (int) ( $status['overdue'] ?? 0 ) > 0 ) {
			return __( 'Review scheduled changes that need attention and ask your host to check WP-Cron if delays continue.', 'jjs-post-scheduler' );
		}

		if ( true === (bool) ( $status['wp_cron_disabled'] ?? false ) ) {
			return __( 'WP-Cron is disabled. Confirm a server cron runs WordPress on schedule.', 'jjs-post-scheduler' );
		}

		return __( 'No scheduler action needed.', 'jjs-post-scheduler' );
	}

	/**
	 * @param array<string,mixed> $repair Repair diagnostics.
	 * @return array<int,array<string,string>>
	 */
	private function schedulerTaskRows( array $repair ): array {
		$rows = array_merge(
			$this->schedulerActionRows( __( 'Orphaned scheduled task', 'jjs-post-scheduler' ), is_array( $repair['orphaned_actions'] ?? null ) ? $repair['orphaned_actions'] : array() ),
			$this->schedulerPointerRows( __( 'Missing scheduled task', 'jjs-post-scheduler' ), is_array( $repair['missing_actions'] ?? null ) ? $repair['missing_actions'] : array() ),
			$this->schedulerMismatchedRows( is_array( $repair['mismatched_pointers'] ?? null ) ? $repair['mismatched_pointers'] : array() )
		);

		return array_slice( $rows, 0, 20 );
	}

	/**
	 * @param array<int,mixed> $actions Action Scheduler rows.
	 * @return array<int,array<string,string>>
	 */
	private function schedulerActionRows( string $state, array $actions ): array {
		return array_values(
			array_map(
				fn ( $action ): array => $this->schedulerTaskRow( $state, is_array( $action ) ? $action : array(), __( 'Review whether this task should be cancelled or reattached.', 'jjs-post-scheduler' ) ),
				$actions
			)
		);
	}

	/**
	 * @param array<int,mixed> $pointers Launch version pointer rows.
	 * @return array<int,array<string,string>>
	 */
	private function schedulerPointerRows( string $state, array $pointers ): array {
		return array_values(
			array_map(
				fn ( $pointer ): array => $this->schedulerTaskRow( $state, is_array( $pointer ) ? $pointer : array(), __( 'Review the scheduled change and reschedule if the task is missing.', 'jjs-post-scheduler' ) ),
				$pointers
			)
		);
	}

	/**
	 * @param array<int,mixed> $mismatches Mismatched action/pointer rows.
	 * @return array<int,array<string,string>>
	 */
	private function schedulerMismatchedRows( array $mismatches ): array {
		$rows = array();

		foreach ( $mismatches as $mismatch ) {
			$mismatch = is_array( $mismatch ) ? $mismatch : array();
			$action   = is_array( $mismatch['action'] ?? null ) ? $mismatch['action'] : array();
			$pointer  = is_array( $mismatch['pointer'] ?? null ) ? $mismatch['pointer'] : array();
			$row      = $this->schedulerTaskRow(
				__( 'Mismatched scheduled task', 'jjs-post-scheduler' ),
				array_merge( $pointer, $action ),
				__( 'Cancel the stale task or restore the saved schedule pointer.', 'jjs-post-scheduler' )
			);

			if ( isset( $pointer['id'] ) ) {
				$row['savedScheduleId'] = (string) (int) $pointer['id'];
			}

			$rows[] = $row;
		}

		return $rows;
	}

	/**
	 * @param array<string,mixed> $row Queue row.
	 * @return array<string,string>
	 */
	private function schedulerTaskRow( string $state, array $row, string $next_action ): array {
		return array(
			'state'           => $state,
			'taskId'          => isset( $row['action_scheduler_id'] ) ? (string) (int) $row['action_scheduler_id'] : __( 'Unavailable', 'jjs-post-scheduler' ),
			'savedScheduleId' => isset( $row['version_id'] ) ? (string) (int) $row['version_id'] : ( isset( $row['id'] ) ? (string) (int) $row['id'] : __( 'Unavailable', 'jjs-post-scheduler' ) ),
			'status'          => sanitize_key( (string) ( $row['status'] ?? 'unknown' ) ),
			'occurrenceAt'    => sanitize_text_field( (string) ( $row['occurrence_at_utc'] ?? '' ) ),
			'attempt'         => isset( $row['attempt_number'] ) ? (string) (int) $row['attempt_number'] : __( 'Unavailable', 'jjs-post-scheduler' ),
			'trigger'         => sanitize_key( (string) ( $row['trigger_source'] ?? 'unknown' ) ),
			'nextAction'      => $next_action,
		);
	}

	/**
	 * @param array<string,mixed> $repair Repair diagnostics.
	 * @return array<int,array<string,string>>
	 */
	private function schedulerRepairActions( array $repair ): array {
		$actions = is_array( $repair['repair_actions'] ?? null ) ? $repair['repair_actions'] : array();

		return array_values(
			array_map(
				static function ( $action ): array {
					$action = is_array( $action ) ? $action : array();

					return array(
						'state'         => sanitize_key( (string) ( $action['state'] ?? 'unknown' ) ),
						'actions'       => implode( ', ', array_map( 'sanitize_key', is_array( $action['actions'] ?? null ) ? $action['actions'] : array() ) ),
						'automaticSafe' => true === ( $action['automatic_safe'] ?? false ) ? __( 'Yes', 'jjs-post-scheduler' ) : __( 'No', 'jjs-post-scheduler' ),
					);
				},
				$actions
			)
		);
	}

	/**
	 * Redacts provider initialization errors before they reach admin screens.
	 *
	 * @param array<int,mixed> $errors Provider errors.
	 * @return string[]
	 */
	private function redactedProviderErrors( array $errors ): array {
		return array_values(
			array_map(
				static fn ( $error ): string => ThrowableDiagnostic::redactMessage( (string) $error ),
				$errors
			)
		);
	}
}
