<?php
/**
 * Read-only PHP and WordPress environment collector.
 *
 * @package MarketingConsole
 */

namespace MarketingConsole\Modules\Speed\Collectors;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Collects only the allowlisted, non-identifying MVP environment fields.
 */
final class EnvCollector implements CollectorInterface {

	/** @var array<string,mixed> */
	private array $probes;

	/**
	 * Test probes are optional; production reads WordPress/PHP directly.
	 *
	 * @param array<string,mixed> $probes Deterministic read-only probe values.
	 */
	public function __construct( array $probes = array() ) {
		$this->probes = $probes;
	}

	/**
	 * @return string
	 */
	public function key(): string {
		return 'env';
	}

	/**
	 * @return array<string,array<string,mixed>>
	 */
	public function collect(): array {
		$now = gmdate( 'Y-m-d\TH:i:s\Z' );

		return array(
			'php_ver'        => $this->string_field( $this->php_version(), 'php_runtime', $now ),
			'wp_ver'         => $this->string_field( $this->wp_version(), 'wordpress', $now ),
			'plugins_active' => $this->plugins_field( $now ),
			'active_theme'   => $this->string_field( $this->active_theme(), 'wordpress', $now ),
			'wp_debug'       => $this->boolean_field( $this->wp_debug(), 'wordpress_config', $now ),
			'opcache_enabled' => $this->boolean_field( $this->opcache_enabled(), 'php_runtime', $now ),
		);
	}

	/**
	 * @param string $key      Probe key.
	 * @param mixed  $fallback Production value.
	 * @return mixed
	 */
	private function probe( string $key, $fallback ) {
		return array_key_exists( $key, $this->probes ) ? $this->probes[ $key ] : $fallback;
	}

	/**
	 * Canonical numeric PHP core version (major.minor.release).
	 *
	 * The raw PHP_VERSION on distro builds carries packaging suffixes such as
	 * "7.4.3-4ubuntu2.29+tuxcare.els2" that are not semantic version data.
	 * The EOL and recommendation logic only needs the numeric core version.
	 *
	 * @return mixed
	 */
	private function php_version() {
		if ( array_key_exists( 'php_ver', $this->probes ) ) {
			return $this->probes['php_ver'];
		}

		if ( defined( 'PHP_MAJOR_VERSION' ) && defined( 'PHP_MINOR_VERSION' ) && defined( 'PHP_RELEASE_VERSION' ) ) {
			return PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
		}

		return PHP_VERSION;
	}

	/**
	 * @return mixed
	 */
	private function wp_version() {
		if ( array_key_exists( 'wp_ver', $this->probes ) ) {
			return $this->probes['wp_ver'];
		}

		global $wp_version;

		if ( isset( $wp_version ) && is_string( $wp_version ) ) {
			return $wp_version;
		}

		return function_exists( 'get_bloginfo' ) ? get_bloginfo( 'version' ) : null;
	}

	/**
	 * @param string $now Observation time.
	 * @return array<string,mixed>
	 */
	private function plugins_field( string $now ): array {
		if ( array_key_exists( 'plugins_active', $this->probes ) ) {
			$value = $this->probes['plugins_active'];
		} elseif ( function_exists( 'get_option' ) ) {
			$value = get_option( 'active_plugins', array() );

			if ( is_array( $value ) && function_exists( 'is_multisite' ) && is_multisite() && function_exists( 'get_site_option' ) ) {
				$network = get_site_option( 'active_sitewide_plugins', array() );

				if ( ! is_array( $network ) ) {
					return $this->error( 'unexpected_type', 'wordpress_option', $now );
				}

				$value = array_values( array_unique( array_merge( $value, array_keys( $network ) ) ) );
			}
		} else {
			$value = null;
		}

		if ( is_array( $value ) ) {
			return $this->ok( count( $value ), 'wordpress_option', $now );
		}

		return null === $value ? $this->unavailable( 'wordpress_option', $now ) : $this->error( 'unexpected_type', 'wordpress_option', $now );
	}

	/**
	 * @return mixed
	 */
	private function active_theme() {
		if ( array_key_exists( 'active_theme', $this->probes ) ) {
			return $this->probes['active_theme'];
		}

		if ( ! function_exists( 'wp_get_theme' ) ) {
			return null;
		}

		$theme = wp_get_theme();

		return is_object( $theme ) && is_callable( array( $theme, 'get_stylesheet' ) ) ? $theme->get_stylesheet() : null;
	}

	/**
	 * @return mixed
	 */
	private function wp_debug() {
		if ( array_key_exists( 'wp_debug', $this->probes ) ) {
			return $this->probes['wp_debug'];
		}

		return defined( 'WP_DEBUG' ) ? (bool) constant( 'WP_DEBUG' ) : false;
	}

	/**
	 * @return mixed
	 */
	private function opcache_enabled() {
		if ( array_key_exists( 'opcache_enabled', $this->probes ) ) {
			return $this->probes['opcache_enabled'];
		}

		// Extension absent is a definitive "not enabled", cheaper than ini_get.
		if ( function_exists( 'extension_loaded' ) && ! extension_loaded( 'Zend OPcache' ) ) {
			return false;
		}

		// Isolate ini_get: on PHP 8 a disabled function throws (removed from the
		// symbol table), which must degrade only this field to unsupported.
		if ( ! function_exists( 'ini_get' ) ) {
			return null;
		}

		try {
			$value = ini_get( 'opcache.enable' );
		} catch ( \Throwable $e ) {
			return null;
		}

		if ( false === $value ) {
			return null;
		}

		return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
	}

	/**
	 * @param mixed  $value  Candidate string.
	 * @param string $source Safe source.
	 * @param string $now    Observation time.
	 * @return array<string,mixed>
	 */
	private function string_field( $value, string $source, string $now ): array {
		if ( null === $value || '' === $value ) {
			return $this->unavailable( $source, $now );
		}

		if ( ! is_string( $value ) ) {
			return $this->error( 'unexpected_type', $source, $now );
		}

		return $this->ok( substr( $value, 0, 128 ), $source, $now );
	}

	/**
	 * @param mixed  $value  Candidate boolean.
	 * @param string $source Safe source.
	 * @param string $now    Observation time.
	 * @return array<string,mixed>
	 */
	private function boolean_field( $value, string $source, string $now ): array {
		if ( null === $value ) {
			return $this->unavailable( $source, $now );
		}

		if ( ! is_bool( $value ) ) {
			return $this->error( 'unexpected_type', $source, $now );
		}

		return $this->ok( $value, $source, $now );
	}

	/**
	 * @param mixed  $value  Measured value.
	 * @param string $source Safe source.
	 * @param string $now    Observation time.
	 * @return array<string,mixed>
	 */
	private function ok( $value, string $source, string $now ): array {
		return array( 'value' => $value, 'status' => 'ok', 'source' => $source, 'observed_at' => $now );
	}

	/**
	 * @param string $source Safe source.
	 * @param string $now    Observation time.
	 * @return array<string,mixed>
	 */
	private function unavailable( string $source, string $now ): array {
		return array( 'value' => null, 'status' => 'unsupported', 'source' => $source, 'observed_at' => $now );
	}

	/**
	 * @param string $code   Allowlisted error code.
	 * @param string $source Safe source.
	 * @param string $now    Observation time.
	 * @return array<string,mixed>
	 */
	private function error( string $code, string $source, string $now ): array {
		return array( 'value' => null, 'status' => 'error', 'error_code' => $code, 'source' => $source, 'observed_at' => $now );
	}
}
