<?php
/**
 * Adapter status reporting service.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Adapter;

use ScheduledChanges\Extension\ModuleDefinition;
use ScheduledChanges\Extension\ModuleRegistry;
use ScheduledChanges\Extension\ExtensionDefinition;
use ScheduledChanges\Extension\ExtensionRegistry;
use ScheduledChanges\Extension\ModuleProfileResolver;
use ScheduledChanges\Extension\PublicHooks;
use ScheduledChanges\Field\OwnershipCollision;
use ScheduledChanges\Provider\FieldProvider;
use ScheduledChanges\Provider\ObjectContext;
use ScheduledChanges\Provider\ProviderManager;
use ScheduledChanges\Support\ThrowableDiagnostic;

/**
 * Builds redacted official-adapter and collision status rows.
 */
final class AdapterStatusService {
	private ?OfficialAdapterCatalog $catalog = null;
	/**
	 * @var ModuleRegistry|\Closure|null
	 */
	private $modules                 = null;
	private const REQUIRED_LIFECYCLE = array(
		'save',
		'validation',
		'recording',
		'diff',
		'snapshot',
		'rollback',
		'cleanup',
	);

	/**
	 * Signal resolver callback.
	 *
	 * @var \Closure
	 */
	private \Closure $signal_resolver;

	/**
	 * Creates the status service.
	 *
	 * @param OfficialAdapterCatalog|null  $catalog Adapter catalog.
	 * @param ModuleRegistry|\Closure|null $modules Module manifest registry, or legacy test signal resolver.
	 * @param \Closure|null                $signal_resolver Optional signal resolver for tests.
	 */
	public function __construct(
		?OfficialAdapterCatalog $catalog = null,
		$modules = null,
		?\Closure $signal_resolver = null
	) {
		$this->catalog = $catalog;
		$this->modules = $modules;
		if ( $this->modules instanceof \Closure && null === $signal_resolver ) {
			$signal_resolver = $this->modules;
		}

		$this->signal_resolver = $signal_resolver ?? \Closure::fromCallable( array( $this, 'defaultSignalResolver' ) );
	}

	/**
	 * Returns official adapter status rows.
	 *
	 * @param ProviderManager|null $providers Provider manager.
	 * @return array<int,array<string,mixed>>
	 */
	public function adapterRows( ?ProviderManager $providers ): array {
		$versions = null === $providers ? array() : $providers->providers()->versions();
		$rows     = array();

		foreach ( $this->catalog()->definitions() as $definition ) {
			$evidence        = $this->matchingSignals( $definition );
			$module          = $this->moduleFor( $definition );
			$provider_id     = $module instanceof ModuleDefinition ? $module->providerId() : $definition->providerId();
			$provider_loaded = isset( $versions[ $provider_id ] );
			$lifecycle       = $this->lifecycleCertification( $provider_loaded, $module );
			$status          = $this->status( $definition, $provider_loaded, $evidence, $module, $lifecycle['status'] );
			$verified        = $module instanceof ModuleDefinition ? $module->verifiedLifecycle() : array();
			$complete        = ModuleDefinition::LIFECYCLE_COMPLETE === $lifecycle['status'];

			$rows[] = array(
				'slug'                        => $definition->slug(),
				'name'                        => $definition->name(),
				'priority'                    => $definition->priority(),
				'providerId'                  => $provider_id,
				'providerVersion'             => $provider_loaded ? (string) $versions[ $provider_id ] : '',
				'status'                      => $status,
				'statusLabel'                 => $this->statusLabel( $status ),
				'detected'                    => array() !== $evidence,
				'evidence'                    => $evidence,
				'coveredByProviderId'         => $definition->coveredByProviderId() ?? '',
				'requiredLifecycle'           => self::REQUIRED_LIFECYCLE,
				'verifiedLifecycle'           => $verified,
				'missingLifecycle'            => $lifecycle['missing'],
				'lifecycleStatus'             => $lifecycle['status'],
				'lifecycleLabel'              => $this->lifecycleLabel( $lifecycle['status'] ),
				'lifecycleMessage'            => $lifecycle['message'],
				'advertisedComplete'          => $complete,
				'certificationRequired'       => $module instanceof ModuleDefinition && $module->certificationRequired(),
				'certificationId'             => $module instanceof ModuleDefinition ? $module->certificationId() : '',
				'certificationValid'          => $module instanceof ModuleDefinition && $module->certificationValid(),
				'certificationResultStatus'   => $module instanceof ModuleDefinition ? $module->certificationResultStatus() : '',
				'certificationCommands'       => $module instanceof ModuleDefinition ? $module->certificationCommands() : array(),
				'certificationPassedCommands' => $module instanceof ModuleDefinition ? $module->certificationPassedCommands() : array(),
				'certificationEvidence'       => $module instanceof ModuleDefinition ? $module->certificationEvidence() : array(),
				'moduleSlug'                  => $module instanceof ModuleDefinition ? $module->slug() : '',
				'moduleName'                  => $module instanceof ModuleDefinition ? $module->name() : '',
				'moduleVersion'               => $module instanceof ModuleDefinition ? $module->version() : '',
				'extensionPluginFile'         => $module instanceof ModuleDefinition ? $module->pluginFile() : '',
				'coverageSummary'             => $this->coverageSummary( $status ),
				'notes'                       => $definition->notes(),
			);
		}

		return $rows;
	}

