<?php

declare(strict_types=1);

namespace AIPT\Utils\Credentials;

use AIPT\Utils\Encryption;
use AIPT\Utils\ProviderNameMapper;

/**
 * Legacy-key credential provider.
 *
 * Reads API keys from the legacy aipt_*_api_key wp_options entries and applies
 * the same AES-256-GCM decryption + plaintext-fallback logic used by
 * ApiKeyManager (src/Utils/ApiKeyManager.php). Never throws on missing keys;
 * returns null and NONE source for absent or unrecoverable credentials.
 * Throws only on programming errors (unknown provider name).
 *
 * Ported decryption logic:
 *   - Early plaintext return:     ApiKeyManager::get_openai_key()        lines 107–110.
 *   - 3-mode decryption attempts: ApiKeyManager::attempt_decryption()    lines 37–58.
 *   - Sentinel constant:          ApiKeyManager::convert_to_plaintext()  lines 76–78.
 *
 * @since 2.6.0
 */
final class LocalCredentialProvider implements CredentialProviderInterface {

	/**
	 * Prefix used by Encryption::encrypt_api_key() to mark encrypted values.
	 *
	 * Must match Encryption::ENCRYPTED_PREFIX (private const in Encryption.php).
	 *
	 * @since 2.6.0
	 * @var string
	 */
	private const ENCRYPTED_PREFIX = 'AIPT_ENC_';

	/**
	 * Sentinel written by ApiKeyManager::convert_to_plaintext() (lines 76–78) when
	 * all decryption attempts fail; signals that the user must re-enter the key.
	 *
	 * @since 2.6.0
	 * @var string
	 */
	private const FALLBACK_SENTINEL = 'ENCRYPTED_FALLBACK_NEEDS_REENTRY';

	/**
	 * Per-request resolved-key cache.
	 *
	 * Keyed by AIPT provider slug. Absence from the array means "not yet resolved".
	 * A null entry means "resolved — key absent or unrecoverable". A non-null string
	 * entry is the resolved plaintext key.
	 *
	 * @since 2.6.0
	 * @var array<string, string|null>
	 */
	private array $resolvedKeys = array();

	/**
	 * Resolves an API key for the given AIPT provider.
	 *
	 * Reads the wp_option aipt_{$aiptProvider}_api_key. Returns the plaintext key
	 * if configured, applying the same three-mode decryption fallback as
	 * ApiKeyManager::attempt_decryption() (lines 37–58). Returns null for absent,
	 * sentinel, or unrecoverable values. Never throws on missing credentials.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return string|null The resolved plaintext key, or null if not configured.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized.
	 */
	public function getKey( string $aiptProvider ): ?string {
		self::validateProvider( $aiptProvider );

		if ( array_key_exists( $aiptProvider, $this->resolvedKeys ) ) {
			return $this->resolvedKeys[ $aiptProvider ];
		}

		$resolved                            = $this->resolveKey( $aiptProvider );
		$this->resolvedKeys[ $aiptProvider ] = $resolved;
		return $resolved;
	}

	/**
	 * Returns the credential source for the given provider.
	 *
	 * Returns CredentialSource::DATABASE_LOCAL when getKey() returns a non-null
	 * string, CredentialSource::NONE otherwise. Uses the cached result of getKey()
	 * to avoid repeated decryption calls within the same request.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider One of 'openai', 'gemini', 'claude', 'openrouter'.
	 * @return string One of the CredentialSource constants.
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not recognized.
	 */
	public function getSource( string $aiptProvider ): string {
		self::validateProvider( $aiptProvider );
		return null !== $this->getKey( $aiptProvider )
			? CredentialSource::DATABASE_LOCAL
			: CredentialSource::NONE;
	}

	/**
	 * Checks if a credential is configured for the given provider.
	 *
	 * Convenience method: returns true iff getKey() returns a non-null string.
	 *
	 * @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.
	 */
	public function isConfigured( string $aiptProvider ): bool {
		return null !== $this->getKey( $aiptProvider );
	}

	/**
	 * Returns the human-readable label for the credential source.
	 *
	 * Delegates to CredentialSource::label( $this->getSource( $aiptProvider ) ).
	 *
	 * @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.
	 */
	public function getSourceLabel( string $aiptProvider ): string {
		return CredentialSource::label( $this->getSource( $aiptProvider ) );
	}

	/**
	 * Clears request-level resolved key memoization.
	 *
	 * Runtime credential writes call through CredentialResolver::flushMemoization()
	 * so subsequent reads in the same request observe fresh option values.
	 *
	 * @since 2.6.0
	 *
	 * @return void
	 */
	public function clearCache(): void {
		$this->resolvedKeys = array();
	}

	/**
	 * Core key resolution logic.
	 *
	 * Reads the raw wp_option value and applies decryption/fallback in order:
	 *   1. Empty value — return null.
	 *   2. Contains ENCRYPTED_FALLBACK_NEEDS_REENTRY sentinel — return null.
	 *   3. No AIPT_ENC_ prefix — return as plaintext (mirrors ApiKeyManager lines 107–110).
	 *   4. Three decryption attempts with legacy_mode null/true/false and silent_mode=true
	 *      (ported from ApiKeyManager::attempt_decryption() lines 37–58).
	 *   5. Last-resort: if raw no longer carries the AIPT_ENC_ prefix, return as plaintext
	 *      (edge case from unconventional migration paths, ref. ApiKeyManager lines 68–93).
	 *   6. Encrypted but unrecoverable — return null.
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider AIPT provider slug (already validated by getKey()).
	 * @return string|null Resolved plaintext key, or null.
	 */
	private function resolveKey( string $aiptProvider ): ?string {
		$raw = (string) get_option( "aipt_{$aiptProvider}_api_key", '' );

		if ( '' === $raw ) {
			return null;
		}

		if ( false !== strpos( $raw, self::FALLBACK_SENTINEL ) ) {
			return null;
		}

		if ( 0 !== strpos( $raw, self::ENCRYPTED_PREFIX ) ) {
			return $raw;
		}

		$decryption_modes = array( null, true, false );
		foreach ( $decryption_modes as $legacy_mode ) {
			try {
				$decrypted = Encryption::decrypt_api_key( $raw, $legacy_mode, true );
				if ( $decrypted !== $raw && '' !== $decrypted ) {
					return $decrypted;
				}
			} catch ( \Exception $e ) {

			}
		}

		if ( 0 !== strpos( $raw, self::ENCRYPTED_PREFIX ) ) {
			return $raw;
		}

		return null;
	}

	/**
	 * Validates an AIPT provider slug against ProviderNameMapper::aiptProviders().
	 *
	 * @since 2.6.0
	 *
	 * @param string $aiptProvider AIPT provider slug to validate.
	 * @return void
	 *
	 * @throws \InvalidArgumentException If $aiptProvider is not in the supported list.
	 */
	private static function validateProvider( string $aiptProvider ): void {
		if ( ! in_array( $aiptProvider, ProviderNameMapper::aiptProviders(), true ) ) {

			throw new \InvalidArgumentException( "Unknown AIPT provider: {$aiptProvider}" );
		}
	}
}
