<?php
/**
 * Handles malware quick scan.
 *
 * @package WP_Defender\Behavior\Scan
 */

namespace WP_Defender\Behavior\Scan;

use Countable;
use WP_Defender\Traits\IO;
use Calotes\Component\Behavior;
use WP_Defender\Behavior\Scan\Malware_Scan;
use WP_Filesystem_Base;

/**
 * This will fast travel all yara rules to quickly get files seem suspicious, this is catchy.
 */
class Malware_Quick_Scan extends Behavior {

	use IO;

	/**
	 * Holds the content of the file being scanned.
	 *
	 * @var string
	 */
	private $content;

	/**
	 * Perform a quick scan on the specified file using the provided rules.
	 *
	 * @param  string $file  The file to scan.
	 * @param  array  $rules  The rules to apply during the scan.
	 *
	 * @return array An array containing the total number of issues found and detailed information about each issue.
	 */
	public function do_quick_scan( string $file, array $rules ): array {
		global $wp_filesystem;
		// Initialize the WP filesystem, no more using 'file-put-contents' function.
		if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$total_issues = 0;
		$qs_detail    = array();
		if ( file_exists( $file ) ) {
			$content       = $wp_filesystem->get_contents( $file );
			$this->content = $content;
			foreach ( $rules as $rule ) {
				$strings      = $rule['strings'];
				$local_issues = array(
					'identifier' => $rule['identifier'],
					'catches'    => array(),
				);
				foreach ( $strings as $string ) {
					$offset = false;
					if ( 0 === $string['type'] ) {
						$offset = strpos( $content, $string['text'] );
						$text   = $string['text'];
					} else {
						$pattern  = "/{$string['text']}/";
						$is_match = preg_match_all(
							$pattern,
							$content,
							$matches,
							PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE
						);
						if ( $is_match ) {
							$offset = $matches[0][0][1];
							$text   = $matches[0][0][0];
						}
					}
					if ( false === $offset ) {
						continue;
					}
					$local_issues['catches'][] = array(
						'id'     => $string['id'],
						'text'   => $text,
						'offset' => $offset,
					);
				}
				if ( array() === $local_issues['catches'] ) {
					continue;
				}

				if ( $this->condition_valid( $local_issues['catches'], $rule ) ) {
					++$total_issues;
					$qs_detail[] = $local_issues;
				}
			}
		}

		return array( $total_issues, $qs_detail );
	}

	/**
	 * Checks if the condition is valid based on the given rule.
	 *
	 * @param  array $found  The found items.
	 * @param  array $rule  The rule to check against.
	 *
	 * @return bool Returns true if the condition is valid, false otherwise.
	 */
	private function condition_valid( $found, $rule ): bool {
		$condition = explode( 'and', $rule['condition'] );
		$count     = 0;
		foreach ( $condition as $item ) {
			if ( preg_match( '/([a-z0-9]+)\s*of\s*(.*)/', $item, $match ) ) {
				++$count;
				$ret = $this->x_of_them( $found, $match, $rule );
				if ( false === $ret ) {
					return false;
				}
			}

			if ( preg_match( '/(\$[a-zA-Z0-9]+)\s*at\s*(\d+)/', $item, $match ) ) {
				++$count;
				$ret = $this->id_at_pos( $found, $match, $rule );
				if ( false === $ret ) {
					return false;
				}
			}
		}
		if ( 0 === $count ) {
			$this->log( $rule['identifier'], Malware_Scan::MALWARE_LOG );
			$this->log( $found, Malware_Scan::MALWARE_LOG );

			return false;
		}

		return true;
	}

	/**
	 * Determines if a certain number of items in an array meet a certain condition.
	 *
	 * @param  array $found  The array of items to check.
	 * @param  array $check_info  The match array containing the number of items to check and the condition.
	 * @param  array $rule  The rule array containing the strings to check against.
	 *
	 * @return bool Returns true if the number of items that meet the condition is greater than or equal to the
	 *     specified number, false otherwise.
	 */
	private function x_of_them( $found, $check_info, $rule ): bool {
		$num = $check_info[1];
		$num = 'all' === $num ? count( $rule['strings'] ) : $num;
		$num = 'any' === $num ? 1 : $num;

		if ( '(s*)' === $check_info[2] ) {
			// Hard code in this situation.
			--$num;
		}
		/**
		 * Todo: uncomment the following lines if you need to get log details.
		 * $this->log( sprintf( 'num %s', $num ), Malware_Scan::MALWARE_LOG );
		 * $this->log( sprintf( 'count %s', count( $found ) ), Malware_Scan::MALWARE_LOG );
		 */
		return ( is_array( $found ) || $found instanceof Countable ? count( $found ) : 0 ) >= $num;
	}

	/**
	 * Checks if the given ID is at the specified position in the content.
	 *
	 * @param  array $found       The array of items to check.
	 * @param  array $check_info  The match array containing the ID and position to check.
	 * @param  array $rule        The rule array containing the strings to check against.
	 *
	 * @return bool Returns true if the ID is at the specified position in the content, false otherwise.
	 */
	private function id_at_pos( $found, $check_info, $rule ): bool {
		// Get the text to check.
		foreach ( $found as $item ) {
			if ( $item['id'] === $check_info[1] ) {
				return strpos( $this->content, $item['text'] ) === $check_info[2];
			}
		}

		return false;
	}
}
