<?php
/**
 * Read-only and diagnostic WP-CLI command handlers.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\WpCli;

use ScheduledChanges\Capabilities\CapabilityManager;
use ScheduledChanges\Capabilities\WordPressPermissionService;
use ScheduledChanges\Database\Migrator;
use ScheduledChanges\Database\TableHealth;
use ScheduledChanges\Domain\Launch\Launch;
use ScheduledChanges\Domain\Launch\LaunchVersion;
use ScheduledChanges\Domain\Shared\Uuid;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Provider\ProviderAuthorization;
use ScheduledChanges\Repository\AdminDashboardRepository;
use ScheduledChanges\Repository\LaunchRepository;
use ScheduledChanges\Repository\LaunchVersionRepository;
use ScheduledChanges\Repository\RecordingSessionRepository;
use ScheduledChanges\Scheduler\QueueMonitor;
use ScheduledChanges\Support\Requirements;
use ScheduledChanges\Validation\ValidationLevel;
use ScheduledChanges\Validation\ValidationModule;

/**
 * Handles CLI commands that inspect providers, queue state, logs, and health.
 */
final class InspectionCommands extends ModuleCommand {
	private ?ProviderManager $providers;
	private ?QueueMonitor $queue;
	private RecordingSessionRepository $recordings;
	private AdminDashboardRepository $dashboard;
	private LaunchRepository $launches;
	private LaunchVersionRepository $versions;
	private WordPressPermissionService $permissions;
	private ProviderAuthorization $authorization;

	/**
	 * Creates the read-only command handler.
	 */
	public function __construct(
		?ProviderManager $providers,
		?QueueMonitor $queue,
		RecordingSessionRepository $recordings,
		AdminDashboardRepository $dashboard,
		LaunchRepository $launches,
		LaunchVersionRepository $versions,
		WordPressPermissionService $permissions,
		ProviderAuthorization $authorization
	) {
		$this->providers     = $providers;
		$this->queue         = $queue;
		$this->recordings    = $recordings;
		$this->dashboard     = $dashboard;
		$this->launches      = $launches;
		$this->versions      = $versions;
		$this->permissions   = $permissions;
		$this->authorization = $authorization;
	}

