<?php

declare(strict_types=1);

namespace AIPT\Utils\Credentials;

/**
 * Ordered credential provider chain.
 *
 * Walks an ordered list of CredentialProviderInterface instances and returns the
 * first configured credential. Earlier providers win over later providers. The
 * resolved key, source, and provider index are memoized only for the lifetime of
 * the current PHP request; no cross-request cache is used.
 *
 * @since 2.6.0
 */
final class ChainedCredentialProvider implements CredentialProviderInterface {

	/**
	 * Ordered credential providers.
	 *
	 * @since 2.6.0
	 * @var array<int, CredentialProviderInterface>
	 */
	private array $providers;

	/**
	 * Request-level resolved credential cache keyed by AIPT provider slug.
	 *
	 * @since 2.6.0
	 * @var array<string, array{key: string|null, source: string, providerIndex: int|null}>
	 */
	private array $resolved = array();

	/**
	 * Creates a credential provider chain.
	 *
	 * @since 2.6.0
	 *
	 * @param array<int, CredentialProviderInterface> $providers Ordered provider list.
	 *
	 * @throws \InvalidArgumentException If any entry is not a CredentialProviderInterface.
	 */
	public function __construct( array $providers ) {
		foreach ( $providers as $provider ) {
			if ( ! $provider instanceof CredentialProviderInterface ) {
				throw new \InvalidArgumentException( 'ChainedCredentialProvider expects only CredentialProviderInterface instances.' );
			}
		}

		$this->providers = array_values( $providers );
	}

	/**
	 * 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.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized by the chain.
	 */
	public function getKey( string $aiptProvider ): ?string {
		$resolved = $this->resolveChain( $aiptProvider );

		return $resolved['key'];
	}

	/**
	 * Returns the source of the credential for the given provider.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return string One of CredentialSource constants.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized by the chain.
	 */
	public function getSource( string $aiptProvider ): string {
		$resolved = $this->resolveChain( $aiptProvider );

		return $resolved['source'];
	}

	/**
	 * 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.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized by the chain.
	 */
	public function isConfigured( string $aiptProvider ): bool {
		$key = $this->getKey( $aiptProvider );

		return null !== $key && '' !== $key;
	}

	/**
	 * 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.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized by the chain.
	 */
	public function getSourceLabel( string $aiptProvider ): string {
		return CredentialSource::label( $this->getSource( $aiptProvider ) );
	}

	/**
	 * Clears request-level chain memoization and any provider caches that expose it.
	 *
	 * The provider interface intentionally does not require clearCache(), because
	 * third-party providers may be injected through aipt_credential_provider_chain.
	 *
	 * @since 2.6.0
	 *
	 * @return void
	 */
	public function clearCache(): void {
		$this->resolved = array();

		foreach ( $this->providers as $provider ) {
			if ( method_exists( $provider, 'clearCache' ) ) {
				$provider->clearCache();
			}
		}
	}

	/**
	 * Resolves the first configured provider in the chain.
	 *
	 * Unknown-provider validation is delegated to wrapped providers. If every
	 * provider rejects the provider slug, the last InvalidArgumentException is
	 * propagated. If at least one provider accepts the slug and none are configured,
	 * the result is cached as CredentialSource::NONE.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return array{key: string|null, source: string, providerIndex: int|null} Resolved tuple.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized by the chain.
	 */
	private function resolveChain( string $aiptProvider ): array {
		if ( array_key_exists( $aiptProvider, $this->resolved ) ) {
			return $this->resolved[ $aiptProvider ];
		}

		$accepted_count = 0;
		$last_exception = null;

		foreach ( $this->providers as $index => $provider ) {
			try {
				$is_configured = $provider->isConfigured( $aiptProvider );
			} catch ( \InvalidArgumentException $exception ) {
				$last_exception = $exception;
				continue;
			}

			++$accepted_count;

			if ( true !== $is_configured ) {
				continue;
			}

			$resolved = array(
				'key'           => $provider->getKey( $aiptProvider ),
				'source'        => $provider->getSource( $aiptProvider ),
				'providerIndex' => $index,
			);

			$this->resolved[ $aiptProvider ] = $resolved;

			return $resolved;
		}

		if ( 0 === $accepted_count && null !== $last_exception ) {
			throw $last_exception;
		}

		$resolved = array(
			'key'           => null,
			'source'        => CredentialSource::NONE,
			'providerIndex' => null,
		);

		$this->resolved[ $aiptProvider ] = $resolved;

		return $resolved;
	}
}
