<?php

namespace WP_Defender\Behavior\Scan;

use Calotes\Base\Component;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Component\Timer;
use WP_Defender\Model\Scan;
use WP_Defender\Model\Scan_Item;
use WP_Defender\Traits\IO;
use WP_Defender\Model\Setting\Scan as Scan_Settings;
use WP_Defender\Model\Scan as Model_Scan;
use WP_Defender\Traits\Plugin;
use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;

class Malware_Scan extends Component {
	use IO, Plugin;

	public const YARA_RULES = 'defender_yara_rules';

	/**
	 * Backup memory.
	 * @var string
	 */
	private $memory;

	/**
	 * @var WPMUDEV
	 */
	private $wpmudev;

	/**
	 * @var Scan
	 */
	private $scan;

	public function __construct( WPMUDEV $wpmudev, Scan $scan ) {
		$this->wpmudev = $wpmudev;
		$this->scan = $scan;
	}

	/**
	 * @param Scan_Settings $scan_settings
	 *
	 * @return array
	 */
	protected function get_additional_rules( Scan_Settings $scan_settings ): array {
		$plugin_cache = false;
		$plugin_slugs_changes = [];
		$plugin_pro_slugs = [];
		$plugin_all_slugs = [];
		// Checked Plugin option.
		if ( $scan_settings->integrity_check && $scan_settings->check_plugins ) {
			$plugin_cache = true;
			$arr = get_site_option( Plugin_Integrity::PLUGIN_SLUGS, false );
			if ( is_array( $arr ) && ! empty( $arr ) ) {
				$plugin_slugs_changes = $arr;
			}
			$plugin_slugs = get_site_option( Plugin_Integrity::PLUGIN_PREMIUM_SLUGS, false );
			if ( is_array( $plugin_slugs ) && ! empty( $plugin_slugs ) ) {
				$plugin_pro_slugs = $plugin_slugs;
			}
			$plugin_all_slugs = $this->get_plugin_slugs();
		}

		return [
			'plugin_change' => $plugin_cache,
			// List of plugins with modifications.
			'plugin_slugs_changes' => $plugin_slugs_changes,
			// List of pro plugins.
			'plugin_pro_slugs' => $plugin_pro_slugs,
			'plugin_all_slugs' => $plugin_all_slugs,
		];
	}

	/**
	 * @param string $file_path
	 * @param array  $rules
	 *
	 * @return bool
	 */
	protected function was_modificated_file( $file_path, $rules ): bool {
		// Unchecked 'Plugin change file' option, so green light to display Suspicious checks.
		if ( ! $rules['plugin_change'] ) {
			return true;
		}

		$search_on_plugin = WP_PLUGIN_DIR . '/';
		if ( false !== stripos( $file_path, $search_on_plugin ) ) {
			/**
			 * Suspicious code in /plugins.
			 * Empty list of plugin slugs because there are premium plugins,
			 * not modifications on Free plugins,
			 * or not plugins on site.
			 * Should check separate custom files/dirs in the root too.
			 */
			$rev_file = str_replace( $search_on_plugin, '', $file_path );
			$matches = explode( '/', $rev_file );
			$base_slug = array_shift( $matches );
			// Custom files/dirs.
			if ( ! in_array( $base_slug, $rules['plugin_all_slugs'], true ) ) {
				return true;
			}
			if ( empty( $rules['plugin_slugs_changes'] ) && empty( $rules['plugin_pro_slugs'] ) ) {
				// No modifications.
				return false;
			}
			// Is it on premium plugins?
			if ( in_array( $base_slug, $rules['plugin_pro_slugs'], true ) ) {
				return true;
			}
			if ( in_array( $base_slug, (array) $rules['plugin_slugs_changes'], true ) ) {
				// Modifications in this plugin.
				return true;
			}
			// Modifications are not here.
			return false;
		}
		// Other WP places.
		return true;
	}

