<?php

declare(strict_types=1);

namespace AIPT\Utils\Credentials;

use AIPT\Utils\ProviderNameMapper;

/**
 * Singleton facade for API credential resolution.
 *
 * Builds the default request-level memoized chain in Connectors-first order, then
 * exposes it through a small service-locator facade. Third-party code may alter
 * the ordered provider list with the aipt_credential_provider_chain filter; the
 * filter must return an array of CredentialProviderInterface instances. reset()
 * exists only for unit-test isolation and must not be used by runtime code.
 *
 * Status metadata keeps references to the original Connectors and Local providers
 * as best-effort enrichment sources even when filters replace the active chain.
 *
 * @since 2.6.0
 */
final class CredentialResolver {

	/**
	 * Singleton instance for the current request.
	 *
	 * @since 2.6.0
	 * @var self|null
	 */
	private static ?self $instance = null;

	/**
	 * Active credential provider chain.
	 *
	 * @since 2.6.0
	 * @var CredentialProviderInterface
	 */
	private CredentialProviderInterface $chain;

	/**
	 * Original Connectors provider used for status enrichment.
	 *
	 * @since 2.6.0
	 * @var ConnectorsCredentialProvider
	 */
	private ConnectorsCredentialProvider $connectorsProvider;

	/**
	 * Original local provider used for status enrichment.
	 *
	 * @since 2.6.0
	 * @var LocalCredentialProvider
	 */
	private LocalCredentialProvider $localProvider;

	/**
	 * Creates the facade around an already-built provider chain.
	 *
	 * @since 2.6.0
	 *
	 * @param CredentialProviderInterface $chain              Active credential chain.
	 * @param ConnectorsCredentialProvider $connectorsProvider Original Connectors provider.
	 * @param LocalCredentialProvider      $localProvider      Original local provider.
	 */
	private function __construct(
		CredentialProviderInterface $chain,
		ConnectorsCredentialProvider $connectorsProvider,
		LocalCredentialProvider $localProvider
	) {
		$this->chain              = $chain;
		$this->connectorsProvider = $connectorsProvider;
		$this->localProvider      = $localProvider;
	}

	/**
	 * Returns the singleton credential resolver.
	 *
	 * @since 2.6.0
	 *
	 * @return self Singleton instance.
	 */
	public static function instance(): self {
		if ( null !== self::$instance ) {
			return self::$instance;
		}

		$connectors_provider = new ConnectorsCredentialProvider();
		$local_provider      = new LocalCredentialProvider();
		$default_chain       = array( $connectors_provider, $local_provider );

		$filtered_chain = apply_filters( 'aipt_credential_provider_chain', $default_chain, null );
		if ( ! self::isValidProviderList( $filtered_chain ) ) {

			$doing_it_wrong = '_' . 'doing_it_wrong';
			$doing_it_wrong(
				__METHOD__,
				esc_html__( 'The aipt_credential_provider_chain filter must return an array of CredentialProviderInterface instances.', 'ai-product-tools' ),
				'2.6.0'
			);

			$filtered_chain = $default_chain;
		}

		self::$instance = new self(
			new ChainedCredentialProvider( $filtered_chain ),
			$connectors_provider,
			$local_provider
		);

		return self::$instance;
	}

	/**
	 * Clears the singleton for unit tests.
	 *
	 * Runtime code must not call this method because it discards request-level
	 * memoization and can change provider-chain identity mid-request.
	 *
	 * @since 2.6.0
	 *
	 * @return void
	 */
	public static function reset(): void {
		self::$instance = null;
	}

	/**
	 * Clears request-level credential memoization without replacing the chain.
	 *
	 * Runtime code must use this after credential writes so subsequent reads in the
	 * same request observe fresh values while preserving provider-chain identity.
	 *
	 * @since 2.6.0
	 *
	 * @return void
	 */
	public function flushMemoization(): void {
		foreach ( array( $this->chain, $this->connectorsProvider, $this->localProvider ) as $provider ) {
			if ( method_exists( $provider, 'clearCache' ) ) {
				$provider->clearCache();
			}
		}
	}

