<?php
/**
 * Environment checker.
 *
 * Verifies that the host environment satisfies affilun's
 * minimum requirements before the plugin wires up any functionality.
 *
 * The checker inspects three axes:
 *
 *   1. PHP version   — compared against AFFILUN_MIN_PHP_VERSION (8.1).
 *   2. WordPress     — compared against AFFILUN_MIN_WP_VERSION (6.6) via the
 *                      global $wp_version when it is available.
 *   3. WooCommerce   — **optional coexistence check only.** Commerce Link
 *                      Builder is an affiliate-link plugin and does NOT
 *                      depend on WooCommerce. This axis exists solely to
 *                      verify that, when WooCommerce happens to be
 *                      installed alongside AFFILUN, its version is recent
 *                      enough for safe coexistence. When WooCommerce is
 *                      absent the check is skipped and does NOT fail;
 *                      callers that want to know whether WooCommerce is
 *                      present can inspect
 *                      {@see self::isWooCommerceActive()} explicitly.
 *
 * The checker itself performs no side effects — it does not emit admin
 * notices, deactivate the plugin, or write to the error log. It returns
 * a structured {@see EnvironmentCheckResult} that higher-level bootstrap
 * code ({@see Plugin}) consumes to decide whether to proceed.
 *
 * This split keeps the checker pure and trivially unit-testable without
 * a running WordPress environment: callers can inject synthetic version
 * strings instead of relying on PHP_VERSION / $wp_version / the real
 * WC() singleton.
 *
 * @package Affilun\Bootstrap
 */

declare(strict_types=1);

namespace Affilun\Bootstrap;

/**
 * Pure environment verification.
 *
 * Instances are cheap to construct and hold no state between checks;
 * each call to {@see self::check()} re-reads the configured version
 * providers.
 *
 * @phpstan-type VersionProvider callable(): ?string
 */
final class EnvironmentChecker {

	/**
	 * Minimum PHP version required by the plugin.
	 */
	private string $minPhpVersion;

	/**
	 * Minimum WordPress version required by the plugin.
	 */
	private string $minWpVersion;

	/**
	 * Minimum WooCommerce version applied as an optional coexistence check.
	 *
	 * affilun does not require WooCommerce. This value is
	 * only consulted when WooCommerce is installed and activated, in
	 * order to verify safe coexistence with a recent-enough WooCommerce
	 * release. Set to null to disable the coexistence comparison
	 * entirely.
	 */
	private ?string $minWcVersion;

	/**
	 * Callable that returns the current PHP version, or null when it
	 * cannot be determined. Defaulted to PHP_VERSION and overridable
	 * from tests.
	 *
	 * @var callable(): ?string
	 */
	private $phpVersionProvider;

	/**
	 * Callable that returns the current WordPress version, or null when
	 * WordPress is not loaded. Defaulted to reading $GLOBALS['wp_version']
	 * and overridable from tests.
	 *
	 * @var callable(): ?string
	 */
	private $wpVersionProvider;

	/**
	 * Callable that returns the current WooCommerce version, or null when
	 * WooCommerce is not active. Defaulted to reading the WC_VERSION
	 * constant and overridable from tests.
	 *
	 * @var callable(): ?string
	 */
	private $wcVersionProvider;

	/**
	 * Construct a checker with explicit minima and version providers.
	 *
	 * All arguments are optional so that production callers can simply do
	 * `new EnvironmentChecker()` and rely on the AFFILUN_MIN_*_VERSION
	 * constants defined by the plugin entry file. Tests override the
	 * arguments to inject synthetic environments.
	 *
	 * @param string|null                $minPhpVersion      Minimum PHP version.
	 *                                                       Defaults to AFFILUN_MIN_PHP_VERSION.
	 * @param string|null                $minWpVersion       Minimum WordPress version.
	 *                                                       Defaults to AFFILUN_MIN_WP_VERSION.
	 * @param string|null                $minWcVersion       Minimum WooCommerce version
	 *                                                       when WC is active. Null to skip.
	 * @param (callable(): ?string)|null $phpVersionProvider Override PHP version lookup.
	 * @param (callable(): ?string)|null $wpVersionProvider  Override WordPress version lookup.
	 * @param (callable(): ?string)|null $wcVersionProvider  Override WooCommerce version lookup.
	 */
	public function __construct(
		?string $minPhpVersion = null,
		?string $minWpVersion = null,
		?string $minWcVersion = null,
		?callable $phpVersionProvider = null,
		?callable $wpVersionProvider = null,
		?callable $wcVersionProvider = null
	) {
		$this->minPhpVersion = $minPhpVersion
			?? ( defined( 'AFFILUN_MIN_PHP_VERSION' ) ? (string) AFFILUN_MIN_PHP_VERSION : '8.1' );
		$this->minWpVersion  = $minWpVersion
			?? ( defined( 'AFFILUN_MIN_WP_VERSION' ) ? (string) AFFILUN_MIN_WP_VERSION : '6.6' );
		$this->minWcVersion  = $minWcVersion;

		$this->phpVersionProvider = $phpVersionProvider ?? static fn (): ?string => PHP_VERSION;

		$this->wpVersionProvider = $wpVersionProvider ?? static function (): ?string {
			if ( isset( $GLOBALS['wp_version'] ) && is_string( $GLOBALS['wp_version'] ) ) {
				return $GLOBALS['wp_version'];
			}
			return null;
		};

		$this->wcVersionProvider = $wcVersionProvider ?? static function (): ?string {
			if ( defined( 'WC_VERSION' ) ) {
				$version = constant( 'WC_VERSION' );
				return is_string( $version ) ? $version : null;
			}
			return null;
		};
	}

