<?php
/**
 * Bearmor Malware Scanner
 *
 * @package Bearmor_Security
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Scans files for malware patterns
 */
class Bearmor_Malware_Scanner {

	/**
	 * File extensions to scan
	 */
	private static $scan_extensions = array( 'php', 'js', 'html', 'htm' );

	/**
	 * Excluded directories (same as file scanner)
	 */
	private static $excluded_patterns = array(
		'/wp-content/uploads/',
		'/wp-content/cache/',
		'/uploads/bearmor-security/quarantine/',
		'/wp-content/plugins/bearmor-security/', // Exclude our own plugin
		'/bearmor-',    // Exclude all bearmor files
		'.log',
		'.tmp',
		'.cache',
		'/node_modules/',
		'/vendor/',
		'.min.js',      // Minified JavaScript
		'.min.css',     // Minified CSS
		'-min.js',      // Alternative minified format
	);

	/**
	 * Known safe plugins that use shell/exec functions legitimately
	 */
	private static $safe_plugins = array(
		'duplicator',
		'updraftplus',
		'wpvivid',
		'wpvivid-backuprestore',
		'backupbuddy',
		'all-in-one-wp-migration',
		'migrate-guru',
		'wp-database-backup',
		'backwpup',
		'mainwp-child',
		'google-site-kit',
		'w3-total-cache',
		'insert-headers-and-footers',
		'sendsmaily-subscription-opt-in-form',
		'sitepress-multilingual-cms',
		'woocommerce',
		'wordfence',
		'seo-by-rank-math',
		'tablepress',
		'mailchimp-for-woocommerce',
		'advanced-custom-fields',
		'secure-custom-fields',
		'revslider',
		'woocommerce-payments',
		'livecanvas',
		'jetpack',
		'js_composer',
		'wpml',
		'wpml-translation-management',
		'wpml-string-translation',
		'wpml-media-translation',
		'wpml-cms-nav',
		'autoptimize',
		'nextgen-gallery',
		'bb-plugin',
		// Security plugins
		'sucuri-scanner',
		'malcare-security',
		'all-in-one-wp-security-and-firewall',
		'better-wp-security',
	);

	/**
	 * WordPress core files that are known to have legitimate dangerous functions
	 */
	private static $wp_core_whitelist = array(
		'wp-includes/class-snoopy.php',
		'wp-includes/class-pop3.php',
		'wp-includes/class-wp-http-curl.php',
		'wp-includes/class-wp-http-encoding.php',
		'wp-includes/class-wp-recovery-mode-cookie-service.php',
		'wp-includes/class-wp-customize-widgets.php',
		'wp-includes/class-wp-simplepie-sanitize-kses.php',
		'wp-includes/load.php',
		'wp-admin/user-edit.php',
		'xmlrpc.php',
	);

