<?php
/**
 * List available providers use case.
 *
 * Phase 6 Application-layer UseCase that drives the §10.3
 * `GET /affilun/v1/editor/providers` endpoint defined in
 * `specifications/rest-api-spec.md`. The class enumerates every
 * provider registered with the DI container at boot time and
 * projects each one onto the §10.3 response shape:
 *
 *   - `key`              — verbatim §4 stable provider key.
 *   - `label`            — human-readable display label.
 *   - `enabled`          — derived from the
 *                          {@see ProviderHealthStatus::isHealthy()}
 *                          accessor on the provider's current
 *                          health-probe snapshot. A provider whose
 *                          probe reports any non-`healthy` status
 *                          (degraded, unconfigured, rate-limited,
 *                          unreachable, invalid_credentials) is
 *                          surfaced to the editor as disabled so
 *                          the UI can hide it from selection
 *                          affordances until the underlying
 *                          condition is resolved.
 *   - `configured`       — derived from the same probe snapshot:
 *                          false when {@see ProviderHealthStatus::status()}
 *                          equals
 *                          {@see ProviderHealthStatus::STATUS_UNCONFIGURED}
 *                          (the canonical "admin has not finished
 *                          setup" token), true otherwise. Other
 *                          unhealthy statuses such as
 *                          `invalid_credentials` are deliberately
 *                          NOT mapped to `configured = false` here
 *                          — a misconfigured provider is still
 *                          "configured but broken" rather than
 *                          "not configured at all"; the §7.10
 *                          status surface carries that nuance and
 *                          §10.3's two-flag projection collapses
 *                          it to the bare "is the admin form
 *                          filled in?" question.
 *   - `search_available` — derived from
 *                          {@see ProviderInterface::supportsFeature()}
 *                          on the §8 feature key
 *                          {@see ProviderCapabilities::FEATURE_SEARCH}.
 *                          The editor uses this flag to decide
 *                          whether to render the provider as a
 *                          search source in the keyword-search
 *                          flow.
 *
 * Defensive contract around `healthCheck()`:
 *
 *   - {@see ProviderInterface::healthCheck()} is documented to
 *     never throw on transient upstream failures: the §7.10 status
 *     surface is itself the "something is wrong" signal. Despite
 *     that documented contract, this UseCase wraps every per-
 *     provider call in a `try` / `catch ( \Throwable $t )` block
 *     so a misbehaving implementation that violates the contract
 *     cannot blow up the §10.3 enumeration as a whole. When a
 *     provider's `healthCheck()` (or its `supportsFeature()` /
 *     `key()` / `label()` accessors) throws, the offending
 *     provider's entry surfaces with `enabled = false`,
 *     `configured = false`, and `search_available = false`; the
 *     editor sees the provider as unusable but still receives a
 *     row for it so the diagnostics surface can render an admin-
 *     facing warning. This defensive posture is the §10.3
 *     equivalent of the §8.1 partial-success contract.
 *
 *   - The `key` and `label` values for a defensive entry come from
 *     the registry key (the array key supplied to the constructor)
 *     and a derived fallback label respectively — relying on the
 *     provider's own `key()` / `label()` accessors would defeat
 *     the purpose of catching their failures.
 *
 * Order: the returned list iterates `$providersByKey` in
 * registration order — the order the DI container chose when it
 * built the registry — so the editor renders providers in a
 * stable, configurable sequence.
 *
 * @package Affilun\Application\Editor\UseCase
 */

declare(strict_types=1);

namespace Affilun\Application\Editor\UseCase;

use Affilun\Domain\Provider\ProviderCapabilities;
use Affilun\Domain\Provider\ProviderHealthStatus;
use Affilun\Domain\Provider\ProviderInterface;
use InvalidArgumentException;
use Throwable;

/**
 * Phase 6 UseCase that drives the §10.3
 * `GET /affilun/v1/editor/providers` endpoint.
 *
 * See the file-level docblock for the §10.3 response-shape
 * mapping, the per-flag derivation rules over
 * {@see ProviderHealthStatus} and {@see ProviderCapabilities},
 * and the defensive try / catch posture around
 * {@see ProviderInterface::healthCheck()} that keeps a single
 * misbehaving provider from breaking the enumeration as a whole.
 */
final class ListAvailableProvidersUseCase {