	/**
	 * Returns provider ownership collision rows.
	 *
	 * @param ProviderManager|null $providers Provider manager.
	 * @return array<int,array<string,mixed>>
	 */
	public function collisionRows( ?ProviderManager $providers ): array {
		if ( null === $providers ) {
			return array();
		}

		$rows = array();

		foreach ( array_values( $providers->fields()->lastCollisions() ) as $index => $collision ) {
			$rows[] = array(
				'fieldId'            => $collision->fieldId(),
				'providers'          => $collision->providerIds(),
				'selectedProviderId' => $collision->selectedProviderId(),
				'blocking'           => $collision->blocking(),
				'nextAction'         => $collision->blocking()
					? __( 'Choose one compatibility layer to own this field before scheduling affected changes.', 'jjs-post-scheduler' )
					: __( 'Review the selected field owner before scheduling affected changes.', 'jjs-post-scheduler' ),
				'supportReportPath'  => 'provider_diagnostics.ownershipCollisions[' . $index . ']',
			);
		}

		return $rows;
	}

	/**
	 * Returns redacted provider and module diagnostics for support.
	 *
	 * @param ProviderManager|null $providers Provider manager.
	 * @return array<string,mixed>
	 */
	public function platformDiagnostics( ?ProviderManager $providers ): array {
		$adapter_rows = $this->adapterRows( $providers );
		$catalog      = $this->modules()->catalogReport();

		return array(
			'providers'            => array(
				'loaded' => null !== $providers && $providers->loaded(),
				'rows'   => $this->providerRows( $providers ),
				'errors' => null === $providers ? array() : $this->redactedProviderErrors( $providers->errors() ),
			),
			'modules'              => $this->moduleRows(),
			'moduleCatalog'        => array(
				'profile' => ( new ModuleProfileResolver() )->currentProfile(),
				'healthy' => $catalog->healthy(),
				'errors'  => $catalog->errors(),
				'rows'    => $this->moduleCatalogRows(),
			),
			'adapters'             => $adapter_rows,
			'lifecycleSummary'     => $this->lifecycleSummary( $adapter_rows ),
			'ownershipCollisions'  => $this->collisionRows( $providers ),
			'cloneLifecycle'       => array(
				'strategyHook'   => 'scheduled_changes_recording_clone_strategies',
				'afterClone'     => true,
				'cleanup'        => true,
				'cleanupReasons' => array( 'clone_failed', 'finalized', 'abandoned', 'expired' ),
			),
			'launchProviderPolicy' => array(
				'capturesProviderVersions' => true,
				'blocksMissingProviders'   => true,
				'blocksVersionDrift'       => true,
			),
		);
	}

	/**
	 * Returns compact feature diagnostics for the admin app.
	 *
	 * @param ProviderManager|null $providers Provider manager.
	 * @return array<int,array<string,mixed>>
	 */
	public function featureDiagnosticRows( ?ProviderManager $providers ): array {
		$adapter_rows    = $this->adapterRows( $providers );
		$provider_rows   = $this->providerRows( $providers );
		$provider_errors = null === $providers ? array() : $this->redactedProviderErrors( $providers->errors() );
		$collisions      = $this->collisionRows( $providers );
		$providers_by_id = array();
		$used_provider   = array();
		$rows            = array();

		foreach ( $provider_rows as $provider_row ) {
			$provider_id = (string) ( $provider_row['id'] ?? '' );
			if ( '' !== $provider_id ) {
				$providers_by_id[ $provider_id ] = $provider_row;
			}
		}

		foreach ( $adapter_rows as $adapter_row ) {
			$provider_id = (string) ( $adapter_row['providerId'] ?? '' );
			if ( '' !== $provider_id ) {
				$used_provider[ $provider_id ] = true;
			}

			$rows[] = $this->featureDiagnosticRow(
				$adapter_row,
				$providers_by_id[ $provider_id ] ?? null,
				$collisions,
				$provider_errors
			);
		}

		foreach ( $providers_by_id as $provider_id => $provider_row ) {
			if ( isset( $used_provider[ $provider_id ] ) ) {
				continue;
			}

			$rows[] = $this->providerOnlyFeatureDiagnosticRow( $provider_row, $collisions, $provider_errors );
		}

		return $rows;
	}

