<?php
/**
 * Handles core integrity scan.
 *
 * @package WP_Defender\Behavior\Scan
 */

namespace WP_Defender\Behavior\Scan;

use WP_Error;
use Countable;
use WP_Defender\Traits\IO;
use Calotes\Base\Component;
use WP_Defender\Model\Scan;
use WP_Defender\Traits\Theme;
use WP_Defender\Traits\Plugin;
use WP_Defender\Model\Scan_Item;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
use WP_Defender\Component\Scan as Scan_Component;

/**
 * Represents a component responsible for checking known vulnerabilities.
 */
class Known_Vulnerability extends Component {

	use IO;
	use Plugin;
	use Theme;

	/**
	 * Holds the Scan model or null if not set.
	 *
	 * @var Scan|null
	 */
	private ?Scan $scan;

	/**
	 * Holds the WPMUDEV object or null if not set.
	 *
	 * @var WPMUDEV|null
	 */
	private ?WPMUDEV $wpmudev;

	/**
	 * Initializes the Known_Vulnerability class with the WPMUDEV and Scan instances.
	 *
	 * @param  WPMUDEV $wpmudev  The WPMUDEV object.
	 * @param  Scan    $scan  The Scan model.
	 */
	public function __construct( WPMUDEV $wpmudev, Scan $scan ) {
		$this->wpmudev = $wpmudev;
		$this->scan    = $scan;
	}

	/**
	 * Performs the vulnerability check.
	 *
	 * @return bool
	 */
	public function vuln_check(): bool {
		global $wp_version;
		$info_cached = array();
		$data        = array(
			'plugins'   => wp_json_encode( $this->gather_fact( 'plugin', $info_cached ) ),
			'themes'    => wp_json_encode( $this->gather_fact( 'theme', $info_cached ) ),
			'wordpress' => $wp_version,
		);

		$ret = $this->make_check( $data );
		if ( is_wp_error( $ret ) ) {
			$this->handle_error( $ret );

			return true;
		}

		$ignored_issues = $this->get_ignored_issues();
		$this->process_results( $ret, $info_cached, $ignored_issues );
		$this->scan->calculate_percent( 100, 5 );
		$this->add_ignored_issues( $ignored_issues );

		return true;
	}

	/**
	 * Gathers information about installed plugins or themes.
	 *
	 * @param  string $type  The type of information to gather (plugin or theme).
	 * @param  array  $info_cached  The cached information.
	 *
	 * @return array
	 */
	private function gather_fact( string $type, &$info_cached ): array {
		$items = array();

		if ( 'plugin' === $type ) {
			$actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
			if ( Scan_Component::are_actioned_plugins( $actioned_plugins ) ) {
				foreach ( $actioned_plugins as $slug => $plugin_data ) {
					$items[ $slug ]       = $plugin_data['Version'];
					$info_cached[ $slug ] = array( $plugin_data['Name'], $plugin_data['Version'], $plugin_data['Slug'] );
				}
			} else {
				// An emergency option if for some reason the plugin's quick data is not saved in the first step.
				foreach ( $this->get_plugins() as $slug => $plugin ) {
					$base_slug                 = $this->get_plugin_slug_by( $slug );
					$items[ $base_slug ]       = $plugin['Version'];
					$info_cached[ $base_slug ] = array( $plugin['Name'], $plugin['Version'], $slug );
				}
			}
		} elseif ( 'theme' === $type ) {
			$model = Scan::get_last();
			foreach ( $this->get_themes() as $slug => $item ) {
				// It's actual because we don't have ignored theme slugs.
				if ( is_object( $model ) && $model->is_issue_ignored( $slug ) ) {
					continue;
				}

				$base_slug                 = explode( '/', $slug );
				$base_slug                 = array_shift( $base_slug );
				$items[ $base_slug ]       = $item['Version'];
				$info_cached[ $base_slug ] = array( $item['Name'], $item['Version'], $slug );
			}
		} else {
			return $items;
		}

		return $items;
	}

