<?php
/**
 * Environment check result aggregate.
 *
 * Collects zero or more {@see EnvironmentCheckError} instances produced
 * by {@see EnvironmentChecker::check()} and exposes a small, purpose-built
 * query surface so that higher-level bootstrap code can decide whether
 * the plugin should proceed, and what to report to the operator.
 *
 * A result with no errors represents a passing environment. A result
 * with one or more errors represents a failing environment; the plugin's
 * bootstrap is expected to halt further wiring and surface the errors
 * via an admin notice (and optionally self-deactivate).
 *
 * Instances are immutable.
 *
 * @package Affilun\Bootstrap
 */

declare(strict_types=1);

namespace Affilun\Bootstrap;

/**
 * Immutable aggregate of environment check failures.
 *
 * @phpstan-type EnvironmentCheckErrorArray array{axis: string, required: string, detected: ?string, message: string}
 */
final class EnvironmentCheckResult {

	/**
	 * Collected errors in the order produced by {@see EnvironmentChecker}.
	 *
	 * @var list<EnvironmentCheckError>
	 */
	private array $errors;

	/**
	 * @param list<EnvironmentCheckError> $errors Collected errors, possibly empty.
	 */
	public function __construct( array $errors = array() ) {
		// Re-index defensively so callers receive a list with contiguous
		// numeric keys regardless of how the caller constructed the array.
		$this->errors = array_values( $errors );
	}

	/**
	 * Report whether every configured check passed.
	 *
	 * Equivalent to {@see self::hasErrors()} returning false, but named
	 * from the perspective of the happy path so that call sites read
	 * naturally (e.g. `if ( $result->isPassing() ) { ... }`).
	 */
	public function isPassing(): bool {
		return $this->errors === array();
	}

	/**
	 * Report whether at least one check failed.
	 */
	public function hasErrors(): bool {
		return $this->errors !== array();
	}

	/**
	 * Number of collected errors.
	 */
	public function count(): int {
		return count( $this->errors );
	}

	/**
	 * Every collected error, in the order produced by the checker.
	 *
	 * @return list<EnvironmentCheckError>
	 */
	public function errors(): array {
		return $this->errors;
	}

	/**
	 * Errors filtered to a single axis.
	 *
	 * Useful for targeted reporting (e.g. "tell the operator only about
	 * the WooCommerce version mismatch") and for tests.
	 *
	 * @param string $axis Axis identifier, such as "php" / "WordPress" / "woocommerce".
	 *
	 * @return list<EnvironmentCheckError>
	 */
	public function errorsFor( string $axis ): array {
		return array_values(
			array_filter(
				$this->errors,
				static fn ( EnvironmentCheckError $error ): bool => $error->axis() === $axis
			)
		);
	}

	/**
	 * Report whether any error was produced for the given axis.
	 *
	 * @param string $axis Axis identifier.
	 */
	public function hasErrorFor( string $axis ): bool {
		foreach ( $this->errors as $error ) {
			if ( $error->axis() === $axis ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Human-readable messages from every collected error, in order.
	 *
	 * Presentation code may join these with line breaks (for admin
	 * notices) or newlines (for log entries) without having to traverse
	 * the error objects itself.
	 *
	 * @return list<string>
	 */
	public function messages(): array {
		return array_map(
			static fn ( EnvironmentCheckError $error ): string => $error->message(),
			$this->errors
		);
	}

	/**
	 * Array representation suitable for logging or serialization.
	 *
	 * @return array{
	 *     passing: bool,
	 *     error_count: int,
	 *     errors: list<EnvironmentCheckErrorArray>,
	 * }
	 */
	public function toArray(): array {
		return array(
			'passing'     => $this->isPassing(),
			'error_count' => $this->count(),
			'errors'      => array_map(
				static fn ( EnvironmentCheckError $error ): array => $error->toArray(),
				$this->errors
			),
		);
	}
}
