<?php
/**
 * Prerequisites Utility
 *
 * @package   PopupMaker
 * @copyright Copyright (c) 2024, Code Atlantic LLC
 */

/**
 * Prerequisite handler.
 *
 * @version 1.0.0
 */
class PUM_Utils_Prerequisites {

	/**
	 * Cache accessible across instances.
	 *
	 * @var array{
	 *     get_plugin_data?: array<string, array{Name?: string, Version?: string}>
	 * }
	 */
	public static $cache = [];

	/**
	 * Array of checks to perform.
	 *
	 * @var array<int, array<string, mixed>>
	 */
	protected $checks = [];

	/**
	 * Array of detected failures.
	 *
	 * @var array<int, array<string, mixed>>
	 */
	protected $failures = [];

	/**
	 * Instantiate prerequisite checker.
	 *
	 * @param array<int, array<string, mixed>> $requirements Array of requirements.
	 */
	public function __construct( $requirements = [] ) {
		foreach ( $requirements as $arguments ) {
			/** @var array{type: string, version?: string, slug?: string, name?: string, check_installed?: bool, dep_label?: string} $arguments */
			switch ( $arguments['type'] ) {
				case 'php':
					$this->checks[] = wp_parse_args(
						$arguments,
						[
							'type'    => 'php',
							'version' => '5.6',
						]
					);
					break;
				case 'plugin':
					$this->checks[] = wp_parse_args(
						$arguments,
						[
							'type'            => 'plugin',
							'slug'            => '',
							'name'            => '',
							'version'         => '',
							'check_installed' => false,
							'dep_label'       => '',
						]
					);
					break;
				default:
					break;
			}
		}
	}

	/**
	 * Check requirements.
	 *
	 * @param bool $return_on_fail Whether it should stop processing if one fails.
	 *
	 * @return bool
	 */
	public function check( $return_on_fail = false ) {
		$end_result = true;

		foreach ( $this->checks as $check ) {
			/** @var array<string, mixed> $check */
			$result = $this->check_handler( $check );

			if ( false === $result ) {
				if ( true === $return_on_fail ) {
					return false;
				}

				$end_result = false;
			}
		}

		return $end_result;
	}

	/**
	 * Render notices when appropriate.
	 *
	 * @return void
	 */
	public function setup_notices() {
		add_action( 'admin_notices', [ $this, 'render_notices' ] );
	}

	/**
	 * Handle individual checks by mapping them to methods.
	 *
	 * @param array<string, mixed> $check Requirement check arguments.
	 *
	 * @return bool
	 */
	public function check_handler( $check ) {
		return method_exists( $this, 'check_' . $check['type'] ) ? $this->{'check_' . $check['type']}( $check ) : false;
	}

	/**
	 * Report failure notice to the queue.
	 *
	 * @param array<string, mixed> $check_args Array of check arguments.
	 *
	 * @return void
	 */
	public function report_failure( $check_args ) {
		$this->failures[] = $check_args;
	}

	/**
	 * Get a list of failures.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function get_failures() {
		return $this->failures;
	}

	/**
	 * Check PHP version against args.
	 *
	 * @param array<string, mixed> $check_args Array of args containing version requirement.
	 *
	 * @return bool
	 */
	public function check_php( $check_args ) {
		/** @var string $version */
		$version = $check_args['version'];
		if ( false === version_compare( phpversion(), $version, '>=' ) ) {
			$this->report_failure( $check_args );
			return false;
		}

		return true;
	}

	/**
	 * Check plugin requirements.
	 *
	 * @param array<string, mixed> $check_args Array of args containing plugin requirements.
	 *
	 * @return bool
	 */
	public function check_plugin( $check_args ) {
		/** @var string $slug */
		$slug   = $check_args['slug'];
		$active = $this->plugin_is_active( $slug );

		/**
		 * The following checks are performed in this order for performance reasons.
		 *
		 * We start with most cached option, to least in hopes of a hit early.
		 *
		 * 1. If active and not checking version.
		 * 2. If active and outdated.
		 * 3. If not active and installed.
		 * 4. If not installed
		 */
		if ( true === $active ) {
			// If required version is set & plugin is active, check that first.
			if ( isset( $check_args['version'] ) ) {
				/** @var string $required_version */
				$required_version = $check_args['version'];
				$version          = $this->get_plugin_data( $slug, 'Version' );

				// If its higher than the required version, we can bail now > true.
				if ( version_compare( $version, $required_version, '>=' ) ) {
					return true;
				} else {
					// If not updated, report the failure and bail > false.
					$this->report_failure(
						array_merge(
							$check_args,
							[
								// Report not_updated status.
								'not_updated' => true,
							]
						)
					);
					return false;
				}
			} else {
				// If the plugin is active, with no required version, were done > true.
				return true;
			}
		}

		/** @var bool $check_installed */
		$check_installed = $check_args['check_installed'];
		if ( $check_installed ) {
			// Check if installed, if so the plugin is not activated.
			/** @var string $name */
			$name = $check_args['name'];
			if ( $name === $this->get_plugin_data( $slug, 'Name' ) ) {
				$this->report_failure(
					array_merge(
						$check_args,
						[
							// Report not_activated status.
							'not_activated' => true,
						]
					)
				);
			} else {
				$this->report_failure(
					array_merge(
						$check_args,
						[
							// Report not_installed status.
							'not_installed' => true,
						]
					)
				);
			}
		}

		return false;
	}