	/**
	 * Run full malware scan
	 *
	 * @return array Scan results
	 */
	public static function run_scan() {
		$results = array(
			'scanned'   => 0,
			'threats'   => 0,
			'time'      => 0,
			'files'     => array(),
		);

		$start_time = microtime( true );
		error_log( 'Bearmor Malware Scan: Starting scan...' );

		// Scan WP core
		$core_files = self::get_files_to_scan( ABSPATH, 1 ); // Max depth 1 for root
		error_log( 'Bearmor Malware Scan: Found ' . count( $core_files ) . ' core files to scan' );
		foreach ( $core_files as $file ) {
			$results['scanned']++;
			$threats = self::scan_file( $file );
			if ( ! empty( $threats ) ) {
				error_log( 'Bearmor Malware Scan: Found ' . count( $threats ) . ' threats in ' . $file );
				$results['threats'] += count( $threats );
				$results['files'][ $file ] = $threats;
			}
		}

		// Scan plugins
		$plugin_files = self::get_files_to_scan( WP_PLUGIN_DIR );
		error_log( 'Bearmor Malware Scan: Found ' . count( $plugin_files ) . ' plugin files to scan' );
		foreach ( $plugin_files as $file ) {
			$results['scanned']++;
			$threats = self::scan_file( $file );
			if ( ! empty( $threats ) ) {
				error_log( 'Bearmor Malware Scan: Found ' . count( $threats ) . ' threats in ' . $file );
				$results['threats'] += count( $threats );
				$results['files'][ $file ] = $threats;
			}
		}

		// Scan themes
		$theme_files = self::get_files_to_scan( get_theme_root() );
		foreach ( $theme_files as $file ) {
			$results['scanned']++;
			$threats = self::scan_file( $file );
			if ( ! empty( $threats ) ) {
				$results['threats'] += count( $threats );
				$results['files'][ $file ] = $threats;
			}
		}

		// Scan mu-plugins if exists
		if ( is_dir( WPMU_PLUGIN_DIR ) ) {
			$mu_files = self::get_files_to_scan( WPMU_PLUGIN_DIR );
			foreach ( $mu_files as $file ) {
				$results['scanned']++;
				$threats = self::scan_file( $file );
				if ( ! empty( $threats ) ) {
					$results['threats'] += count( $threats );
					$results['files'][ $file ] = $threats;
				}
			}
		}

		$results['time'] = round( microtime( true ) - $start_time, 2 );

		error_log( 'Bearmor Malware Scan: Completed - Scanned: ' . $results['scanned'] . ', Threats: ' . $results['threats'] . ', Time: ' . $results['time'] . 's' );
		error_log( 'Bearmor Malware Scan: Threat files count: ' . count( $results['files'] ) );

		// Store results
		self::store_scan_results( $results );

		return $results;
	}

	/**
	 * Scan a single file for malware patterns
	 *
	 * @param string $file_path Full path to file.
	 * @return array Array of detected threats
	 */
	public static function scan_file( $file_path ) {
		if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
			return array();
		}

		// Check if file is whitelisted (user whitelist)
		if ( self::is_whitelisted( $file_path ) ) {
			return array();
		}

		// Check if file is in WP core whitelist
		$relative_path = str_replace( ABSPATH, '', $file_path );
		if ( in_array( $relative_path, self::$wp_core_whitelist, true ) ) {
			return array();
		}

		// Check if file belongs to safe plugin
		if ( self::is_safe_plugin_file( $file_path ) ) {
			return array();
		}

		$original_content = file_get_contents( $file_path );
		if ( $original_content === false ) {
			return array();
		}

		// Strip PHP comments to avoid false positives, but keep original for line numbers
		$extension = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
		$content_to_scan = $original_content;
		if ( $extension === 'php' ) {
			$content_to_scan = self::strip_php_comments( $original_content );
		}

		$patterns = Bearmor_Malware_Patterns::get_patterns();
		$threats = array();

		foreach ( $patterns as $pattern ) {
			// Skip LOW and MEDIUM severity patterns
			if ( in_array( $pattern['severity'], array( 'low', 'medium' ), true ) ) {
				continue;
			}

			// Skip PHP-specific patterns for JS/CSS files
			if ( in_array( $extension, array( 'js', 'css', 'json' ), true ) ) {
				if ( in_array( $pattern['category'], array( 'shell_execution', 'code_execution', 'obfuscation' ), true ) ) {
					continue; // JS exec() and eval() are normal, not PHP malware
				}
			}

			if ( preg_match( $pattern['pattern'], $content_to_scan, $matches, PREG_OFFSET_CAPTURE ) ) {
				// Map position from stripped content back to original content
				$line_number = self::map_stripped_to_original_line( $original_content, $content_to_scan, $matches[0][1] );
				$code_snippet = self::get_code_snippet( $original_content, $line_number );

				// Remap severity: critical -> high, high -> medium
				$remapped_severity = $pattern['severity'];
				if ( $pattern['severity'] === 'critical' ) {
					$remapped_severity = 'high';
				} elseif ( $pattern['severity'] === 'high' ) {
					$remapped_severity = 'medium';
				}

				$threats[] = array(
					'pattern_id'   => $pattern['id'],
					'pattern_name' => $pattern['name'],
					'severity'     => $remapped_severity,
					'description'  => $pattern['description'],
					'category'     => $pattern['category'],
					'line_number'  => $line_number,
					'code_snippet' => $code_snippet,
					'matched_text' => $matches[0][0],
				);
			}
		}