	/**
	 * Returns provider errors without credentials, local paths, or stack context.
	 *
	 * @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
			)
		);
	}

	/**
	 * Returns redacted provider rows.
	 *
	 * @param ProviderManager|null $providers Provider manager.
	 * @return array<int,array<string,mixed>>
	 */
	public function providerRows( ?ProviderManager $providers ): array {
		if ( null === $providers ) {
			return array();
		}

		$rows     = array();
		$contexts = PublicHooks::providerDiagnosticContexts( array(), $providers );

		foreach ( $providers->providers()->all() as $provider ) {
			$rows[] = array(
				'id'       => $provider->id(),
				'version'  => $provider->version(),
				'category' => $provider->category(),
				'fields'   => $this->providerFieldRows( $provider, $contexts ),
			);
		}

		return $rows;
	}

	/**
	 * Returns registered module manifest rows.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function moduleRows(): array {
		return array_map(
			static fn ( ModuleDefinition $module ): array => array(
				'slug'                        => $module->slug(),
				'name'                        => $module->name(),
				'version'                     => $module->version(),
				'adapterSlug'                 => $module->adapterSlug(),
				'providerId'                  => $module->providerId(),
				'hasPluginFile'               => '' !== $module->pluginFile(),
				'declaredLifecycleStatus'     => $module->declaredLifecycleStatus(),
				'lifecycleMessage'            => $module->lifecycleMessage(),
				'verifiedLifecycle'           => $module->verifiedLifecycle(),
				'certificationRequired'       => $module->certificationRequired(),
				'certificationId'             => $module->certificationId(),
				'certificationValid'          => $module->certificationValid(),
				'certificationResultStatus'   => $module->certificationResultStatus(),
				'certificationCommands'       => $module->certificationCommands(),
				'certificationPassedCommands' => $module->certificationPassedCommands(),
				'certificationEvidence'       => $module->certificationEvidence(),
			),
			$this->modules()->definitions()
		);
	}

	/**
	 * Returns the profile-selected module catalog in a Modules page compatible shape.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function moduleCatalogRows( string $profile = '' ): array {
		$resolver = new ModuleProfileResolver();
		$profile  = '' === $profile ? $resolver->currentProfile() : $resolver->normalizeProfile( $profile );
		$report   = $this->modules()->catalogReport();
		$rows   = array();

		foreach ( $this->modules()->visibleCatalogDefinitions() as $index => $module ) {
			$rows[] = array(
				'slug'                        => $module->slug(),
				'name'                        => $module->name(),
				'priority'                    => $index,
				'providerId'                  => $module->providerId(),
				'providerVersion'             => '',
				'status'                      => ModuleDefinition::LIFECYCLE_COMPLETE === $module->declaredLifecycleStatus()
					? 'module_supported'
					: 'module_lifecycle_unknown',
				'statusLabel'                 => ModuleDefinition::LIFECYCLE_COMPLETE === $module->declaredLifecycleStatus()
					? $this->statusLabel( 'module_supported' )
					: $this->statusLabel( 'module_lifecycle_unknown' ),
				'detected'                    => true,
				'evidence'                    => array( $module->packageForm() ),
				'coveredByProviderId'         => '',
				'requiredLifecycle'           => $module->capabilities(),
				'verifiedLifecycle'           => $module->capabilities(),
				'missingLifecycle'            => array(),
				'lifecycleStatus'             => $module->declaredLifecycleStatus(),
				'lifecycleLabel'              => $this->lifecycleLabel( $module->declaredLifecycleStatus() ),
				'lifecycleMessage'            => $module->lifecycleMessage(),
				'advertisedComplete'          => ModuleDefinition::LIFECYCLE_COMPLETE === $module->declaredLifecycleStatus(),
				'certificationRequired'       => $module->certificationRequired(),
				'certificationId'             => $module->certificationId(),
				'certificationValid'          => $module->certificationValid(),
				'certificationResultStatus'   => $module->certificationResultStatus(),
				'certificationCommands'       => $module->certificationCommands(),
				'certificationPassedCommands' => $module->certificationPassedCommands(),
				'certificationEvidence'       => $module->certificationEvidence(),
				'moduleSlug'                  => $module->slug(),
				'moduleName'                  => $module->name(),
				'moduleVersion'               => $module->version(),
				'extensionPluginFile'         => $module->pluginFile(),
				'coverageSummary'             => $this->moduleCoverageSummary( $module ),
				'notes'                       => $module->documentationUrl(),
				'description'                 => $module->description(),
				'dependencies'                => $module->dependencies(),
				'capabilities'                => $module->capabilities(),
				'resourceOwnership'           => $module->resourceOwnership(),
				'packageForm'                 => $module->packageForm(),
				'visibility'                  => $module->visibility(),
				'sourcePackage'               => $module->sourcePackage(),
				'group'                       => $module->group(),
				'profile'                     => $profile,
				'catalogHealthy'              => $report->healthy(),
				'catalogErrors'               => $report->errors(),
			);
		}

		return $rows;
	}

	/**
	 * Returns profile-selected distributable Extensions for the catalog page.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function extensionCatalogRows( string $profile = '' ): array {
		$resolver = new ModuleProfileResolver();
		$profile  = '' === $profile ? $resolver->currentProfile() : $resolver->normalizeProfile( $profile );
		$registry = new ExtensionRegistry();
		$report   = $registry->catalogReport();

		return array_map(
			static fn ( ExtensionDefinition $extension ): array => array(
				'slug'                => $extension->id(),
				'name'                => $extension->name(),
				'priority'            => -1,
				'providerId'          => '',
				'providerVersion'     => '',
				'status'              => 'module_supported',
				'statusLabel'         => __( 'Available', 'jjs-post-scheduler' ),
				'detected'            => true,
				'evidence'            => array( sprintf( '%d modules', count( $extension->modules() ) ) ),
				'coveredByProviderId' => '',
				'requiredLifecycle'   => $extension->modules(),
				'verifiedLifecycle'   => $extension->modules(),
				'advertisedComplete'  => $report->healthy(),
				'moduleSlug'          => '',
				'moduleName'          => $extension->name(),
				'moduleVersion'       => $extension->version(),
				'extensionPluginFile' => $extension->pluginFile(),
				'coverageSummary'     => $extension->description(),
				'description'         => $extension->description(),
				'notes'               => $extension->documentationUrl(),
				'kind'                => 'extension',
				'bundledModules'      => $extension->modules(),
				'profile'             => $profile,
				'catalogHealthy'      => $report->healthy(),
				'catalogErrors'       => $report->errors(),
			),
			$registry->visibleDefinitions()
		);
	}

	/**
	 * Returns the configured catalog.
	 */
	private function catalog(): OfficialAdapterCatalog {
		return $this->catalog ?? new OfficialAdapterCatalog();
	}

