<?php
/**
 * Declarative Extension catalog.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Extension;

/** Resolves distributable packages separately from runtime modules. */
final class ExtensionRegistry {
	private BundledExtensionCatalog|DevelopmentBundledExtensionCatalog $bundled;
	/** @var ExtensionDefinition[]|null */
	private ?array $definitions = null;
	/** @var string[] */
	private array $definition_errors = array();

	public function __construct( BundledExtensionCatalog|DevelopmentBundledExtensionCatalog|null $bundled = null ) {
		$this->bundled = $bundled ?? new BundledExtensionCatalog();
	}

	/** @return ExtensionDefinition[] */
	public function definitions(): array {
		if ( null !== $this->definitions ) {
			return $this->definitions;
		}

		$this->definition_errors = array();
		$definitions             = array();
		foreach ( PublicHooks::extensionDefinitions( $this->bundled->definitions() ) as $index => $candidate ) {
			try {
				$definition = $candidate instanceof ExtensionDefinition
					? $candidate
					: ( is_array( $candidate ) ? ExtensionDefinition::fromArray( $candidate ) : null );
			} catch ( \Throwable $exception ) {
				$this->definition_errors[] = sprintf(
					'Extension registration failed for %1$s: %2$s',
					$this->candidateLabel( $candidate, (int) $index ),
					$this->diagnosticMessage( $exception )
				);
				continue;
			}
			if ( null === $definition ) {
				$this->definition_errors[] = sprintf(
					'Extension registration failed for %s: expected ExtensionDefinition or array manifest.',
					$this->candidateLabel( $candidate, (int) $index )
				);
				continue;
			}

			$definitions[] = $definition;
		}

		$this->definitions = $definitions;

		return $this->definitions;
	}

	public function catalogReport( string $profile = '' ): ExtensionCatalogReport {
		$resolver    = new ModuleProfileResolver();
		$profile     = '' === $profile ? $resolver->currentProfile() : $resolver->normalizeProfile( $profile );
		$definitions = $this->definitions();
		$by_id       = array();
		$errors      = $this->definition_errors;
		foreach ( $definitions as $definition ) {
			if ( isset( $by_id[ $definition->id() ] ) ) {
				$errors[] = 'Duplicate Extension ID: ' . $definition->id();
				continue;
			}
			$by_id[ $definition->id() ] = $definition;
		}

		$included = array_filter(
			$by_id,
			static fn ( ExtensionDefinition $definition ): bool => ExtensionDefinition::VISIBILITY_HIDDEN !== $definition->visibility()
		);
		$selected = PublicHooks::extensionProfileSelection( array_keys( $included ), $profile, array_values( $included ) );
		$included = array_filter(
			$included,
			static fn ( ExtensionDefinition $definition ): bool => in_array( $definition->id(), $selected, true )
		);
		ksort( $included );

		return new ExtensionCatalogReport( $included, $errors );
	}

	/** @return ExtensionDefinition[] */
	public function visibleDefinitions( string $profile = '' ): array {
		$report = $this->catalogReport( $profile );

		return array_values(
			array_filter(
				$report->definitions(),
				static fn ( ExtensionDefinition $definition ): bool => ExtensionDefinition::VISIBILITY_PUBLIC === $definition->visibility()
			)
		);
	}

	/**
	 * @param mixed $candidate
	 */
	private function candidateLabel( $candidate, int $index ): string {
		if ( $candidate instanceof ExtensionDefinition ) {
			return 'Extension ' . $candidate->id();
		}

		if ( is_array( $candidate ) ) {
			foreach ( array( 'id', 'name' ) as $key ) {
				if ( is_scalar( $candidate[ $key ] ?? null ) && '' !== trim( (string) $candidate[ $key ] ) ) {
					return $key . ' ' . $this->diagnosticValue( (string) $candidate[ $key ] );
				}
			}
		}

		return 'candidate #' . $index;
	}

	private function diagnosticMessage( \Throwable $throwable ): string {
		return $this->diagnosticValue( $throwable->getMessage() );
	}

	private function diagnosticValue( string $value ): string {
		return trim( (string) preg_replace( '/[\r\n\t]+/', ' ', $value ) );
	}
}