	/**
	 * Lists loaded providers.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function providers( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::MANAGE_SETTINGS );
		$subcommand = (string) ( $args[0] ?? '' );

		if ( 'list' !== $subcommand ) {
			$this->usage( 'Unknown providers subcommand. Use list.' );
		}

		$providers = $this->providersOrFail();
		$errors    = $providers->errors();
		$rows      = array();

		foreach ( $providers->providers()->versions() as $id => $version ) {
			$rows[] = array(
				'id'      => (string) $id,
				'version' => (string) $version,
				'status'  => array() === $errors ? 'loaded' : 'loaded_with_errors',
			);
		}

		$this->formatRows( $rows, array( 'id', 'version', 'status' ), $assoc_args );
	}

	/**
	 * Scans fields for a provider-owned target.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function fields( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::VIEW_SCHEDULES );
		$subcommand = (string) ( $args[0] ?? '' );

		if ( 'scan' !== $subcommand ) {
			$this->usage( 'Unknown fields subcommand. Use scan <target-reference>.' );
		}

		$target_reference = (string) ( $args[1] ?? '' );
		$context          = PublicHooks::cliFieldScanContext( null, $target_reference, $assoc_args );

		if ( null === $context ) {
			\WP_CLI::error( 'No provider could resolve the requested field scan target.' );
		}

		if (
			null !== $context->targetId()
			&& in_array( $context->targetType(), array( 'post', 'page' ), true )
			&& ! $this->permissions->canInspectPost( $this->actorId(), $context->targetId() )
		) {
			\WP_CLI::error( 'The current actor cannot inspect this WordPress target.' );
		}

		$rows = array();

		foreach ( $this->providersOrFail()->fields()->discover( $context )->fields() as $field ) {
			$rows[] = array(
				'id'          => $field->id(),
				'label'       => $field->label(),
				'provider'    => $field->provider(),
				'providerId'  => $field->provider(),
				'targetType'  => $context->targetType(),
				'targetId'    => $context->targetId(),
				'type'        => $field->dataType(),
				'writable'    => $field->writable() ? 'yes' : 'no',
				'schedulable' => $field->schedulable() ? 'yes' : 'no',
				'reversible'  => $field->reversible() ? 'yes' : 'no',
				'sensitive'   => $field->sensitive() ? 'yes' : 'no',
			);
		}

		$this->formatRows(
			$rows,
			array( 'id', 'label', 'providerId', 'targetType', 'targetId', 'type', 'writable', 'schedulable', 'reversible', 'sensitive' ),
			$assoc_args
		);
	}

	/**
	 * Shows queue diagnostics.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function queue( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::VIEW_LOGS );
		$subcommand = (string) ( $args[0] ?? '' );

		if ( 'status' !== $subcommand ) {
			$this->usage( 'Unknown queue subcommand. Use status.' );
		}

		if ( null === $this->queue ) {
			\WP_CLI::error( 'Queue monitor is unavailable.' );
		}

		$this->formatAssoc( $this->queue->status()->toArray(), $assoc_args );
	}

	/**
	 * Lists or shows permission-filtered execution logs.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function logs( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::VIEW_LOGS );
		$subcommand = (string) ( $args[0] ?? '' );
		$rows       = $this->dashboard->runs( $this->limit( $assoc_args ) );

		if ( 'list' === $subcommand ) {
			$this->formatRows(
				$rows,
				array( 'id', 'uuid', 'launchUuid', 'launchName', 'status', 'triggerSource', 'startedAt', 'completedAt', 'warningCount', 'errorCount' ),
				$assoc_args
			);
			return;
		}

		if ( 'show' !== $subcommand ) {
			$this->usage( 'Unknown logs subcommand. Use list or show <run-id-or-uuid>.' );
		}

		$identifier = trim( (string) ( $args[1] ?? '' ) );
		$matched    = array_values(
			array_filter(
				$rows,
				static fn ( array $row ): bool => (string) ( $row['id'] ?? '' ) === $identifier
					|| (string) ( $row['uuid'] ?? '' ) === $identifier
			)
		);

		if ( '' === $identifier || ! isset( $matched[0] ) ) {
			\WP_CLI::error( 'Run log was not found or is not available to the current actor.' );
		}

		$this->formatAssoc( $matched[0], $assoc_args );
	}

	/**
	 * Lists current warning and blocking validation conflicts.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function conflicts( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::VIEW_SCHEDULES );

		if ( 'list' !== (string) ( $args[0] ?? '' ) ) {
			$this->usage( 'Unknown conflicts subcommand. Use list.' );
		}

		$launch_filter = trim( (string) ( $assoc_args['launch'] ?? '' ) );
		$launch_rows   = '' === $launch_filter
			? $this->dashboard->launches( $this->limit( $assoc_args ) )
			: array_filter( array( $this->dashboard->launchByUuid( $launch_filter ) ) );

		if ( '' !== $launch_filter && array() === $launch_rows ) {
			\WP_CLI::error( 'Launch was not found or is not available to the current actor.' );
		}

		$visible_launches = array();

		foreach ( $launch_rows as $launch_row ) {
			if ( ! is_array( $launch_row ) || '' === (string) ( $launch_row['uuid'] ?? '' ) ) {
				continue;
			}

			$launch  = $this->launchByUuid( new Uuid( (string) $launch_row['uuid'] ) );
			$version = $this->visibleVersionForLaunch( $launch );

			if ( null === $version ) {
				continue;
			}

			$visible_launches[] = array( $launch_row, $launch, $version );
		}

		if ( '' !== $launch_filter && array() === $visible_launches ) {
			\WP_CLI::error( 'Launch was not found or is not available to the current actor.' );
		}

		$providers = $this->providersOrFail();
		$validator = ( new ValidationModule() )->createServices(
			$providers->providers(),
			$providers->fields()
		)->validator();
		$rows      = array();

		foreach ( $visible_launches as $visible_launch ) {
			list( $launch_row, $launch, $version ) = $visible_launch;
			$report                                = $validator->validate(
				$version->plan()->graph(),
				$version->plan()->requiredProviders(),
				$this->actorId()
			);

			foreach ( $report->issues() as $issue ) {
				if ( ValidationLevel::INFORMATIONAL === $issue->level() ) {
					continue;
				}

				$rows[] = array(
					'launchUuid' => $launch->uuid()->value(),
					'launchName' => (string) ( $launch_row['name'] ?? '' ),
					'code'       => $issue->code(),
					'level'      => $issue->level(),
					'path'       => $issue->path(),
					'message'    => $issue->message(),
				);
			}
		}

		$this->formatRows( $rows, array( 'launchUuid', 'launchName', 'code', 'level', 'path', 'message' ), $assoc_args );
	}

	/**
	 * Shows migration status or runs pending migrations.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function migrations( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::MANAGE_SETTINGS );
		$subcommand = (string) ( $args[0] ?? '' );
		$migrator   = new Migrator();

		if ( 'status' === $subcommand ) {
			$this->formatAssoc( $this->migrationStatus( $migrator ), $assoc_args );
			return;
		}

		if ( 'run' !== $subcommand ) {
			$this->usage( 'Unknown migrations subcommand. Use status or run.' );
		}

		$this->requireYes( $assoc_args, 'Running migrations mutates the local schema.' );
		$migrator->migrate();
		$this->formatAssoc( $this->migrationStatus( $migrator ), $assoc_args );
	}

	/**
	 * Runs broad JJ\'s Post Scheduler diagnostics.
	 *
	 * @param string[]            $args Positional arguments.
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	public function doctor( array $args, array $assoc_args ): void {
		$this->requireCapability( CapabilityManager::MANAGE_SETTINGS );
		unset( $args );

		$migrator      = new Migrator();
		$table_health  = ( new TableHealth() )->summary();
		$requirements  = ( new Requirements() )->errors();
		$queue_status  = null === $this->queue ? array() : $this->queue->status()->toArray();
		$provider_info = null === $this->providers
			? array(
				'loaded' => false,
				'count'  => 0,
				'errors' => array( 'Provider manager unavailable.' ),
			)
			: array(
				'loaded' => $this->providers->loaded(),
				'count'  => $this->providers->providers()->count(),
				'errors' => $this->providers->errors(),
			);

		$doctor = array(
			'healthy'                           => array() === $requirements
				&& (int) $table_health['existing'] === (int) $table_health['expected']
				&& (bool) ( $queue_status['healthy'] ?? false )
				&& true === $provider_info['loaded'],
			'requirement_errors'                => $requirements,
			'installed_schema'                  => $migrator->currentVersion(),
			'required_schema'                   => $migrator->latestVersion(),
			'database_tables_existing'          => $table_health['existing'],
			'database_tables_expected'          => $table_health['expected'],
			'database_tables_missing'           => $table_health['missing'],
			'database_transaction_capable'      => (bool) ( $table_health['transaction_capable'] ?? false ),
			'database_non_transactional_tables' => $table_health['non_transactional'] ?? array(),
			'database_table_engines'            => $table_health['tables'] ?? array(),
			'queue'                             => $queue_status,
			'providers'                         => $provider_info,
			'active_recordings'                 => $this->recordings->activeCount(),
			'stale_recordings'                  => $this->recordings->staleCount(),
			'orphaned_workspaces'               => $this->recordings->orphanedWorkspaceCount(),
		);

		$this->formatAssoc(
			PublicHooks::cliDoctorPayload( $doctor, $this->providers ),
			$assoc_args
		);
	}

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

		if ( null === $launch ) {
			\WP_CLI::error( 'Launch was not found or is not available to the current actor.' );
		}

		return $launch;
	}

	/**
	 * Returns a version only when every target is visible to the current actor.
	 */
	private function visibleVersionForLaunch( Launch $launch ): ?LaunchVersion {
		$version = $this->resolveVersionForLaunch( $launch );

		return null !== $version && $this->canInspectVersion( $version )
			? $version
			: null;
	}

