<?php

namespace WP_Defender\Behavior\Scan;

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

class Known_Vulnerability extends Component {
	use IO;
	use Plugin;
	use Theme;

	/**
	 * @var WPMUDEV|null
	 */
	private ?WPMUDEV $wpmudev;

	/**
	 * @var Scan|null
	 */
	private ?Scan $scan;

	/**
	 * Initializes the Known_Vulnerability class with the WPMUDEV and Scan instances.
	 *
	 * @param WPMUDEV $wpmudev
	 * @param Scan $scan
	 */
	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 = [];
		$data        = [
			'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 false;
		}

		$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
	 *
	 * @return array
	 */
	private function gather_fact( string $type, &$info_cached ): array {
		$items = [];
		$model = Scan::get_last();

		if ( $type === 'plugin' ) {
			$get_items = $this->get_plugins();
		} elseif ( $type === 'theme' ) {
			$get_items = $this->get_themes();
		} else {
			return $items;
		}

		foreach ( $get_items as $slug => $item ) {
			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 ] = [ $item['Name'], $item['Version'], $slug ];
		}

		return $items;
	}

	/**
	 * Makes a request to the WPMUDEV API to check for known vulnerabilities.
	 *
	 * @param mixed $data
	 *
	 * @return array|WP_Error
	 */
	private function make_check( $data ) {
		return $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_KNOWN_VULN, $data, [ '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,
			[
				Scan_Analytics::EVENT_SCAN_FAILED_PROP => Scan_Analytics::EVENT_SCAN_FAILED_ERROR,
				'Error_Reason'                         => $error_message,
			]
		);

		$this->log( $error_message, '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 ) : [];
	}

	/**
	 * 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'], [], 'wp_core', $ignored_issues );
	}

	/**
	 * Processes the results of the vulnerability check and saves them in the Scan model.
	 *
	 * @param mixed $result
	 * @param array $info
	 * @param string $type
	 * @param array $ignored_issues
	 */
	private function process_result( $result, $info, $type, $ignored_issues ) {
		if ( empty( $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 ( ! empty( $ignored_issues ) ) {
					foreach ( $ignored_issues as $issue ) {
						if ( $wp_version === $issue->raw_data['version'] ) {
							$is_exist = true;
						}
					}
				}

				if ( ! $is_exist ) {
					$raw_data = [
						'type'          => $type,
						'slug'          => '',
						'base_slug'     => '',
						'version'       => $wp_version,
						'name'          => 'WordPress Core',
						'bugs'          => [
							[
								'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 ( empty( $info[ $base_slug ] ) ) {
					continue;
				}

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

				if ( isset( $bugs['confirmed'] ) && ( is_array( $bugs['confirmed'] ) || $bugs['confirmed'] instanceof Countable ? count( $bugs['confirmed'] ) : 0 ) ) {
					if ( ! $this->is_likely_wporg_slug( $base_slug ) ) {
						continue;
					}

					$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 ) {
						$raw_data['bugs'][] = [
							'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 ( ! empty( $ignored_issues ) ) {
			foreach ( $ignored_issues as $issue ) {
				$this->scan->add_item( Scan_Item::TYPE_VULNERABILITY, $issue->raw_data, Scan_Item::STATUS_IGNORE );
			}
		}
	}
}