	/**
	 * Resolves an API key for the given AIPT provider.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return string|null The resolved API key, or null if not configured.
	 */
	public function getKey( string $aiptProvider ): ?string {
		return $this->chain->getKey( $aiptProvider );
	}

	/**
	 * Returns the credential source for the given provider.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return string One of CredentialSource constants.
	 */
	public function getSource( string $aiptProvider ): string {
		return $this->chain->getSource( $aiptProvider );
	}

	/**
	 * Checks if a credential is configured for the given provider.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return bool True if credential is configured, false otherwise.
	 */
	public function isConfigured( string $aiptProvider ): bool {
		return $this->chain->isConfigured( $aiptProvider );
	}

	/**
	 * Returns the human-readable label for the credential source.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return string Translated label suitable for display.
	 */
	public function getSourceLabel( string $aiptProvider ): string {
		return $this->chain->getSourceLabel( $aiptProvider );
	}

	/**
	 * Checks if any supported provider has a resolvable key.
	 *
	 * @since 2.6.0
	 *
	 * @return bool True if at least one provider is configured.
	 */
	public function hasAnyConfiguredProvider(): bool {
		return $this->hasAnyUsableKey();
	}

	/**
	 * Checks if any supported provider has a resolvable non-empty key.
	 *
	 * @since 2.6.0
	 *
	 * @return bool True if at least one provider has a usable key.
	 */
	public function hasAnyUsableKey(): bool {
		foreach ( ProviderNameMapper::aiptProviders() as $aipt_provider ) {
			if ( $this->isConfigured( $aipt_provider ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns aggregate credential status for REST and UI consumers.
	 *
	 * @since 2.6.0
	 *
	 * @return array<string, array{id: string, isConnectorsManaged: bool, source: string, sourceLabel: string, canEditLocally: bool, hasLocalKey: bool, hasUsableKey: bool, envVarName: string|null, constantName: string|null}> Per-provider status map.
	 */
	public function getStatus(): array {
		$status = array();

		foreach ( ProviderNameMapper::aiptProviders() as $aipt_provider ) {
			$source     = $this->getSource( $aipt_provider );
			$is_env     = CredentialSource::ENV === $source;
			$is_constant = CredentialSource::CONSTANT === $source;
			$is_usable  = $this->isConfigured( $aipt_provider );

			$status[ $aipt_provider ] = array(
				'id'                   => $aipt_provider,
				'isConnectorsManaged'  => self::isConnectorsManagedSource( $source ),
				'source'               => $source,
				'sourceLabel'          => $this->getSourceLabel( $aipt_provider ),
				'canEditLocally'       => ! in_array(
					$source,
					array( CredentialSource::ENV, CredentialSource::CONSTANT, CredentialSource::DATABASE_CONNECTORS ),
					true
				),
				'hasLocalKey'          => $this->localProvider->isConfigured( $aipt_provider ),
				'hasUsableKey'         => $is_usable,
				'envVarName'           => $is_env ? $this->connectorsProvider->getEnvVarName( $aipt_provider ) : null,
				'constantName'         => $is_constant ? $this->connectorsProvider->getConstantName( $aipt_provider ) : null,
			);
		}

		return $status;
	}

	/**
	 * Validates the provider list returned by the extension filter.
	 *
	 * @since 2.6.0
	 *
	 * @param mixed $providers Candidate provider list.
	 * @return bool True when every entry implements CredentialProviderInterface.
	 */
	private static function isValidProviderList( $providers ): bool {
		if ( ! is_array( $providers ) ) {
			return false;
		}

		foreach ( $providers as $provider ) {
			if ( ! $provider instanceof CredentialProviderInterface ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Checks whether a source is managed by WordPress Connectors.
	 *
	 * @since 2.6.0
	 *
	 * @param string $source Credential source.
	 * @return bool True when the source belongs to Connectors.
	 */
	private static function isConnectorsManagedSource( string $source ): bool {
		return in_array(
			$source,
			array(
				CredentialSource::ENV,
				CredentialSource::CONSTANT,
				CredentialSource::DATABASE_CONNECTORS,
			),
			true
		);
	}
}