	/**
	 * Makes a request to the WPMUDEV API to check for known vulnerabilities.
	 *
	 * @param  mixed $data  The data to send in the request.
	 *
	 * @return array|WP_Error
	 */
	protected function make_check( $data ) {
		return $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_KNOWN_VULN, $data, array( 'method' => 'POST' ) );
	}

	/**
	 * Handles the error by tracking the failure event, logging the error message, and updating the scan status.
	 *
	 * @param  WP_Error $error  The error object containing the error message.
	 */
	private function handle_error( WP_Error $error ) {
		$error_message  = $error->get_error_message();
		$scan_analytics = wd_di()->get( Scan_Analytics::class );

		$scan_analytics->track_feature(
			Scan_Analytics::EVENT_SCAN_FAILED,
			array(
				Scan_Analytics::EVENT_SCAN_FAILED_PROP => Scan_Analytics::EVENT_SCAN_FAILED_ERROR,
				'Error_Reason'                         => $error_message,
			)
		);

		$this->log( $error_message, \WP_Defender\Controller\Scan::SCAN_LOG );
		$this->scan->status = Scan::STATUS_ERROR;
		$this->scan->save();
	}

	/**
	 * Retrieves the list of ignored issues.
	 *
	 * @return array The list of ignored issues.
	 */
	private function get_ignored_issues() {
		$last = Scan::get_last();

		return is_object( $last ) ? $last->get_issues(
			Scan_Item::TYPE_VULNERABILITY,
			Scan_Item::STATUS_IGNORE
		) : array();
	}

	/**
	 * Processes the results of the vulnerability check and saves them in the Scan model.
	 *
	 * @param  array $results  The results of the vulnerability check.
	 * @param  array $info_cached  The cached information.
	 * @param  array $ignored_issues  The ignored issues.
	 */
	private function process_results( $results, $info_cached, $ignored_issues ) {
		$this->process_result( $results['plugins'], $info_cached, 'plugin', $ignored_issues );
		$this->process_result( $results['themes'], $info_cached, 'theme', $ignored_issues );
		$this->process_result( $results['wordpress'], array(), 'wp_core', $ignored_issues );
	}

	/**
	 * Processes the result of the vulnerability check and saves them in the Scan model.
	 *
	 * @param  mixed  $result  The result of the vulnerability check.
	 * @param  array  $info  The cached information.
	 * @param  string $type  The type of the vulnerability check.
	 * @param  array  $ignored_issues  The ignored issues.
	 */
	private function process_result( $result, $info, $type, $ignored_issues ) {
		if ( ! is_array( $result ) || array() === $result ) {
			return;
		}

		$this->log( sprintf( 'Checking %s:', $type ) );
		$model = $this->scan;

		foreach ( $result as $base_slug => $bugs ) {
			if ( 'wp_core' === $type ) {
				global $wp_version;
				$is_exist = false;
				if ( is_array( $ignored_issues ) && array() !== $ignored_issues ) {
					foreach ( $ignored_issues as $issue ) {
						if ( $wp_version === $issue->raw_data['version'] ) {
							$is_exist = true;
						}
					}
				}

				if ( ! $is_exist ) {
					$raw_data = array(
						'type'          => $type,
						'slug'          => '',
						'base_slug'     => '',
						'version'       => $wp_version,
						'name'          => 'WordPress Core',
						'bugs'          => array(
							array(
								'vuln_type'  => $bugs['vuln_type'],
								'title'      => $bugs['title'],
								'ref'        => $bugs['references'],
								'fixed_in'   => $bugs['fixed_in'],
								'cvss_score' => $bugs['cvss']['score'] ?? '',
							),
						),
						'new_structure' => '3.4.0',
					);

					$model->add_item( Scan_Item::TYPE_VULNERABILITY, $raw_data );
				}
			} elseif ( 'plugin' === $type || 'theme' === $type ) {
				if ( ! isset( $info[ $base_slug ] ) || ! is_array( $info[ $base_slug ] ) ) {
					continue;
				}

				if ( isset( $bugs['confirmed'] ) && ( is_array( $bugs['confirmed'] ) || $bugs['confirmed'] instanceof Countable ? count( $bugs['confirmed'] ) : 0 ) ) {
					$wp_org_plugin_details = array(
						'is_existed'   => false,
						'last_version' => '0',
					);
					if ( 'plugin' === $type ) {
						if ( ! $this->is_likely_wporg_slug( $base_slug ) ) {
							continue;
						}
						// Additional conditions for wp.org plugin.
						$results = $this->handle_wp_org_response_by( $base_slug );
						if ( $results['success'] && isset( $results['body']['version'] ) ) {
							$wp_org_plugin_details = array(
								'is_existed'   => true,
								'last_version' => $results['body']['version'],
							);
						}
						// If there is not a plugin version, likely the plugin is closed. We'd not display Update button.
					}

					[ $name, $current_version, $slug ] = $info[ $base_slug ];
					$raw_data                          = array(
						'type'      => $type,
						'slug'      => $slug,
						'base_slug' => $base_slug,
						'version'   => $current_version,
						'name'      => $name,
						'bugs'      => array(),
					);

					$confirmed_bugs = (array) $bugs['confirmed'];
					// @since 3.4.0 Restructure the view in relation to the new CVSS Score field.
					$raw_data['new_structure'] = '3.4.0';
					$this->log( sprintf( '%s has %d known bugs', $slug, count( $confirmed_bugs ) ) );

					foreach ( $confirmed_bugs as $bug ) {
						if ( 'plugin' === $type && $wp_org_plugin_details['is_existed']
							&& version_compare( $wp_org_plugin_details['last_version'], $bug['fixed_in'], '<=' )
						) {
							// This is another plugin but with the same slug.
							continue;
						}
						$raw_data['bugs'][] = array(
							'vuln_type'  => $bug['vuln_type'],
							'title'      => $bug['title'],
							'ref'        => $bug['references'],
							'fixed_in'   => $bug['fixed_in'],
							'cvss_score' => $bug['cvss']['score'] ?? '',
						);
					}

					$model->add_item( Scan_Item::TYPE_VULNERABILITY, $raw_data );
				}
			}
		}
	}

	/**
	 * Adds ignored vulnerability issues to the scan.
	 *
	 * @param  array $ignored_issues  The array of ignored vulnerability issues to add.
	 */
	private function add_ignored_issues( $ignored_issues ) {
		if ( is_array( $ignored_issues ) && array() !== $ignored_issues ) {
			foreach ( $ignored_issues as $issue ) {
				$this->scan->add_item( Scan_Item::TYPE_VULNERABILITY, $issue->raw_data, Scan_Item::STATUS_IGNORE );
			}
		}
	}
}