	/**
	 * Registered providers keyed by their §4 stable token.
	 *
	 * Iteration order is the registration order; the UseCase
	 * honors it verbatim so callers can rely on a deterministic
	 * editor display order.
	 *
	 * @var array<string, ProviderInterface>
	 */
	private array $providersByKey;

	/**
	 * Construct the UseCase with an explicit provider registry.
	 *
	 * Validation:
	 *
	 *   - Empty registry is allowed. Phase 6's minimum bootstrap
	 *     ships with no providers wired by default; the §10.3
	 *     endpoint must answer `items: []` rather than failing
	 *     loudly so the editor surface remains operational while
	 *     the admin completes provider setup.
	 *
	 *   - Every key MUST be a non-empty string after trimming.
	 *     Non-string keys, empty-string keys, and whitespace-only
	 *     keys raise {@see InvalidArgumentException}.
	 *
	 *   - Every value MUST implement
	 *     {@see ProviderInterface}. Other types raise
	 *     {@see InvalidArgumentException}.
	 *
	 * @param array<string, ProviderInterface> $providersByKey Provider registry.
	 *
	 * @throws InvalidArgumentException When any registry entry
	 *                                  violates the validation
	 *                                  rules summarized above.
	 */
	public function __construct( array $providersByKey ) {
		$normalized = array();
		foreach ( $providersByKey as $key => $provider ) {
			if ( ! is_string( $key ) ) {
				$message = sprintf(
					'ListAvailableProvidersUseCase: provider registry keys must be strings; got %s.',
					gettype( $key )
				);
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new InvalidArgumentException( $message );
			}
			if ( trim( $key ) === '' ) {
				$message = 'ListAvailableProvidersUseCase: provider registry keys must be non-empty strings.';
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new InvalidArgumentException( $message );
			}
			if ( ! $provider instanceof ProviderInterface ) {
				$message = sprintf(
					'ListAvailableProvidersUseCase: provider registry["%s"] must implement ProviderInterface; got %s.',
					$key,
					is_object( $provider ) ? get_class( $provider ) : gettype( $provider )
				);
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
				throw new InvalidArgumentException( $message );
			}

			$normalized[ $key ] = $provider;
		}

		$this->providersByKey = $normalized;
	}

	/**
	 * Execute the §10.3 enumeration.
	 *
	 * Iterates the registered providers in registration order and
	 * projects each one onto the §10.3 response shape. See the
	 * file-level docblock for the per-flag derivation rules and
	 * the defensive `try` / `catch` posture around
	 * {@see ProviderInterface::healthCheck()}.
	 *
	 * @return list<array{key: string, label: string, enabled: bool, configured: bool, search_available: bool}>
	 */
	public function execute(): array {
		$items = array();
		foreach ( $this->providersByKey as $registryKey => $provider ) {
			$items[] = $this->describeProvider( $registryKey, $provider );
		}

		return $items;
	}

	/**
	 * Project a single registered provider onto the §10.3 row
	 * shape, applying the defensive `try` / `catch` posture
	 * documented at the file level.
	 *
	 * Identity (`key`, `label`) is read from the provider's own
	 * accessors on the happy path; if any of those accessors —
	 * along with `healthCheck()` and `supportsFeature()` — throws,
	 * the row falls back to the registry key for `key`, a derived
	 * label for `label`, and `false` for all three boolean flags.
	 *
	 * @param string            $registryKey Registry key for the provider.
	 * @param ProviderInterface $provider    Registered provider instance.
	 *
	 * @return array{key: string, label: string, enabled: bool, configured: bool, search_available: bool}
	 */
	private function describeProvider( string $registryKey, ProviderInterface $provider ): array {
		try {
			$key             = $provider->key();
			$label           = $provider->label();
			$health          = $provider->healthCheck();
			$enabled         = $health->isHealthy();
			$configured      = $health->status() !== ProviderHealthStatus::STATUS_UNCONFIGURED;
			$searchAvailable = $provider->supportsFeature( ProviderCapabilities::FEATURE_SEARCH );

			return array(
				'key'              => $key,
				'label'            => $label,
				'enabled'          => $enabled,
				'configured'       => $configured,
				'search_available' => $searchAvailable,
			);
		} catch ( Throwable $unused ) {
			return array(
				'key'              => $registryKey,
				'label'            => $registryKey,
				'enabled'          => false,
				'configured'       => false,
				'search_available' => false,
			);
		}
	}
}