	/**
	 * Returns the configured module registry.
	 */
	private function modules(): ModuleRegistry {
		return $this->modules instanceof ModuleRegistry ? $this->modules : new ModuleRegistry();
	}

	/**
	 * Returns a human-readable catalog coverage summary.
	 */
	private function moduleCoverageSummary( ModuleDefinition $module ): string {
		if ( '' !== $module->description() ) {
			return $module->description();
		}

		if ( ModuleDefinition::PACKAGE_SEPARATE_PLUGIN === $module->packageForm() ) {
			return __( 'Separate-plugin module registered through the Scheduled Changes module catalog.', 'jjs-post-scheduler' );
		}

		return __( 'Bundled module selected by the active Scheduled Changes distribution profile.', 'jjs-post-scheduler' );
	}

	/**
	 * Returns the registered module for an adapter target.
	 *
	 * @param OfficialAdapterDefinition $definition Adapter target definition.
	 */
	private function moduleFor( OfficialAdapterDefinition $definition ): ?ModuleDefinition {
		return $this->modules()->forAdapterSlug( $definition->slug() );
	}

	/**
	 * Returns safe field metadata for module-provided diagnostic contexts.
	 *
	 * @param ObjectContext[] $contexts Provider diagnostic contexts.
	 * @return array<int,array<string,mixed>>
	 */
	private function providerFieldRows( FieldProvider $provider, array $contexts ): array {
		$fields = array();

		foreach ( $contexts as $context ) {
			if ( ! $provider->supports( $context ) ) {
				continue;
			}

			try {
				foreach ( $provider->discover( $context ) as $field ) {
					$fields[ $field->id() ] = array(
						'id'           => $field->id(),
						'label'        => $field->label(),
						'scope'        => $field->scope(),
						'productTypes' => $field->productTypes(),
						'writable'     => $field->writable(),
						'schedulable'  => $field->schedulable(),
						'reversible'   => $field->reversible(),
						'sensitive'    => $field->sensitive(),
						'display'      => $this->fieldDisplayDiagnostics( $field->display() ),
					);
				}
			} catch ( \Throwable $throwable ) {
				$fields[ 'diagnostic-error:' . $context->targetType() . ':' . $context->subtype() ] = array(
					'id'           => '',
					'label'        => __( 'Discovery unavailable for diagnostic context', 'jjs-post-scheduler' ),
					'scope'        => array( $context->targetType() ),
					'productTypes' => array( $context->subtype() ),
					'error'        => ThrowableDiagnostic::redactMessage( $throwable->getMessage() ),
				);
			}
		}

		ksort( $fields );

		return array_values( $fields );
	}