	/**
	 * Checks every provider-owned target in a version.
	 */
	private function canInspectVersion( LaunchVersion $version ): bool {
		$actor_id = $this->actorId();

		foreach ( $version->plan()->graph()->targets() as $target ) {
			if ( ! $this->authorization->canInspect( $actor_id, $target ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Resolves the current version for a launch without emitting CLI output.
	 */
	private function resolveVersionForLaunch( Launch $launch ): ?LaunchVersion {
		$version_id = $launch->currentVersionId();

		return null === $version_id ? null : $this->versions->findById( $version_id );
	}

	/**
	 * Returns the provider manager or exits with an error.
	 */
	private function providersOrFail(): ProviderManager {
		if ( null === $this->providers ) {
			\WP_CLI::error( 'Provider manager is unavailable.' );
		}

		return $this->providers;
	}

	/**
	 * @return array<string,mixed>
	 */
	private function migrationStatus( Migrator $migrator ): array {
		return array(
			'installed_schema' => $migrator->currentVersion(),
			'required_schema'  => $migrator->latestVersion(),
			'status'           => $migrator->status(),
		);
	}

	/**
	 * Returns a bounded row limit.
	 *
	 * @param array<string,mixed> $assoc_args Associative arguments.
	 */
	private function limit( array $assoc_args ): int {
		return max( 1, min( 100, (int) ( $assoc_args['limit'] ?? 25 ) ) );
	}

	/**
	 * Emits a usage error.
	 *
	 * @return void
	 */
	private function usage( string $message ) {
		\WP_CLI::error( $message );
	}
}