		return $threats;
	}

	/**
	 * Get files to scan from directory
	 *
	 * @param string $dir Directory path.
	 * @param int    $max_depth Maximum depth (0 = unlimited).
	 * @return array Array of file paths
	 */
	private static function get_files_to_scan( $dir, $max_depth = 0 ) {
		$files = array();

		if ( ! is_dir( $dir ) ) {
			return $files;
		}

		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::SELF_FIRST
		);

		if ( $max_depth > 0 ) {
			$iterator->setMaxDepth( $max_depth );
		}

		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}

			$file_path = $file->getPathname();

			// Check exclusions
			if ( self::should_exclude( $file_path ) ) {
				continue;
			}

			// Check extension
			$extension = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
			if ( ! in_array( $extension, self::$scan_extensions, true ) ) {
				continue;
			}

			$files[] = $file_path;
		}

		return $files;
	}

	/**
	 * Check if file should be excluded
	 *
	 * @param string $file_path File path.
	 * @return bool
	 */
	private static function should_exclude( $file_path ) {
		foreach ( self::$excluded_patterns as $pattern ) {
			if ( strpos( $file_path, $pattern ) !== false ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check if file belongs to a safe plugin
	 *
	 * @param string $file_path File path.
	 * @return bool
	 */
	private static function is_safe_plugin_file( $file_path ) {
		foreach ( self::$safe_plugins as $plugin_slug ) {
			if ( strpos( $file_path, '/wp-content/plugins/' . $plugin_slug . '/' ) !== false ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Map line number from stripped content to original content
	 *
	 * @param string $original_content Original file content with comments.
	 * @param string $stripped_content Content with comments removed.
	 * @param int    $stripped_offset Character offset in stripped content.
	 * @return int Line number in original content
	 */
	private static function map_stripped_to_original_line( $original_content, $stripped_content, $stripped_offset ) {
		// Get the matched text from stripped content
		$stripped_lines = explode( "\n", $stripped_content );
		$stripped_line_num = self::get_line_number( $stripped_content, $stripped_offset );
		
		// Get the line content from stripped version
		if ( isset( $stripped_lines[ $stripped_line_num - 1 ] ) ) {
			$stripped_line_content = trim( $stripped_lines[ $stripped_line_num - 1 ] );
			
			// Find this line in original content
			$original_lines = explode( "\n", $original_content );
			for ( $i = 0; $i < count( $original_lines ); $i++ ) {
				if ( trim( $original_lines[ $i ] ) === $stripped_line_content ) {
					return $i + 1; // Return 1-indexed line number
				}
			}
		}
		
		// Fallback: return stripped line number
		return $stripped_line_num;
	}

	/**
	 * Get line number from character offset
	 *
	 * @param string $content File content.
	 * @param int    $offset Character offset.
	 * @return int Line number
	 */
	private static function get_line_number( $content, $offset ) {
		$lines_before = substr( $content, 0, $offset );
		return substr_count( $lines_before, "\n" ) + 1;
	}

	/**
	 * Get code snippet around line
	 *
	 * @param string $content File content.
	 * @param int    $line_number Line number.
	 * @param int    $context_lines Lines before/after to include.
	 * @return string Code snippet
	 */
	private static function get_code_snippet( $content, $line_number, $context_lines = 2 ) {
		$lines = explode( "\n", $content );
		$start = max( 0, $line_number - $context_lines - 1 );
		$end = min( count( $lines ), $line_number + $context_lines );

		$snippet_lines = array_slice( $lines, $start, $end - $start );
		$snippet = '';

		for ( $i = 0; $i < count( $snippet_lines ); $i++ ) {
			$current_line = $start + $i + 1;
			$prefix = ( $current_line === $line_number ) ? '>>> ' : '    ';
			$snippet .= $prefix . $current_line . ': ' . $snippet_lines[ $i ] . "\n";
		}

		return $snippet;
	}

	/**
	 * Store scan results in database
	 *
	 * @param array $results Scan results.
	 */
	private static function store_scan_results( $results ) {
		global $wpdb;
		$table_name = $wpdb->prefix . 'bearmor_malware_detections';

		// Clear old detections
		$wpdb->query( "TRUNCATE TABLE $table_name" );

		// Insert new detections
		foreach ( $results['files'] as $file_path => $threats ) {
			foreach ( $threats as $threat ) {
				$relative_path = str_replace( ABSPATH, '', $file_path );

				$insert_result = $wpdb->insert(
					$table_name,
					array(
						'file_path'    => $relative_path,
						'pattern_id'   => $threat['pattern_id'],
						'pattern_name' => $threat['pattern_name'],
						'severity'     => $threat['severity'],
						'category'     => $threat['category'],
						'description'  => $threat['description'],
						'line_number'  => $threat['line_number'],
						'code_snippet' => $threat['code_snippet'],
						'matched_text' => $threat['matched_text'],
						'detected_at'  => current_time( 'mysql' ),
						'status'       => 'pending',
					),
					array( '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s' )
				);
				
				// Log errors
				if ( $insert_result === false ) {
					error_log( 'Bearmor insert error: ' . $wpdb->last_error );
				}
			}
		}

		// Update scan metadata
		update_option( 'bearmor_last_malware_scan', current_time( 'mysql' ) );
		update_option( 'bearmor_malware_scan_results', array(
			'scanned' => $results['scanned'],
			'threats' => $results['threats'],
			'time'    => $results['time'],
		) );
	}

	/**
	 * Check if file is whitelisted
	 *
	 * @param string $file_path File path.
	 * @return bool
	 */
	private static function is_whitelisted( $file_path ) {
		$relative_path = str_replace( ABSPATH, '', $file_path );
		$whitelist = get_option( 'bearmor_malware_whitelist', array() );

		return in_array( $relative_path, $whitelist, true );
	}

	/**
	 * Add file to whitelist
	 *
	 * @param string $file_path File path.
	 */
	public static function whitelist_file( $file_path ) {
		$whitelist = get_option( 'bearmor_malware_whitelist', array() );
		if ( ! in_array( $file_path, $whitelist, true ) ) {
			$whitelist[] = $file_path;
			update_option( 'bearmor_malware_whitelist', $whitelist );
		}
	}

	/**
	 * Get all detected threats
	 *
	 * @param string $status Filter by status (pending, whitelisted, quarantined).
	 * @return array Array of threats
	 */
	public static function get_threats( $status = 'pending' ) {
		global $wpdb;
		$table_name = $wpdb->prefix . 'bearmor_malware_detections';

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe, query uses prepare()
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM $table_name WHERE status = %s ORDER BY detected_at DESC",
				$status
			)
		);
		
		// Debug: log if no results
		if ( empty( $results ) && $wpdb->last_error ) {
			error_log( 'Bearmor get_threats error: ' . $wpdb->last_error );
		}

		return $results;
	}

	/**
	 * Update threat status
	 *
	 * @param int    $threat_id Threat ID.
	 * @param string $status New status.
	 */
	public static function update_threat_status( $threat_id, $status ) {
		global $wpdb;
		$table_name = $wpdb->prefix . 'bearmor_malware_detections';

		$wpdb->update(
			$table_name,
			array( 'status' => $status ),
			array( 'id' => $threat_id ),
			array( '%s' ),
			array( '%d' )
		);
	}

	/**
	 * Strip PHP comments from content to avoid false positives
	 *
	 * @param string $content PHP file content.
	 * @return string Content without comments
	 */
	private static function strip_php_comments( $content ) {
		// Use PHP's built-in tokenizer to properly strip comments
		$tokens = token_get_all( $content );
		$output = '';
		
		foreach ( $tokens as $token ) {
			if ( is_array( $token ) ) {
				// Skip comments and docblocks
				if ( $token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT ) {
					// Replace with empty lines to preserve line numbers
					$output .= str_repeat( "\n", substr_count( $token[1], "\n" ) );
				} else {
					$output .= $token[1];
				}
			} else {
				$output .= $token;
			}
		}
		
		return $output;
	}
}