	/**
	 * @param array<string,mixed>            $adapter_row Adapter diagnostic row.
	 * @param array<string,mixed>|null       $provider_row Provider diagnostic row.
	 * @param array<int,array<string,mixed>> $collisions Ownership collision rows.
	 * @param string[]                       $provider_errors Redacted provider errors.
	 * @return array<string,mixed>
	 */
	private function featureDiagnosticRow( array $adapter_row, ?array $provider_row, array $collisions, array $provider_errors ): array {
		$slug             = (string) ( $adapter_row['slug'] ?? $adapter_row['moduleSlug'] ?? $adapter_row['name'] ?? 'feature' );
		$provider_id      = (string) ( $adapter_row['providerId'] ?? '' );
		$field_errors     = $this->providerFieldErrorMessages( $provider_row );
		$conflict_count   = $this->ownershipConflictCount( $provider_id, $collisions );
		$error_count      = count( $field_errors ) + ( null === $provider_row ? 0 : count( $provider_errors ) );
		$messages         = $this->featureDiagnosticMessages( $adapter_row, $field_errors, null === $provider_row ? array() : $provider_errors );
		$provider_version = (string) ( $adapter_row['providerVersion'] ?? '' );

		if ( '' === $provider_version && null !== $provider_row ) {
			$provider_version = (string) ( $provider_row['version'] ?? '' );
		}

		if ( '' === $provider_version ) {
			$provider_version = (string) ( $adapter_row['moduleVersion'] ?? '' );
		}

		return array(
			'id'                     => $slug,
			'supportedArea'          => (string) ( $adapter_row['name'] ?? $adapter_row['moduleName'] ?? $slug ),
			'status'                 => $this->featureDiagnosticStatusLabel( $adapter_row ),
			'statusCode'             => (string) ( $adapter_row['status'] ?? '' ),
			'featureSource'          => $this->featureSourceLabel( $adapter_row ),
			'providerId'             => $provider_id,
			'providerVersion'        => $provider_version,
			'lifecycleHealth'        => (string) ( $adapter_row['lifecycleLabel'] ?? $adapter_row['lifecycleStatus'] ?? __( 'Unknown', 'jjs-post-scheduler' ) ),
			'lifecycleStatus'        => (string) ( $adapter_row['lifecycleStatus'] ?? '' ),
			'fieldCount'             => $this->providerFieldCount( $provider_row ),
			'errorCount'             => $error_count,
			'ownershipConflictCount' => $conflict_count,
			'diagnosticMessages'     => $messages,
			'nextAction'             => $this->featureDiagnosticNextAction( $adapter_row, $conflict_count, $error_count ),
			'supportReportPath'      => 'provider_diagnostics.adapters.' . $slug,
		);
	}

	/**
	 * @param array<string,mixed>            $provider_row Provider diagnostic row.
	 * @param array<int,array<string,mixed>> $collisions Ownership collision rows.
	 * @param string[]                       $provider_errors Redacted provider errors.
	 * @return array<string,mixed>
	 */
	private function providerOnlyFeatureDiagnosticRow( array $provider_row, array $collisions, array $provider_errors ): array {
		$provider_id    = (string) ( $provider_row['id'] ?? 'provider' );
		$field_errors   = $this->providerFieldErrorMessages( $provider_row );
		$conflict_count = $this->ownershipConflictCount( $provider_id, $collisions );
		$error_count    = count( $field_errors ) + count( $provider_errors );
		$messages       = array_slice( array_values( array_unique( array_merge( $field_errors, $provider_errors ) ) ), 0, 5 );

		return array(
			'id'                     => 'provider-' . $provider_id,
			'supportedArea'          => __( 'Additional provider diagnostics', 'jjs-post-scheduler' ),
			'status'                 => __( 'Diagnostics only', 'jjs-post-scheduler' ),
			'statusCode'             => 'read_only_discovery',
			'featureSource'          => $provider_id,
			'providerId'             => $provider_id,
			'providerVersion'        => (string) ( $provider_row['version'] ?? '' ),
			'lifecycleHealth'        => __( 'Provider loaded', 'jjs-post-scheduler' ),
			'lifecycleStatus'        => 'loaded',
			'fieldCount'             => $this->providerFieldCount( $provider_row ),
			'errorCount'             => $error_count,
			'ownershipConflictCount' => $conflict_count,
			'diagnosticMessages'     => $messages,
			'nextAction'             => $error_count > 0 || $conflict_count > 0
				? __( 'Download the support report and review this provider before scheduling affected fields.', 'jjs-post-scheduler' )
				: __( 'No action needed.', 'jjs-post-scheduler' ),
			'supportReportPath'      => 'provider_diagnostics.providers.rows.' . $provider_id,
		);
	}

	/**
	 * @param array<string,mixed> $adapter_row Adapter diagnostic row.
	 */
	private function featureSourceLabel( array $adapter_row ): string {
		$provider_id = (string) ( $adapter_row['providerId'] ?? '' );
		if ( '' !== $provider_id ) {
			return $provider_id;
		}

		$module_slug = (string) ( $adapter_row['moduleSlug'] ?? '' );
		if ( '' !== $module_slug ) {
			return $module_slug;
		}

		return (string) ( $adapter_row['slug'] ?? __( 'Unavailable', 'jjs-post-scheduler' ) );
	}

	/**
	 * @param array<string,mixed>|null $provider_row Provider diagnostic row.
	 */
	private function providerFieldCount( ?array $provider_row ): int {
		return count( $this->providerFieldRowsFromDiagnostics( $provider_row ) );
	}

	/**
	 * @param array<string,mixed>|null $provider_row Provider diagnostic row.
	 * @return string[]
	 */
	private function providerFieldErrorMessages( ?array $provider_row ): array {
		$messages = array();

		foreach ( $this->providerFieldRowsFromDiagnostics( $provider_row ) as $field ) {
			$message = (string) ( $field['error'] ?? '' );
			if ( '' !== $message ) {
				$messages[] = ThrowableDiagnostic::redactMessage( $message );
			}
		}

		return array_values( array_unique( $messages ) );
	}