	/**
	 * Internally cached get_plugin_data/get_file_data wrapper.
	 *
	 * @param string      $slug Plugins `folder/file.php` slug.
	 * @param string|null $header Specific plugin header needed.
	 * @return ($header is null ? array<string, string> : string|null)
	 */
	private function get_plugin_data( $slug, $header = null ) {
		if ( ! isset( static::$cache['get_plugin_data'][ $slug ] ) ) {
			$headers = \get_file_data(
				WP_PLUGIN_DIR . '/' . $slug,
				[
					'Name'    => 'Plugin Name',
					'Version' => 'Version',
				],
				'plugin'
			);

			static::$cache['get_plugin_data'][ $slug ] = $headers;
		}

		$plugin_data = static::$cache['get_plugin_data'][ $slug ];

		if ( empty( $header ) ) {
			return $plugin_data;
		}

		return isset( $plugin_data[ $header ] ) ? $plugin_data[ $header ] : null;
	}

	/**
	 * Check if plugin is active.
	 *
	 * @param string $slug Slug to check for.
	 *
	 * @return bool
	 */
	protected function plugin_is_active( $slug ) {
		/** @var string[] $active_plugins */
		$active_plugins = get_option( 'active_plugins', [] );

		return in_array( $slug, $active_plugins, true );
	}


	/**
	 * Get php error message.
	 *
	 * @param array<string, mixed> $failed_check_args Check arguments containing version requirement.
	 *
	 * @return string
	 */
	public function get_php_message( $failed_check_args ) {
		/* translators: 1. PHP Version */
		$message = __( 'This plugin requires <b>PHP %s</b> or higher in order to run.', 'popup-maker' );
		/** @var string $version */
		$version = $failed_check_args['version'];
		return sprintf( $message, $version );
	}

	/**
	 * Get plugin error message.
	 *
	 * @param array<string, mixed> $failed_check_args Plugin failure check arguments.
	 *
	 * @return string
	 */
	public function get_plugin_message( $failed_check_args ) {
		/** @var string $slug */
		$slug = $failed_check_args['slug'];
		// Without file path.
		$short_slug = explode( '/', $slug );
		$short_slug = $short_slug[0];
		/** @var string $name */
		$name = $failed_check_args['name'];
		/** @var string $dep_label */
		$dep_label = $failed_check_args['dep_label'];

		if ( isset( $failed_check_args['not_activated'] ) ) {
			$url  = esc_url( wp_nonce_url( admin_url( 'plugins.php?action=activate&plugin=' . $slug ), 'activate-plugin_' . $slug ) );
			$link = '<a href="' . $url . '">' . __( 'activate it', 'popup-maker' ) . '</a>';

			$text = sprintf(
				/* translators: 1. Plugin Name, 2. Required Plugin Name, 4. `activate it` link. */
				__( 'The plugin "%1$s" requires %2$s! Please %3$s to continue!', 'popup-maker' ),
				$dep_label,
				'<strong>' . $name . '</strong>',
				$link
			);
		} elseif ( isset( $failed_check_args['not_updated'] ) ) {
			$url  = esc_url( wp_nonce_url( admin_url( 'update.php?action=upgrade-plugin&plugin=' . $slug ), 'upgrade-plugin_' . $slug ) );
			$link = '<a href="' . $url . '">' . __( 'update it', 'popup-maker' ) . '</a>';

			$text = sprintf(
				/* translators: 1. Plugin Name, 2. Required Plugin Name, 3. Version number, 4. `update it` link. */
				__( 'The plugin "%1$s" requires %2$s v%3$s or higher! Please %4$s to continue!', 'popup-maker' ),
				$dep_label,
				'<strong>' . $name . '</strong>',
				'<strong>' . ( $failed_check_args['version'] ?? '' ) . '</strong>',
				$link
			);
		} else {
			$url  = esc_url( wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $short_slug ), 'install-plugin_' . $short_slug ) );
			$link = '<a href="' . $url . '">' . __( 'install it', 'popup-maker' ) . '</a>';

			$text = sprintf(
				/* translators: 1. Plugin Name, 2. Required Plugin Name, 3. `install it` link. */
				__( 'The plugin "%1$s" requires %2$s! Please %3$s to continue!', 'popup-maker' ),
				$dep_label,
				'<strong>' . $name . '</strong>',
				$link
			);
		}

		return $text;
	}

	/**
	 * Render needed admin notices.
	 *
	 * @return void
	 */
	public function render_notices() {
		foreach ( $this->failures as $failure ) {
			/** @var array{type: string} $failure */
			$class   = 'notice notice-error';
			$message = method_exists( $this, 'get_' . $failure['type'] . '_message' ) ? $this->{'get_' . $failure['type'] . '_message'}( $failure ) : false;

			/* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped */
			printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), $message );
		}
	}
}