	/**
	 * @return bool
	 */
	public function suspicious_check( Malware_Quick_Scan $quick_scan, Malware_Deep_Scan $deep_scan ): bool {
		$files = get_site_option( Gather_Fact::CACHE_CONTENT, [] );
		if ( empty( $files ) ) {
			return true;
		}

		set_time_limit( 0 );
		$this->prepare_emergency_shutdown();
		$timer = new Timer();
		$rules = $this->fetch_yara_rules();

		$model = $this->scan;
		$pos = (int) $model->task_checkpoint;

		$combinations = $this->get_additional_rules( new Scan_Settings() );
		$files = new \ArrayIterator( $files );
		$files->seek( $pos );
		while ( $files->valid() ) {
			if ( ! $timer->check() ) {
				$reason = 'Rage quit';

				/**
				 * @var Scan_Analytics
				 */
				$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' => $reason,
					]
				);

				$this->log( $reason, 'malware_scan.log' );
				$model->save();
				break;
			}
			if ( $model->is_issue_ignored( $files->current() ) ) {
				$this->log( sprintf( 'skip %s because of file is ignored', $files->current() ), 'malware_scan.log' );
				$files->next();
				continue;
			}

			[ $result, $qs_detail ] = $quick_scan->do_quick_scan( $files->current(), $rules );
			if ( $result ) {
				$this->log( sprintf( 'file %s suspicious', $files->current() ), 'malware_scan.log' );
				$result = $deep_scan->do_deep_scan( $files->current(), $rules, $qs_detail );
				/**
				 * Add new item if Suspicious code is found and:
				 * plugins are premium,
				 * plugins are on wp.org but the code doesn't match from the WP repo (there are differences in checksums),
				 * deactivated options of File change detection > Scan plugin file changes.
				*/
				if ( is_array( $result ) && $this->was_modificated_file( $files->current(), $combinations ) ) {
					$result['file'] = $files->current();
					$model->add_item( Scan_Item::TYPE_SUSPICIOUS, $result );
				}
			}
			$files->next();
			$files_key = $files->key();
			$model->task_checkpoint = ! is_null( $files_key ) ? $files_key : '';
			$model->calculate_percent( $files_key * 100 / $files->count(), 6 );
			if ( 0 === $files_key % 100 ) {
				// We should update the model percent each 100 files so we have some progress ont he screen$pos * 100 / $core_files->count().
				$model->save();
			}
		}

		if ( ! $files->valid() ) {
			$last = Model_Scan::get_last();
			if ( is_object( $last ) ) {
				$ignored_issues = $last->get_issues( Scan_Item::TYPE_SUSPICIOUS, Scan_Item::STATUS_IGNORE );
				foreach ( $ignored_issues as $issue ) {
					$this->scan->add_item(
						Scan_Item::TYPE_SUSPICIOUS,
						$issue->raw_data,
						Scan_Item::STATUS_IGNORE
					);
				}
			}
			$model->task_checkpoint = '';
		}

		$model->save();

		return ! $files->valid();
	}

	/**
	 * We will use this for a safe switch when memory out happen.
	 */
	public function prepare_emergency_shutdown() {
		$this->memory = str_repeat( '*', 1024 * 1024 );

		register_shutdown_function(
			function () {
				if ( Model_Scan::STATUS_FINISH === $this->scan->status ) {
					return;
				}

				$this->memory = null;
				$err = error_get_last();
				if (
					( ! is_null( $err ) )
					&& ( ! in_array( $err['type'], [ E_NOTICE, E_WARNING, E_DEPRECATED ], true ) )
				) {
					$this->log( var_export( $err, true ), 'scan.log' );
					$this->log( 'Something wrong happen, saving and quit.', 'scan.log' );
					$this->scan->status = Model_Scan::STATUS_ERROR;
					++$this->scan->task_checkpoint;
					$this->scan->save();
				}
			}
		);
	}

	/**
	 * Fetch yara rules from API.
	 *
	 * @return array
	 */
	private function fetch_yara_rules(): array {
		$rules = get_site_option( self::YARA_RULES, false );
		if ( is_array( $rules ) ) {
			return $rules;
		}

		$rules = $this->wpmudev->make_wpmu_request( WPMUDEV::API_SCAN_SIGNATURE );
		if ( is_array( $rules ) ) {
			update_site_option( self::YARA_RULES, $rules );

			return $rules;
		}

		return [];
	}
}