	/**
	 * Minimum PHP version this checker is configured to require.
	 */
	public function minPhpVersion(): string {
		return $this->minPhpVersion;
	}

	/**
	 * Minimum WordPress version this checker is configured to require.
	 */
	public function minWpVersion(): string {
		return $this->minWpVersion;
	}

	/**
	 * Minimum WooCommerce version this checker is configured to require,
	 * or null when the WooCommerce version check is disabled.
	 */
	public function minWcVersion(): ?string {
		return $this->minWcVersion;
	}

	/**
	 * Current PHP version as reported by the configured provider.
	 */
	public function phpVersion(): ?string {
		return ( $this->phpVersionProvider )();
	}

	/**
	 * Current WordPress version, or null when WordPress is not loaded.
	 */
	public function wpVersion(): ?string {
		return ( $this->wpVersionProvider )();
	}

	/**
	 * Current WooCommerce version, or null when WooCommerce is inactive.
	 */
	public function wcVersion(): ?string {
		return ( $this->wcVersionProvider )();
	}

	/**
	 * Report whether WooCommerce appears to be active in the environment.
	 *
	 * Exposed for callers that want to branch on optional coexistence
	 * (for example, to log that WooCommerce is present); AFFILUN itself does
	 * not require WooCommerce for any feature.
	 *
	 * Defined here as "the WC version provider returned a non-null value"
	 * so that tests can drive the check deterministically by providing a
	 * synthetic provider, without depending on class_exists() or the real
	 * WC() function.
	 */
	public function isWooCommerceActive(): bool {
		return $this->wcVersion() !== null;
	}

	/**
	 * Run all configured checks and return a structured result.
	 *
	 * A single call aggregates every failure rather than short-circuiting
	 * on the first mismatch, so the operator sees all problems in one
	 * admin notice instead of fixing them one at a time.
	 */
	public function check(): EnvironmentCheckResult {
		$errors = array();

		$phpVersion = $this->phpVersion();
		if ( $phpVersion === null || version_compare( $phpVersion, $this->minPhpVersion, '<' ) ) {
			$errors[] = new EnvironmentCheckError(
				'php',
				$this->minPhpVersion,
				$phpVersion,
				sprintf(
					'affilun requires PHP %s or later. Detected: %s.',
					$this->minPhpVersion,
					$phpVersion ?? 'unknown'
				)
			);
		}

		$wpVersion = $this->wpVersion();
		if ( $wpVersion === null || version_compare( $wpVersion, $this->minWpVersion, '<' ) ) {
			$errors[] = new EnvironmentCheckError(
				'wordpress',
				$this->minWpVersion,
				$wpVersion,
				sprintf(
					'affilun requires WordPress %s or later. Detected: %s.',
					$this->minWpVersion,
					$wpVersion ?? 'unknown'
				)
			);
		}

		if ( $this->minWcVersion !== null && $this->isWooCommerceActive() ) {
			$wcVersion = $this->wcVersion();
			if ( $wcVersion === null || version_compare( $wcVersion, $this->minWcVersion, '<' ) ) {
				$errors[] = new EnvironmentCheckError(
					'woocommerce',
					$this->minWcVersion,
					$wcVersion,
					sprintf(
						'affilun requires WooCommerce %s or later. Detected: %s.',
						$this->minWcVersion,
						$wcVersion ?? 'unknown'
					)
				);
			}
		}

		return new EnvironmentCheckResult( $errors );
	}
}