	/**
	 * @param array<string,mixed>|null $provider_row Provider diagnostic row.
	 * @return array<int,array<string,mixed>>
	 */
	private function providerFieldRowsFromDiagnostics( ?array $provider_row ): array {
		$fields = is_array( $provider_row['fields'] ?? null ) ? $provider_row['fields'] : array();

		return array_values(
			array_filter(
				$fields,
				static fn ( $field ): bool => is_array( $field )
			)
		);
	}

	/**
	 * @param array<string,mixed> $adapter_row Adapter diagnostic row.
	 * @param string[]            $field_errors Redacted field discovery errors.
	 * @param string[]            $provider_errors Redacted provider errors.
	 * @return string[]
	 */
	private function featureDiagnosticMessages( array $adapter_row, array $field_errors, array $provider_errors ): array {
		$messages          = array();
		$lifecycle_message = ThrowableDiagnostic::redactMessage( (string) ( $adapter_row['lifecycleMessage'] ?? '' ) );

		if ( '' !== $lifecycle_message ) {
			$messages[] = $lifecycle_message;
		}

		$missing = array_values(
			array_filter(
				array_map( 'strval', is_array( $adapter_row['missingLifecycle'] ?? null ) ? $adapter_row['missingLifecycle'] : array() )
			)
		);

		if ( array() !== $missing ) {
			/* translators: %s is a comma-separated list of missing feature-health checks. */
			$messages[] = sprintf( __( 'Missing feature-health checks: %s.', 'jjs-post-scheduler' ), implode( ', ', $missing ) );
		}

		$messages = array_merge( $messages, $field_errors, $provider_errors );

		return array_slice(
			array_values(
				array_unique(
					array_filter(
						array_map( 'strval', $messages )
					)
				)
			),
			0,
			5
		);
	}

	/**
	 * @param string                         $provider_id Provider ID.
	 * @param array<int,array<string,mixed>> $collisions Ownership collision rows.
	 */
	private function ownershipConflictCount( string $provider_id, array $collisions ): int {
		if ( '' === $provider_id ) {
			return 0;
		}

		$count = 0;

		foreach ( $collisions as $collision ) {
			$provider_ids = is_array( $collision['providers'] ?? null ) ? $collision['providers'] : array();
			if (
				in_array( $provider_id, array_map( 'strval', $provider_ids ), true )
				|| (string) ( $collision['selectedProviderId'] ?? '' ) === $provider_id
			) {
				++$count;
			}
		}

		return $count;
	}

	/**
	 * @param array<string,mixed> $adapter_row Adapter diagnostic row.
	 */
	private function featureDiagnosticStatusLabel( array $adapter_row ): string {
		$status = (string) ( $adapter_row['status'] ?? '' );

		switch ( $status ) {
			case 'module_supported':
				return __( 'Available', 'jjs-post-scheduler' );
			case 'module_lifecycle_partial':
				return __( 'Limited support', 'jjs-post-scheduler' );
			case 'module_lifecycle_missing':
				return __( 'Not supported yet', 'jjs-post-scheduler' );
			case 'read_only_discovery':
				return __( 'Diagnostics only', 'jjs-post-scheduler' );
			case 'module_incompatible':
				return __( 'Needs review', 'jjs-post-scheduler' );
			case 'future_module_planned':
				return __( 'Planned', 'jjs-post-scheduler' );
			default:
				return (string) ( $adapter_row['statusLabel'] ?? $this->statusLabel( $status ) );
		}
	}

	/**
	 * @param array<string,mixed> $adapter_row Adapter diagnostic row.
	 */
	private function featureDiagnosticNextAction( array $adapter_row, int $ownership_conflicts, int $error_count ): string {
		if ( $ownership_conflicts > 0 ) {
			return __( 'Download the support report and choose one owner before scheduling fields in this area.', 'jjs-post-scheduler' );
		}

		if ( $error_count > 0 ) {
			return __( 'Download the support report and review provider errors before scheduling this area.', 'jjs-post-scheduler' );
		}

		switch ( (string) ( $adapter_row['status'] ?? '' ) ) {
			case 'module_supported':
				return __( 'No action needed.', 'jjs-post-scheduler' );
			case 'module_lifecycle_partial':
				return __( 'Review support details before scheduling affected fields.', 'jjs-post-scheduler' );
			case 'module_incompatible':
				return __( 'Update the compatibility extension or provider before scheduling this area.', 'jjs-post-scheduler' );
			case 'read_only_discovery':
				return __( 'Use this area for diagnostics only; Scheduled Changes will not write these fields.', 'jjs-post-scheduler' );
			case 'module_lifecycle_missing':
				return __( 'Activate a compatible extension before scheduling this area.', 'jjs-post-scheduler' );
			case 'future_module_planned':
				return __( 'No scheduling action is available yet.', 'jjs-post-scheduler' );
			default:
				return __( 'Download the support report before scheduling affected fields.', 'jjs-post-scheduler' );
		}
	}

	/**
	 * @param array<string,mixed> $display Field display metadata.
	 * @return array<string,mixed>
	 */
	private function fieldDisplayDiagnostics( array $display ): array {
		return array_intersect_key(
			$display,
			array_flip( array( 'group_key', 'group_label', 'provider_label', 'sort_order' ) )
		);
	}

	/**
	 * @param array<int,array<string,mixed>> $adapter_rows Adapter rows.
	 * @return array<string,int>
	 */
	private function lifecycleSummary( array $adapter_rows ): array {
		$summary = array(
			'complete'     => 0,
			'partial'      => 0,
			'missing'      => 0,
			'incompatible' => 0,
			'unknown'      => 0,
		);

		foreach ( $adapter_rows as $row ) {
			$status = (string) ( $row['lifecycleStatus'] ?? ModuleDefinition::LIFECYCLE_UNKNOWN );

			if ( ! isset( $summary[ $status ] ) ) {
				$status = ModuleDefinition::LIFECYCLE_UNKNOWN;
			}

			++$summary[ $status ];
		}

		return $summary;
	}

	/**
	 * Returns matching detection evidence labels.
	 *
	 * @param OfficialAdapterDefinition $definition Adapter definition.
	 * @return string[]
	 */
	private function matchingSignals( OfficialAdapterDefinition $definition ): array {
		$evidence = array();
		$resolver = $this->signal_resolver;

		foreach ( $definition->signals() as $signal ) {
			if ( $resolver( $signal ) ) {
				$evidence[] = (string) ( $signal['label'] ?? $signal['value'] ?? '' );
			}
		}

		return array_values( array_filter( $evidence ) );
	}

	/**
	 * Resolves a display status.
	 *
	 * @param OfficialAdapterDefinition $definition Adapter definition.
	 * @param bool                      $provider_loaded Whether the dedicated provider is loaded.
	 * @param string[]                  $evidence Matched detection labels.
	 * @param ModuleDefinition|null     $module Registered module manifest.
	 * @param string                    $lifecycle_status Lifecycle certification status.
	 */
	private function status(
		OfficialAdapterDefinition $definition,
		bool $provider_loaded,
		array $evidence,
		?ModuleDefinition $module,
		string $lifecycle_status
	): string {
		if ( ModuleDefinition::LIFECYCLE_COMPLETE === $lifecycle_status ) {
			return 'module_supported';
		}

		if ( $module instanceof ModuleDefinition ) {
			switch ( $lifecycle_status ) {
				case ModuleDefinition::LIFECYCLE_INCOMPATIBLE:
					return 'module_incompatible';
				case ModuleDefinition::LIFECYCLE_PARTIAL:
					return 'module_lifecycle_partial';
				case ModuleDefinition::LIFECYCLE_MISSING:
					return 'module_lifecycle_missing';
				default:
					return 'module_lifecycle_unknown';
			}
		}

		if ( $provider_loaded ) {
			return 'module_lifecycle_unknown';
		}

		if ( null !== $definition->coveredByProviderId() && array() !== $evidence ) {
			return 'read_only_discovery';
		}

		if ( array() !== $evidence ) {
			return 'module_lifecycle_missing';
		}

		return 'future_module_planned';
	}

	/**
	 * Returns lifecycle certification details for a registered module.
	 *
	 * @param bool                  $provider_loaded Whether the dedicated provider is loaded.
	 * @param ModuleDefinition|null $module Registered module manifest.
	 * @return array{status:string,missing:string[],message:string}
	 */
	private function lifecycleCertification( bool $provider_loaded, ?ModuleDefinition $module ): array {
		if ( ! $module instanceof ModuleDefinition ) {
			return array(
				'status'  => ModuleDefinition::LIFECYCLE_UNKNOWN,
				'missing' => self::REQUIRED_LIFECYCLE,
				'message' => '',
			);
		}

		$declared = $module->declaredLifecycleStatus();
		$missing  = array_values( array_diff( self::REQUIRED_LIFECYCLE, $module->verifiedLifecycle() ) );

		if ( ModuleDefinition::LIFECYCLE_INCOMPATIBLE === $declared ) {
			return array(
				'status'  => ModuleDefinition::LIFECYCLE_INCOMPATIBLE,
				'missing' => $missing,
				'message' => $module->lifecycleMessage(),
			);
		}

		if ( ! $provider_loaded || ModuleDefinition::LIFECYCLE_MISSING === $declared ) {
			return array(
				'status'  => ModuleDefinition::LIFECYCLE_MISSING,
				'missing' => $missing,
				'message' => $module->lifecycleMessage(),
			);
		}

		if ( $module->certificationRequired() && ! $module->certificationValid() ) {
			return array(
				'status'  => ModuleDefinition::LIFECYCLE_PARTIAL,
				'missing' => $missing,
				'message' => '' !== $module->lifecycleMessage()
					? $module->lifecycleMessage()
					: 'Lifecycle certification evidence is missing or invalid.',
			);
		}

		if ( $module->satisfiesLifecycle( self::REQUIRED_LIFECYCLE ) ) {
			return array(
				'status'  => ModuleDefinition::LIFECYCLE_COMPLETE,
				'missing' => array(),
				'message' => $module->lifecycleMessage(),
			);
		}

		if ( array() !== $module->verifiedLifecycle() || ModuleDefinition::LIFECYCLE_PARTIAL === $declared ) {
			return array(
				'status'  => ModuleDefinition::LIFECYCLE_PARTIAL,
				'missing' => $missing,
				'message' => $module->lifecycleMessage(),
			);
		}

		return array(
			'status'  => ModuleDefinition::LIFECYCLE_UNKNOWN,
			'missing' => $missing,
			'message' => $module->lifecycleMessage(),
		);
	}

	/**
	 * Returns a human-readable status label.
	 *
	 * @param string $status Machine status.
	 */
	private function statusLabel( string $status ): string {
		switch ( $status ) {
			case 'module_supported':
				return __( 'Module supported', 'jjs-post-scheduler' );
			case 'module_lifecycle_partial':
				return __( 'Module lifecycle partial', 'jjs-post-scheduler' );
			case 'module_lifecycle_missing':
				return __( 'Module lifecycle missing', 'jjs-post-scheduler' );
			case 'module_lifecycle_unknown':
				return __( 'Module lifecycle unknown', 'jjs-post-scheduler' );
			case 'module_incompatible':
				return __( 'Module incompatible', 'jjs-post-scheduler' );
			case 'read_only_discovery':
				return __( 'Read-only discovery', 'jjs-post-scheduler' );
			case 'unsupported':
				return __( 'Unsupported', 'jjs-post-scheduler' );
			case 'never_schedule':
				return __( 'Never schedule', 'jjs-post-scheduler' );
			default:
				return __( 'Addon needed', 'jjs-post-scheduler' );
		}
	}

	/**
	 * Returns a human-readable lifecycle label.
	 *
	 * @param string $status Lifecycle status.
	 */
	private function lifecycleLabel( string $status ): string {
		switch ( $status ) {
			case ModuleDefinition::LIFECYCLE_COMPLETE:
				return __( 'Complete', 'jjs-post-scheduler' );
			case ModuleDefinition::LIFECYCLE_PARTIAL:
				return __( 'Partial', 'jjs-post-scheduler' );
			case ModuleDefinition::LIFECYCLE_MISSING:
				return __( 'Missing', 'jjs-post-scheduler' );
			case ModuleDefinition::LIFECYCLE_INCOMPATIBLE:
				return __( 'Incompatible', 'jjs-post-scheduler' );
			default:
				return __( 'Unknown', 'jjs-post-scheduler' );
		}
	}

	/**
	 * Returns the status coverage summary.
	 *
	 * @param string $status Machine status.
	 */
	private function coverageSummary( string $status ): string {
		switch ( $status ) {
			case 'module_supported':
				return __( 'A separate compatibility module is installed, its provider is loaded, and all required lifecycle checks are declared certified.', 'jjs-post-scheduler' );
			case 'module_lifecycle_partial':
				return __( 'A compatibility module is installed, but it has not declared every required lifecycle check certified.', 'jjs-post-scheduler' );
			case 'module_lifecycle_missing':
				return __( 'This plugin-owned data requires a loaded compatibility module before Scheduled Changes core will record or schedule it.', 'jjs-post-scheduler' );
			case 'module_lifecycle_unknown':
				return __( 'A provider or module was detected, but lifecycle certification details are missing or unknown.', 'jjs-post-scheduler' );
			case 'module_incompatible':
				return __( 'A compatibility module reported that its lifecycle support is incompatible with this installation.', 'jjs-post-scheduler' );
			case 'read_only_discovery':
				return __( 'This data is detected for reporting only. Core does not schedule it unless an explicit compatibility module claims write ownership.', 'jjs-post-scheduler' );
			case 'unsupported':
				return __( 'This data is outside the core scheduling boundary and is not safe to schedule automatically.', 'jjs-post-scheduler' );
			case 'never_schedule':
				return __( 'Generated, cache, credential, cursor, or external sync data must never be scheduled by core.', 'jjs-post-scheduler' );
			default:
				return __( 'Future compatibility module target. No supported module is currently detected.', 'jjs-post-scheduler' );
		}
	}

	/**
	 * Resolves a signal against the current WordPress process.
	 *
	 * @param array<string,string> $signal Signal definition.
	 */
	private function defaultSignalResolver( array $signal ): bool {
		$type  = (string) ( $signal['type'] ?? '' );
		$value = (string) ( $signal['value'] ?? '' );

		switch ( $type ) {
			case 'class':
				return class_exists( $value, false );
			case 'function':
				return function_exists( $value );
			case 'constant':
				return defined( $value );
			case 'taxonomy':
				return function_exists( 'taxonomy_exists' ) && taxonomy_exists( $value );
			default:
				return false;
		}
	}
}
