<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class ASA_Static_Scan {

	const MAX_FILES        = 5000;
	const MAX_FINDINGS     = 4000;
	const MAX_FILE_BYTES   = 524288;
	const MAX_ROOT_DEPTH   = 6;
	const ROOT_DEPTH_SHALLOW = 2;

	private $phpcs_execution = array();
	private $phpcs_issue_count = 0;
	private $plugin_scope = array();

	public function __construct( array $plugin_scope = array() ) {
		$this->plugin_scope = self::sanitize_plugin_scope( $plugin_scope );
	}

	public function build_payload( $scan_profile = '' ) {
		$scan_profile = sanitize_key( (string) $scan_profile );
		$run_data     = $this->run_scan_bundle( $scan_profile );
		$findings = ! empty( $run_data['findings'] ) ? (array) $run_data['findings'] : array();
		$summary  = array(
			'total_findings'             => count( $findings ),
			'phpcs_available'            => self::phpcs_available(),
			'wpcs_available'             => self::wpcs_available(),
			'phpcs_issue_count'          => $this->phpcs_issue_count,
			'by_engine'                  => array(),
			'by_severity'                => array(),
			'engine_status'              => ! empty( $run_data['engine_status'] ) ? $run_data['engine_status'] : array(),
			'tooling_capabilities'       => class_exists( 'ASA_Local_Inventory' ) ? ASA_Local_Inventory::get_tooling_capabilities() : array(),
			'source_file_count'          => ! empty( $run_data['source_file_count'] ) ? absint( $run_data['source_file_count'] ) : 0,
			'rule_definition_count'      => ! empty( $run_data['rule_definition_count'] ) ? absint( $run_data['rule_definition_count'] ) : 0,
			'evaluated_touchpoints'      => ! empty( $run_data['evaluated_touchpoints'] ) ? absint( $run_data['evaluated_touchpoints'] ) : 0,
			'execution_model'            => array(
				'phpcs_wpcs'            => 'bundled_plugin_check_1_9_0_extended_ruleset',
				'aegisify_php_rules'    => 'phpcs_custom_sniffs_and_agent_rules',
				'aegisify_js_rules'     => 'phpcs_custom_sniffs_and_agent_rules',
				'aegisify_python_rules' => 'agent_native_ruleset',
				'composer_audit'        => 'manifest_driven',
				'pip_audit'             => 'optional_manifest_driven',
				'npm_audit'             => 'lockfile_driven',
				'yarn_audit'            => 'lockfile_driven',
				'pnpm_audit'            => 'lockfile_driven',
			),
		);

		foreach ( $findings as $finding ) {
			$engine   = ! empty( $finding['engine'] ) ? (string) $finding['engine'] : 'custom';
			$severity = ! empty( $finding['severity'] ) ? (string) $finding['severity'] : 'medium';
			$summary['by_engine'][ $engine ]   = isset( $summary['by_engine'][ $engine ] ) ? ( (int) $summary['by_engine'][ $engine ] + 1 ) : 1;
			$summary['by_severity'][ $severity ] = isset( $summary['by_severity'][ $severity ] ) ? ( (int) $summary['by_severity'][ $severity ] + 1 ) : 1;
		}

		return array(
			'static_version' => '1.1.0',
			'generated_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
			'results' => $this->build_profile_results( $summary, $scan_profile ),
			'findings' => $findings,
			'summary'  => $summary,
			'privacy'  => array(
				'mode'        => 'metadata_only',
				'description' => 'Static scan results exclude source code bodies, customer content, usernames, emails, comments, secrets, tokens, API keys, and database row content. Only file metadata, rule identifiers, line ranges, and normalized evidence are returned.',
			),
		);
	}

	public function run() {
		$bundle = $this->run_scan_bundle();
		return ! empty( $bundle['findings'] ) ? (array) $bundle['findings'] : array();
	}

	private function run_scan_bundle( $scan_profile = '' ) {
		$scan_profile   = sanitize_key( (string) $scan_profile );
		$files           = self::discover_source_files( $this->plugin_scope );
		$findings        = array();
		$tooling         = class_exists( 'ASA_Local_Inventory' ) ? ASA_Local_Inventory::get_tooling_capabilities() : array();
		$php_count       = count( ! empty( $files['php'] ) ? (array) $files['php'] : array() );
		$js_count        = count( ! empty( $files['js'] ) ? (array) $files['js'] : array() );
		$python_count    = count( ! empty( $files['python'] ) ? (array) $files['python'] : array() );
		$total_sources   = $php_count + $js_count + $python_count;
		$is_deep_profile = self::is_deep_scan_profile( $scan_profile );
		$engine_status = array(
			'aegisify_php_rules' => array(
				'status'     => $is_deep_profile ? ( $php_count > 0 ? 'planned' : 'skipped' ) : 'skipped',
				'reason'     => $is_deep_profile ? 'Aegisify PHP heuristic rules are enabled for the Static Code Analysis profile.' : 'Disabled for exact Plugin Check 1.9.0 parity to reduce false positives from Aegisify-only heuristic PHP rules.',
				'file_count' => $php_count,
			),
			'aegisify_js_rules' => array(
				'status'     => $is_deep_profile ? ( $js_count > 0 ? 'planned' : 'skipped' ) : 'skipped',
				'reason'     => $is_deep_profile ? 'Aegisify JavaScript heuristic rules are enabled for the Static Code Analysis profile.' : 'Disabled for exact Plugin Check 1.9.0 parity to reduce false positives from Aegisify-only heuristic JavaScript rules.',
				'file_count' => $js_count,
			),
			'aegisify_python_rules' => array(
				'status'     => $is_deep_profile ? ( $python_count > 0 ? 'planned' : 'skipped' ) : 'skipped',
				'reason'     => $is_deep_profile ? 'Aegisify native Python rules are enabled for the Static Code Analysis profile.' : 'Disabled for exact Plugin Check 1.9.0 parity because Plugin Check does not execute the agent Python heuristic rules.',
				'file_count' => $python_count,
			),
			'wordpress_custom_rules' => array(
				'status'     => $is_deep_profile ? ( $php_count > 0 ? 'planned' : 'skipped' ) : 'skipped',
				'reason'     => $is_deep_profile ? 'Aegisify WordPress custom security rules are enabled for the Static Code Analysis profile.' : 'Disabled for exact Plugin Check 1.9.0 parity and to suppress noisy custom REST and upload heuristic findings.',
				'file_count' => $php_count,
			),
		);

		$findings = array_merge( $findings, $this->scan_plugin_repo_checks( $files ) );

		if ( $is_deep_profile ) {
			$native_php_findings = $this->scan_php_files( ! empty( $files['php'] ) ? (array) $files['php'] : array() );
			if ( ! empty( $native_php_findings ) ) {
				$findings = array_merge( $findings, $native_php_findings );
			}

			$native_js_findings = $this->scan_js_files( ! empty( $files['js'] ) ? (array) $files['js'] : array() );
			if ( ! empty( $native_js_findings ) ) {
				$findings = array_merge( $findings, $native_js_findings );
			}

			$native_python_findings = $this->scan_python_files( ! empty( $files['python'] ) ? (array) $files['python'] : array() );
			if ( ! empty( $native_python_findings ) ) {
				$findings = array_merge( $findings, $native_python_findings );
			}

			$native_wordpress_findings = $this->scan_wordpress_custom_rules( ! empty( $files['php'] ) ? (array) $files['php'] : array() );
			if ( ! empty( $native_wordpress_findings ) ) {
				$findings = array_merge( $findings, $native_wordpress_findings );
			}

		}

		$phpcs_run = $this->run_phpcs_wpcs_scans( $files );
		if ( ! empty( $phpcs_run['findings'] ) ) {
			$findings = array_merge( $findings, (array) $phpcs_run['findings'] );
		}
		$engine_status['phpcs_wpcs'] = ! empty( $phpcs_run['status'] ) && is_array( $phpcs_run['status'] )
			? $phpcs_run['status']
			: array(
				'status'     => $total_sources > 0 ? 'unavailable' : 'skipped',
				'reason'     => $total_sources > 0 ? 'Bundled PHPCS/WPCS tooling was not available for this agent scan.' : 'No PHP or JavaScript source files were discovered for PHPCS/WPCS.',
				'file_count' => $php_count + $js_count,
			);

		$manifest_status = $this->build_manifest_engine_status( $tooling );
		if ( ! empty( $manifest_status ) ) {
			$engine_status = array_merge( $engine_status, $manifest_status );
		}

		$indexed = array();
		foreach ( $findings as $finding ) {
			if ( self::is_ignored_finding( $finding ) ) {
				continue;
			}
			if ( empty( $finding['dedupe_key'] ) ) {
				continue;
			}
			$indexed[ $finding['dedupe_key'] ] = $finding;
			if ( count( $indexed ) >= self::MAX_FINDINGS ) {
				break;
			}
		}

		$source_file_count = $total_sources;
		$rule_definition_count = self::get_primary_rule_definition_count() + ( $is_deep_profile ? 17 : 0 );
		$evaluated_touchpoints = ( $php_count + $js_count ) * 5;
		if ( $is_deep_profile ) {
			$evaluated_touchpoints += $php_count * 17;
		}

		return array(
			'findings'                  => array_values( $indexed ),
			'engine_status'             => $engine_status,
			'tooling_capabilities'      => $tooling,
			'source_file_count'         => $source_file_count,
			'rule_definition_count'     => $rule_definition_count,
			'evaluated_touchpoints'     => $evaluated_touchpoints,
		);
	}


	private function build_manifest_engine_status( array $tooling ) {
		$composer_manifests = self::discover_manifest_files( array( 'composer.json', 'composer.lock' ) );
		$python_manifests   = self::discover_manifest_files( array( 'requirements.txt', 'pyproject.toml', 'Pipfile', 'poetry.lock' ) );
		$npm_manifests      = self::discover_manifest_files( array( 'package-lock.json' ) );
		$yarn_manifests     = self::discover_manifest_files( array( 'yarn.lock' ) );
		$pnpm_manifests     = self::discover_manifest_files( array( 'pnpm-lock.yaml' ) );
		$statuses = array();

		$statuses['composer_audit'] = array(
			'status'    => ! empty( $composer_manifests ) ? ( ! empty( $tooling['composer']['present'] ) ? 'executed' : 'unavailable' ) : 'skipped',
			'reason'    => ! empty( $composer_manifests )
				? ( ! empty( $tooling['composer']['present'] ) ? 'Composer manifests were discovered and composer audit is available on the agent host.' : 'Composer manifests were discovered but the composer binary is unavailable on the agent host.' )
				: 'Composer audit skipped because no composer manifests were discovered on the agent host.',
			'file_count' => count( $composer_manifests ),
		);

		$statuses['pip_audit'] = array(
			'status'    => ! empty( $python_manifests ) ? ( ! empty( $tooling['pip_audit']['present'] ) && ! empty( $tooling['python']['present'] ) ? 'executed' : 'unavailable' ) : 'skipped',
			'reason'    => ! empty( $python_manifests )
				? ( ! empty( $tooling['pip_audit']['present'] ) && ! empty( $tooling['python']['present'] ) ? 'Python manifests were discovered and pip-audit is available on the agent host.' : 'Python manifests were discovered but Python or pip-audit is unavailable on the agent host.' )
				: 'pip-audit skipped because no Python manifests were discovered on the agent host.',
			'file_count' => count( $python_manifests ),
		);

		$statuses['npm_audit'] = array(
			'status'    => ! empty( $npm_manifests ) ? ( ! empty( $tooling['npm']['present'] ) ? 'executed' : 'unavailable' ) : 'skipped',
			'reason'    => ! empty( $npm_manifests )
				? ( ! empty( $tooling['npm']['present'] ) ? 'package-lock.json files were discovered and npm audit is available on the agent host.' : 'package-lock.json files were discovered but the npm binary is unavailable on the agent host.' )
				: 'npm audit skipped because no package-lock.json files were discovered on the agent host.',
			'file_count' => count( $npm_manifests ),
		);

		$statuses['yarn_audit'] = array(
			'status'    => ! empty( $yarn_manifests ) ? ( ! empty( $tooling['yarn']['present'] ) ? 'executed' : 'unavailable' ) : 'skipped',
			'reason'    => ! empty( $yarn_manifests )
				? ( ! empty( $tooling['yarn']['present'] ) ? 'yarn.lock files were discovered and yarn audit is available on the agent host.' : 'yarn.lock files were discovered but the yarn binary is unavailable on the agent host.' )
				: 'yarn audit skipped because no yarn.lock files were discovered on the agent host.',
			'file_count' => count( $yarn_manifests ),
		);

		$statuses['pnpm_audit'] = array(
			'status'    => ! empty( $pnpm_manifests ) ? ( ! empty( $tooling['pnpm']['present'] ) ? 'executed' : 'unavailable' ) : 'skipped',
			'reason'    => ! empty( $pnpm_manifests )
				? ( ! empty( $tooling['pnpm']['present'] ) ? 'pnpm-lock.yaml files were discovered and pnpm audit is available on the agent host.' : 'pnpm-lock.yaml files were discovered but the pnpm binary is unavailable on the agent host.' )
				: 'pnpm audit skipped because no pnpm-lock.yaml files were discovered on the agent host.',
			'file_count' => count( $pnpm_manifests ),
		);

		return $statuses;
	}

	private static function discover_manifest_files( array $filenames ) {
		$found = array();
		$roots = array( WP_CONTENT_DIR, ABSPATH );
		foreach ( $roots as $root ) {
			if ( empty( $root ) || ! is_dir( $root ) ) {
				continue;
			}
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ),
				RecursiveIteratorIterator::SELF_FIRST
			);
			foreach ( $iterator as $file_info ) {
				if ( $iterator->getDepth() > self::ROOT_DEPTH_SHALLOW ) {
					continue;
				}
				if ( ! $file_info->isFile() ) {
					continue;
				}
				if ( in_array( $file_info->getFilename(), $filenames, true ) ) {
					$found[] = self::relative_path( $file_info->getPathname() );
					if ( count( $found ) >= 20 ) {
						return array_values( array_unique( $found ) );
					}
				}
			}
		}
		return array_values( array_unique( $found ) );
	}

	private function scan_php_files( array $files ) {
		$findings = array();
		$scanner  = 'aegisify_ruleset';

		foreach ( $files as $file_path ) {
			$body = self::read_file( $file_path );
			if ( '' === $body ) {
				continue;
			}
			$relative = self::relative_path( $file_path );

			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_wpdb_string_concat', '/\$wpdb->(?:query|get_var|get_row|get_results|get_col|get_blog_prefix)\s*\([^\n\r;]*(?:\.|\$\{|sprintf\s*\()/i', 'Potential unsafe wpdb SQL construction', 'high', 88 ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_wpdb_prepare_unsafe', '/\$wpdb->prepare\s*\(\s*[^,\)\n\r]+\s*\)/i', 'Potential unsafe $wpdb->prepare() usage', 'high', 84 ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_file_unsafe_upload', '/(?:move_uploaded_file\s*\(|wp_handle_upload\s*\(|wp_handle_sideload\s*\(|\$_FILES\s*\[)/i', 'Potential unsafe file upload handling', 'critical', 84 ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_code_eval_include_user_input', '/(?:\beval\s*\(|\binclude(?:_once)?\s*\(|\brequire(?:_once)?\s*\()([^\n\r;]{0,200})(?:\$_GET|\$_POST|\$_REQUEST|\$_COOKIE|\$_SERVER|\$[A-Za-z_][A-Za-z0-9_]*)/i', 'Dynamic code execution or include sink', 'critical', 82 ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_serialization_unserialize_untrusted', '/\bunserialize\s*\(/i', 'Untrusted deserialization pattern', 'high', 80 ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_network_ssrf_sink', '/\bwp_remote_(?:get|post|request|head)\s*\([^\n\r;]{0,220}(?:\$_GET|\$_POST|\$_REQUEST|\$_COOKIE|\$_SERVER|\$url|\$uri|\$endpoint)/i', 'Potential SSRF sink in wp_remote_* call', 'high', 78 ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'aegisify_php_rules', 'php_output_reflected_unescaped', '/\b(?:echo|print|printf)\b[^\n\r;]{0,220}(?:\$_GET|\$_POST|\$_REQUEST|\$_COOKIE|\$_SERVER)/i', 'Potential unescaped output', 'high', 76 ) );

			foreach ( $findings as &$finding ) {
				if ( empty( $finding['scanner'] ) ) {
					$finding['scanner'] = $scanner;
				}
			}
			unset( $finding );
		}

		return $findings;
	}

	private function scan_js_files( array $files ) {
		$findings = array();
		$scanner  = 'aegisify_ruleset';

		foreach ( $files as $file_path ) {
			$body = self::read_file( $file_path );
			if ( '' === $body ) {
				continue;
			}
			$relative = self::relative_path( $file_path );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'javascript', 'aegisify_js_rules', 'js_dom_innerhtml', '/\.innerHTML\s*=\s*/i', 'Potential DOM XSS sink via innerHTML', 'high', 88, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'javascript', 'aegisify_js_rules', 'js_dom_unsafe_template', '/(?:document\.write\s*\(|insertAdjacentHTML\s*\(|\.outerHTML\s*=|\$\([^\)]*\)\.html\s*\()/i', 'Potential unsafe HTML templating sink', 'high', 86, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'javascript', 'aegisify_js_rules', 'js_storage_token_leak', '/(?:localStorage|sessionStorage)\.setItem\s*\([^\n\r;]{0,180}(?:token|bearer|auth|password|secret|cookie)/i', 'Token persisted to browser storage', 'high', 84, $scanner ) );
			$findings = array_merge( $findings, $this->scan_js_fetch_patterns( $body, $relative, $scanner ) );
		}

		return $findings;
	}

	private function scan_python_files( array $files ) {
		$findings = array();
		$scanner  = 'aegisify_ruleset';

		foreach ( $files as $file_path ) {
			$body = self::read_file( $file_path );
			if ( '' === $body ) {
				continue;
			}
			$relative = self::relative_path( $file_path );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'python', 'aegisify_python_rules', 'py_subprocess_shell_true', '/subprocess\.(?:run|Popen|call|check_call|check_output)\s*\([^\n\r]{0,220}shell\s*=\s*True/i', 'Unsafe subprocess execution pattern', 'high', 90, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'python', 'aegisify_python_rules', 'py_deserialization_pickle', '/\bpickle\.(?:load|loads)\s*\(/i', 'Unsafe pickle deserialization pattern', 'high', 88, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'python', 'aegisify_python_rules', 'py_sql_dynamic', '/(?:execute\s*\(\s*f["\"]|execute\s*\([^\n\r]{0,180}\+[^\n\r]{0,180}\)|cursor\.execute\s*\([^\n\r]{0,180}%\s*[a-zA-Z_])/i', 'Dynamic SQL construction in Python', 'high', 82, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'python', 'aegisify_python_rules', 'py_network_ssrf', '/requests\.(?:get|post|request|put|delete)\s*\([^\n\r]{0,220}(?:request\.args|request\.form|input\(|os\.getenv|sys\.argv|url)/i', 'Potential SSRF sink in Python request', 'high', 80, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'python', 'aegisify_python_rules', 'py_framework_debug', '/(?:app\.run\s*\([^\n\r]{0,120}debug\s*=\s*True|FastAPI\s*\([^\n\r]{0,120}debug\s*=\s*True)/i', 'Debug mode exposed in Python web framework', 'medium', 78, $scanner ) );
		}

		return $findings;
	}

	private function scan_wordpress_custom_rules( array $files ) {
		$findings = array();
		foreach ( $files as $file_path ) {
			$body = self::read_file( $file_path );
			if ( '' === $body ) {
				continue;
			}
			$relative = self::relative_path( $file_path );
			$scanner  = 'agent_regex';

			$has_privileged_handler = (bool) preg_match( '/add_action\s*\(\s*["\'](?:admin_post_|wp_ajax_)(?!nopriv_)/i', $body );
			$has_nonce_handler      = (bool) preg_match( '/add_action\s*\(\s*["\'](?:admin_post_|wp_ajax_)/i', $body );
			$has_capability_check   = (bool) preg_match( '/\bcurrent_user_can\s*\(/i', $body );
			$has_nonce_check        = (bool) preg_match( '/\b(?:check_admin_referer|check_ajax_referer|wp_verify_nonce)\s*\(/i', $body );

			if ( $has_privileged_handler && ! $has_capability_check ) {
				$line = self::first_line_for_pattern( $body, '/add_action\s*\(\s*["\'](?:admin_post_|wp_ajax_)(?!nopriv_)/i' );
				$findings[] = $this->build_finding( 'php', 'wordpress_custom_rules', 'wp_auth_current_user_can_missing', 'Potential missing current_user_can() authorization check', 'high', 78, $relative, $line, $line, $scanner, 'Administrative handlers appear to execute without a nearby capability check.' );
			}
			if ( $has_nonce_handler && ! $has_nonce_check ) {
				$line = self::first_line_for_pattern( $body, '/add_action\s*\(\s*["\'](?:admin_post_|wp_ajax_)/i' );
				$findings[] = $this->build_finding( 'php', 'wordpress_custom_rules', 'wp_csrf_nonce_missing', 'Potential missing nonce verification', 'high', 80, $relative, $line, $line, $scanner, 'State-changing form, ajax, or admin-post logic appears to execute without nonce verification.' );
			}

			$findings = array_merge( $findings, $this->scan_rest_route_permission_callbacks( $body, $relative, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'wordpress_custom_rules', 'wp_options_sensitive_update', '/update_(?:site_)?option\s*\(\s*["\'](?:active_plugins|active_sitewide_plugins|siteurl|home|users_can_register|default_role|admin_email|blog_public|template|stylesheet|permalink_structure)["\']/i', 'Potential sensitive option update', 'critical', 86, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'wordpress_custom_rules', 'wp_shortcode_unsafe_render', '/(?:add_shortcode\s*\(|shortcode_atts\s*\()[\s\S]{0,400}?(?:\b(?:echo|return)\b[^\n\r;]{0,180}(?:\$_GET|\$_POST|\$_REQUEST|\$atts)(?![^\n\r;]{0,120}(?:esc_|wp_kses)))/i', 'Potential unsafe shortcode rendering', 'high', 74, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'wordpress_custom_rules', 'wp_logging_token_leak', '/(?:error_log|add_log|logger|->log)\s*\([^\n\r]{0,220}(?:authorization|bearer|token|password|passwd|cookie|secret)/i', 'Potential auth or token leakage in logs', 'medium', 72, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'wordpress_custom_rules', 'wp_db_admin_tool_exposed', '/(?:phpmyadmin|adminer\.php|adminer\(|myadmin|dbadmin)/i', 'Potential exposed database administration tool reference', 'medium', 68, $scanner ) );
			$findings = array_merge( $findings, $this->find_regex_matches( $body, $relative, 'php', 'wordpress_custom_rules', 'wp_sql_dangerous_grants', '/(?:GRANT\s+ALL|CREATE\s+USER|SHOW\s+GRANTS|SET\s+sql_mode|information_schema|\$wpdb->(?:query|get_var|get_results|get_row)\s*\([^\n\r;]{0,180}(?:GRANT|CREATE\s+USER|sql_mode))/i', 'Potential dangerous database grant or SQL mode assumption', 'high', 70, $scanner ) );
		}

		return $findings;
	}

	private function scan_js_fetch_patterns( $body, $relative, $scanner ) {
		$findings = array();
		if ( preg_match_all( '/fetch\s*\(([^;]{0,500})\)/i', $body, $matches, PREG_OFFSET_CAPTURE ) ) {
			foreach ( $matches[0] as $index => $match ) {
				$snippet = isset( $match[0] ) ? (string) $match[0] : '';
				$offset  = isset( $match[1] ) ? (int) $match[1] : 0;
				if ( '' === $snippet ) {
					continue;
				}
				$line = self::line_from_offset( $body, $offset );
				$targets_wp = (bool) preg_match( '/(?:wp-json|admin-ajax\.php|\/wp-admin\/admin-ajax\.php)/i', $snippet );
				$has_nonce  = (bool) preg_match( '/X-WP-Nonce|_wpnonce|X-CSRF/i', $snippet );
				$has_creds  = (bool) preg_match( '/credentials\s*:\s*["\'](?:include|same-origin)["\']/i', $snippet );
				if ( $targets_wp && ( ! $has_nonce || ! $has_creds ) ) {
					$findings[] = $this->build_finding( 'javascript', 'aegisify_js_rules', 'js_fetch_insecure_rest', 'Potential insecure REST request pattern', 'medium', 74, $relative, $line, $line, $scanner, 'Client code appears to call REST or AJAX endpoints without nonce, credential, or same-origin protections.' );
				}
				if ( $targets_wp && ! $has_nonce ) {
					$findings[] = $this->build_finding( 'javascript', 'aegisify_js_rules', 'js_nonce_header_weak', 'Weak nonce or header handling', 'medium', 72, $relative, $line, $line, $scanner, 'JavaScript appears to omit an expected nonce or security header on a WordPress-sensitive request.' );
				}
			}
		}
		return $findings;
	}

	private function scan_rest_route_permission_callbacks( $body, $relative, $scanner ) {
		$findings = array();
		if ( preg_match_all( '/register_rest_route\s*\((?:.|\n|\r){0,900}?\)\s*;/i', $body, $matches, PREG_OFFSET_CAPTURE ) ) {
			foreach ( $matches[0] as $match ) {
				$block  = isset( $match[0] ) ? (string) $match[0] : '';
				$offset = isset( $match[1] ) ? (int) $match[1] : 0;
				if ( '' === $block ) {
					continue;
				}
				$line = self::line_from_offset( $body, $offset );
				$missing = ! preg_match( '/permission_callback\s*[=:]>?\s*/i', $block );
				$always_true = (bool) preg_match( '/permission_callback\s*[=:]>?\s*["\']__return_true["\']/i', $block );
				if ( $missing || $always_true ) {
					$findings[] = $this->build_finding( 'php', 'wordpress_custom_rules', 'wp_rest_permission_callback_missing', 'Potential unsafe register_rest_route() permission callback', 'critical', 90, $relative, $line, $line, $scanner, 'REST routes appear to expose weak, missing, or always-true permission callbacks.' );
				}
			}
		}
		return $findings;
	}

	private function find_regex_matches( $body, $relative, $language, $engine, $rule_key, $pattern, $title, $severity, $confidence, $scanner = 'agent_regex' ) {
		$findings = array();
		if ( ! preg_match_all( $pattern, $body, $matches, PREG_OFFSET_CAPTURE ) ) {
			return $findings;
		}
		foreach ( $matches[0] as $match ) {
			if ( empty( $match[0] ) ) {
				continue;
			}
			$offset = isset( $match[1] ) ? (int) $match[1] : 0;
			$line   = self::line_from_offset( $body, $offset );
			$findings[] = $this->build_finding( $language, $engine, $rule_key, $title, $severity, $confidence, $relative, $line, $line, $scanner );
			if ( count( $findings ) >= 25 ) {
				break;
			}
		}
		return $findings;
	}

	private function build_finding( $language, $engine, $rule_key, $title, $severity, $confidence, $relative, $line, $end_line, $scanner, $description = '', array $extra = array() ) {
		$relative       = sanitize_text_field( (string) $relative );
		$line           = max( 0, absint( $line ) );
		$end_line       = max( $line, absint( $end_line ) );
		$description    = sanitize_text_field( (string) $description );
		$plugin_context = self::resolve_plugin_context_from_relative( $relative );
		$rule_key       = self::normalize_rule_key( $rule_key );
		$finding = array(
			'engine'       => sanitize_key( (string) $engine ),
			'rule_key'     => $rule_key,
			'title'        => sanitize_text_field( (string) $title ),
			'description'  => $description,
			'details'      => $description,
			'severity'     => sanitize_key( (string) $severity ),
			'confidence'   => max( 1, min( 100, absint( $confidence ) ) ),
			'language'     => sanitize_key( (string) $language ),
			'file_name'    => sanitize_text_field( basename( $relative ) ),
			'file_path'    => $relative,
			'line'         => $line,
			'end_line'     => $end_line,
			'type'         => 'warning',
			'code'         => $rule_key,
			'plugin_slug'  => $plugin_context['plugin_slug'],
			'plugin_name'  => $plugin_context['plugin_name'],
			'scanner'      => sanitize_key( (string) $scanner ),
			'category'     => 'code',
		);
		foreach ( $this->sanitize_finding_extra( $extra ) as $key => $value ) {
			$finding[ $key ] = $value;
		}
		if ( empty( $finding['category'] ) ) {
			$finding['category'] = 'code';
		}
		if ( empty( $finding['file_name'] ) ) {
			$finding['file_name'] = sanitize_text_field( basename( $relative ) );
		}
		if ( empty( $finding['code'] ) ) {
			$finding['code'] = $rule_key;
		}
		if ( empty( $finding['type'] ) ) {
			$finding['type'] = 'warning';
		}
		if ( empty( $finding['details'] ) ) {
			$finding['details'] = $description;
		}
		if ( empty( $finding['plugin_name'] ) ) {
			$finding['plugin_name'] = $plugin_context['plugin_name'];
		}
		if ( empty( $finding['plugin_slug'] ) ) {
			$finding['plugin_slug'] = $plugin_context['plugin_slug'];
		}
		$finding['dedupe_key'] = md5( wp_json_encode( array(
			$finding['engine'],
			$finding['rule_key'],
			$finding['file_path'],
			$finding['line'],
			$finding['end_line'],
			$finding['plugin_slug'],
			isset( $finding['code'] ) ? $finding['code'] : '',
			isset( $finding['type'] ) ? $finding['type'] : '',
			isset( $finding['column'] ) ? absint( $finding['column'] ) : 0,
			isset( $finding['title'] ) ? $finding['title'] : '',
			isset( $finding['details'] ) ? $finding['details'] : '',
		) ) );
		return $finding;
	}


	private static function normalize_rule_key( $rule_key ) {
		$rule_key = strtolower( trim( (string) $rule_key ) );
		$rule_key = preg_replace( '/[^a-z0-9._-]+/', '_', $rule_key );
		$rule_key = trim( (string) $rule_key, '._-' );
		return '' !== $rule_key ? $rule_key : 'external_rule';
	}

	private function sanitize_finding_extra( array $extra ) {
		$sanitized = array();
		if ( ! empty( $extra['category'] ) ) {
			$sanitized['category'] = sanitize_key( (string) $extra['category'] );
		}
		if ( ! empty( $extra['subcategory'] ) ) {
			$sanitized['subcategory'] = sanitize_key( (string) $extra['subcategory'] );
		}
		if ( ! empty( $extra['remediation'] ) ) {
			$sanitized['remediation'] = sanitize_text_field( (string) $extra['remediation'] );
		}
		if ( ! empty( $extra['rule_source'] ) ) {
			$sanitized['rule_source'] = sanitize_text_field( (string) $extra['rule_source'] );
		}
		if ( ! empty( $extra['details'] ) ) {
			$sanitized['details'] = sanitize_text_field( (string) $extra['details'] );
		}
		if ( ! empty( $extra['file_name'] ) ) {
			$sanitized['file_name'] = sanitize_text_field( (string) $extra['file_name'] );
		}
		if ( ! empty( $extra['type'] ) ) {
			$sanitized['type'] = sanitize_key( strtolower( (string) $extra['type'] ) );
		}
		if ( ! empty( $extra['code'] ) ) {
			$sanitized['code'] = sanitize_text_field( (string) $extra['code'] );
		}
		if ( ! empty( $extra['plugin_slug'] ) ) {
			$sanitized['plugin_slug'] = sanitize_key( (string) $extra['plugin_slug'] );
		}
		if ( ! empty( $extra['plugin_name'] ) ) {
			$sanitized['plugin_name'] = sanitize_text_field( (string) $extra['plugin_name'] );
		}
		if ( isset( $extra['column'] ) ) {
			$sanitized['column'] = max( 0, absint( $extra['column'] ) );
		}
		foreach ( array( 'cwe', 'owasp', 'wstg' ) as $list_key ) {
			if ( ! empty( $extra[ $list_key ] ) ) {
				$sanitized[ $list_key ] = array_values( array_filter( array_map( 'sanitize_text_field', (array) $extra[ $list_key ] ) ) );
			}
		}
		return $sanitized;
	}

	private static function sanitize_plugin_scope( array $plugin_scope ) {
		$sanitized = array();
		foreach ( $plugin_scope as $plugin_file ) {
			$plugin_file = plugin_basename( sanitize_text_field( (string) $plugin_file ) );
			if ( '' !== $plugin_file ) {
				$sanitized[] = $plugin_file;
			}
		}
		return array_values( array_unique( $sanitized ) );
	}

	private static function resolve_plugin_context_from_relative( $relative ) {
		$relative = ltrim( wp_normalize_path( (string) $relative ), '/' );
		$context  = array(
			'plugin_slug' => '',
			'plugin_name' => __( 'WordPress / Site', 'aegisify-audit-agent' ),
		);

		if ( 0 !== strpos( $relative, 'wp-content/plugins/' ) ) {
			return $context;
		}

		$plugin_relative = substr( $relative, strlen( 'wp-content/plugins/' ) );
		$plugin_relative = ltrim( (string) $plugin_relative, '/' );
		if ( '' === $plugin_relative ) {
			return $context;
		}

		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$plugins   = function_exists( 'get_plugins' ) ? (array) get_plugins() : array();
		$best_file = '';
		$best_name = '';
		$best_len  = 0;

		foreach ( $plugins as $plugin_file => $plugin_data ) {
			$plugin_file = wp_normalize_path( (string) $plugin_file );
			$dir         = dirname( $plugin_file );
			$prefixes    = array();
			if ( '.' !== $dir && '' !== $dir ) {
				$prefixes[] = trailingslashit( $dir );
			}
			$prefixes[] = $plugin_file;
			foreach ( $prefixes as $prefix ) {
				$prefix = ltrim( wp_normalize_path( (string) $prefix ), '/' );
				if ( '' === $prefix ) {
					continue;
				}
				if ( 0 === strpos( $plugin_relative, $prefix ) && strlen( $prefix ) > $best_len ) {
					$best_len  = strlen( $prefix );
					$best_file = $plugin_file;
					$best_name = ! empty( $plugin_data['Name'] ) ? sanitize_text_field( (string) $plugin_data['Name'] ) : sanitize_text_field( ucfirst( basename( dirname( $plugin_file ) ) ) );
				}
			}
		}

		if ( '' === $best_file ) {
			$parts = explode( '/', $plugin_relative );
			$slug  = ! empty( $parts[0] ) ? sanitize_key( (string) $parts[0] ) : '';
			if ( '' !== $slug ) {
				$context['plugin_slug'] = $slug;
				$context['plugin_name'] = sanitize_text_field( ucfirst( str_replace( array( '-', '_' ), ' ', $slug ) ) );
			}
			return $context;
		}

		$context['plugin_slug'] = sanitize_key( ( '.' !== dirname( $best_file ) && '' !== dirname( $best_file ) ) ? dirname( $best_file ) : basename( $best_file, '.php' ) );
		$context['plugin_name'] = '' !== $best_name ? $best_name : __( 'Plugin', 'aegisify-audit-agent' );
		return $context;
	}

	private function scan_plugin_repo_checks( array $files ) {
		$plugin_scope = self::sanitize_plugin_scope( $this->plugin_scope );
		if ( empty( $plugin_scope ) ) {
			return array();
		}

		$php_files = ! empty( $files['php'] ) ? (array) $files['php'] : array();
		$findings  = array();

		foreach ( $plugin_scope as $plugin_file ) {
			$plugin_file = plugin_basename( (string) $plugin_file );
			$main_file   = wp_normalize_path( trailingslashit( WP_PLUGIN_DIR ) . $plugin_file );
			if ( ! is_file( $main_file ) || ! is_readable( $main_file ) ) {
				continue;
			}
			$plugin_dir     = wp_normalize_path( dirname( $main_file ) );
			$plugin_dir_rel = '.' !== dirname( $plugin_file ) && '' !== dirname( $plugin_file ) ? 'wp-content/plugins/' . trim( dirname( $plugin_file ), '/' ) : 'wp-content/plugins';
			$main_rel       = self::relative_path( $main_file );
			$main_body      = self::read_file( $main_file );

			if ( ! self::plugin_header_has_license( $main_body ) ) {
				$header_line = self::detect_plugin_header_line( $main_body );
				$findings[]  = $this->build_finding( 'php', 'phpcs_wpcs', 'plugin_header_no_license', __( 'Missing plugin license header', 'aegisify-audit-agent' ), 'high', 92, $main_rel, $header_line, $header_line, 'plugin_review', __( 'Missing "License" in Plugin Header. Please update your Plugin Header with a valid GPLv2 (or later) compatible license.', 'aegisify-audit-agent' ), array( 'category' => 'plugin_repo', 'type' => 'error', 'code' => 'plugin_header_no_license' ) );
			}

			if ( is_dir( $plugin_dir ) ) {
				$readme_file = wp_normalize_path( $plugin_dir . '/readme.txt' );
				if ( ! file_exists( $readme_file ) ) {
					$findings[] = $this->build_finding( 'php', 'phpcs_wpcs', 'no_plugin_readme', __( 'Missing plugin readme.txt', 'aegisify-audit-agent' ), 'medium', 88, $plugin_dir_rel . '/readme.txt', 0, 0, 'plugin_review', __( 'The plugin readme.txt does not exist.', 'aegisify-audit-agent' ), array( 'category' => 'plugin_repo', 'type' => 'error', 'code' => 'no_plugin_readme' ) );
				}

				$composer_file = wp_normalize_path( $plugin_dir . '/composer.json' );
				$vendor_autoload = wp_normalize_path( $plugin_dir . '/vendor/autoload.php' );
				if ( is_dir( $plugin_dir . '/vendor' ) && file_exists( $vendor_autoload ) && ! file_exists( $composer_file ) ) {
					$findings[] = $this->build_finding( 'php', 'phpcs_wpcs', 'missing_composer_json_file', __( 'Missing composer.json file', 'aegisify-audit-agent' ), 'low', 82, $plugin_dir_rel . '/composer.json', 0, 0, 'plugin_review', __( 'The "/vendor" directory using composer exists, but "composer.json" file is missing.', 'aegisify-audit-agent' ), array( 'category' => 'plugin_repo', 'type' => 'warning', 'code' => 'missing_composer_json_file' ) );
				}
			}

			$plugin_php_files = array_values( array_filter(
				$php_files,
				static function ( $file_path ) use ( $plugin_dir ) {
					$file_path = wp_normalize_path( (string) $file_path );
					return 0 === strpos( $file_path, trailingslashit( $plugin_dir ) ) || $file_path === $plugin_dir;
				}
			) );

			foreach ( self::find_updater_matches( $plugin_php_files ) as $updater_match ) {
				/* translators: %s: Update-related hook or function name detected in plugin code. */
				$updater_message = sprintf( __( 'Plugin Updater detected. Detected code which may be altering WordPress update routines. Detected: %s', 'aegisify-audit-agent' ), $updater_match['match'] );
				$findings[] = $this->build_finding( 'php', 'phpcs_wpcs', 'update_modification_detected', __( 'Plugin updater detected', 'aegisify-audit-agent' ), 'medium', 80, self::relative_path( $updater_match['file'] ), ! empty( $updater_match['line'] ) ? absint( $updater_match['line'] ) : 0, ! empty( $updater_match['line'] ) ? absint( $updater_match['line'] ) : 0, 'plugin_review', $updater_message, array( 'category' => 'plugin_repo', 'type' => 'warning', 'code' => 'update_modification_detected' ) );
			}

			$findings = array_merge( $findings, $this->scan_plugin_uninstall_checks( $plugin_dir, $plugin_dir_rel ) );
			$findings = array_merge( $findings, $this->scan_direct_file_access_checks( $plugin_php_files ) );
			$findings = array_merge( $findings, $this->scan_no_unfiltered_uploads_checks( $plugin_php_files ) );
			$findings = array_merge( $findings, $this->scan_five_star_review_link_checks( $plugin_php_files ) );
			$findings = array_merge( $findings, $this->scan_external_admin_menu_link_checks( $plugin_php_files ) );
			$findings = array_merge( $findings, $this->scan_block_json_api_version_checks( $plugin_dir ) );
		}

		return $findings;
	}

	private static function plugin_header_has_license( $main_body ) {
		$main_body = (string) $main_body;
		if ( '' === $main_body ) {
			return false;
		}
		if ( preg_match( '/^[ 	\/*#@]*License:\s*(.+)$/mi', $main_body, $matches ) ) {
			return '' !== trim( (string) $matches[1] );
		}
		return false;
	}

	private static function detect_plugin_header_line( $main_body ) {
		$main_body = (string) $main_body;
		if ( '' === $main_body ) {
			return 1;
		}
		if ( preg_match( '/^[ 	\/*#@]*(Plugin Name|Version|Author|Description):/mi', $main_body, $matches, PREG_OFFSET_CAPTURE ) ) {
			$offset = isset( $matches[0][1] ) ? absint( $matches[0][1] ) : 0;
			return 1 + substr_count( substr( $main_body, 0, $offset ), "\n" );
		}
		return 1;
	}

	private static function find_updater_matches( array $php_files ) {
		$matches_found = array();
		$patterns = array(
			'/auto_update_' . 'plugin/i',
			'/pre_set_site_transient_update_[A-Za-z0-9_]+/i',
			'/_site_transient_update_[A-Za-z0-9_]+/i',
		);
		foreach ( $php_files as $php_file ) {
			$body = self::read_file( $php_file );
			if ( '' === $body ) {
				continue;
			}
			foreach ( $patterns as $pattern ) {
				if ( preg_match( $pattern, $body, $match, PREG_OFFSET_CAPTURE ) ) {
					$match_text    = isset( $match[0][0] ) ? (string) $match[0][0] : '';
					$match_offset  = isset( $match[0][1] ) ? absint( $match[0][1] ) : 0;
					$matches_found[] = array(
						'file'  => $php_file,
						'match' => sanitize_text_field( $match_text ),
						'line'  => 1 + substr_count( substr( $body, 0, $match_offset ), "\n" ),
					);
				}
			}
		}
		return $matches_found;
	}


	private function scan_plugin_uninstall_checks( $plugin_dir, $plugin_dir_rel ) {
		$findings       = array();
		$uninstall_file = wp_normalize_path( trailingslashit( $plugin_dir ) . 'uninstall.php' );
		if ( ! is_file( $uninstall_file ) || ! is_readable( $uninstall_file ) ) {
			return $findings;
		}

		$body = self::read_file( $uninstall_file );
		if ( '' === $body || preg_match( '/defined\s*\(\s*[\'\"]WP_UNINSTALL_PLUGIN[\'\"]\s*\)/i', $body ) ) {
			return $findings;
		}

		$findings[] = $this->build_finding(
			'php',
			'phpcs_wpcs',
			'uninstall_missing_constant_check',
			__( 'Uninstall guard missing', 'aegisify-audit-agent' ),
			'high',
			88,
			$plugin_dir_rel . '/uninstall.php',
			1,
			1,
			'plugin_review',
			__( 'The uninstall.php file is present but does not verify the WP_UNINSTALL_PLUGIN constant before executing uninstall logic.', 'aegisify-audit-agent' ),
			array(
				'category'    => 'plugin_repo',
				'type'        => 'error',
				'code'        => 'uninstall_missing_constant_check',
				'remediation' => __( 'Add a WP_UNINSTALL_PLUGIN constant check at the top of uninstall.php before any uninstall logic executes.', 'aegisify-audit-agent' ),
			)
		);

		return $findings;
	}

	private function scan_direct_file_access_checks( array $php_files ) {
		$findings = array();

		foreach ( $php_files as $php_file ) {
			$relative = self::relative_path( $php_file );
			if ( 'uninstall.php' === basename( $relative ) ) {
				continue;
			}

			$body = self::read_file( $php_file );
			if ( '' === $body || self::has_direct_access_guard( $body ) || ! self::requires_direct_access_guard( $body ) ) {
				continue;
			}

			$findings[] = $this->build_finding(
				'php',
				'phpcs_wpcs',
				'missing_direct_file_access_protection',
				__( 'Missing direct file access guard', 'aegisify-audit-agent' ),
				'medium',
				74,
				$relative,
				1,
				1,
				'plugin_review',
				__( 'This PHP file appears to contain executable plugin logic but does not include an early ABSPATH or WPINC direct-access guard.', 'aegisify-audit-agent' ),
				array(
					'category'    => 'security',
					'type'        => 'warning',
					'code'        => 'missing_direct_file_access_protection',
					'remediation' => __( 'Add a top-of-file guard such as if ( ! defined( \'ABSPATH\' ) ) { exit; } before executable plugin logic.', 'aegisify-audit-agent' ),
				)
			);
		}

		return $findings;
	}

	private function scan_no_unfiltered_uploads_checks( array $php_files ) {
		$findings = array();
		$unfiltered_uploads_constant = implode( '_', array( 'ALLOW', 'UNFILTERED', 'UPLOADS' ) );

		foreach ( $php_files as $php_file ) {
			$body = self::read_file( $php_file );
			if ( '' === $body || false === strpos( $body, $unfiltered_uploads_constant ) ) {
				continue;
			}

			$line = self::first_line_for_pattern( $body, '/' . preg_quote( $unfiltered_uploads_constant, '/' ) . '/' );
			$findings[] = $this->build_finding(
				'php',
				'phpcs_wpcs',
				'allow_unfiltered_uploads_detected',
				__( 'Unsafe unfiltered uploads constant detected', 'aegisify-audit-agent' ),
				'high',
				90,
				self::relative_path( $php_file ),
				$line,
				$line,
				'plugin_review',
				__( 'The plugin references the unsafe unfiltered uploads constant, which can permit unsafe file uploads when enabled.', 'aegisify-audit-agent' ),
				array(
					'category'    => 'plugin_repo',
					'type'        => 'error',
					'code'        => 'allow_unfiltered_uploads_detected',
					'remediation' => __( 'Remove unsafe unfiltered uploads constant usage and enforce safe file-type validation through the WordPress upload APIs.', 'aegisify-audit-agent' ),
				)
			);
		}

		return $findings;
	}


	private function scan_five_star_review_link_checks( array $php_files ) {
		$findings = array();

		foreach ( $php_files as $php_file ) {
			$body = self::read_file( $php_file );
			if ( '' === $body || ! preg_match( '/(?:https?:\/\/)?(?:wordpress\.org|wp\.org)\/.*reviews\/\?filter=5/i', $body, $match, PREG_OFFSET_CAPTURE ) ) {
				continue;
			}

			$offset = isset( $match[0][1] ) ? absint( $match[0][1] ) : 0;
			$line   = 1 + substr_count( substr( $body, 0, $offset ), "\n" );

			$findings[] = $this->build_finding(
				'php',
				'phpcs_wpcs',
				'five_star_reviews_detected',
				__( 'Direct 5-star review link detected', 'aegisify-audit-agent' ),
				'medium',
				82,
				self::relative_path( $php_file ),
				$line,
				$line,
				'plugin_review',
				__( 'Linking directly to WordPress.org 5-star reviews is not allowed in plugin-distributed code.', 'aegisify-audit-agent' ),
				array(
					'category'    => 'plugin_repo',
					'type'        => 'error',
					'code'        => 'five_star_reviews_detected',
					'remediation' => __( 'Remove direct links that pre-filter WordPress.org reviews to only 5-star ratings.', 'aegisify-audit-agent' ),
				)
			);
		}

		return $findings;
	}

	private function scan_external_admin_menu_link_checks( array $php_files ) {
		$findings = array();
		$pattern  = '/\badd_menu_page\s*\(\s*(?:[^,]+)\s*,\s*(?:[^,]+)\s*,\s*(?:[^,]+)\s*,\s*[\'"](?:https?:)?\/\/[^\'"]+[\'"]/i';

		foreach ( $php_files as $php_file ) {
			$body = self::read_file( $php_file );
			if ( '' === $body || ! preg_match( $pattern, $body, $match, PREG_OFFSET_CAPTURE ) ) {
				continue;
			}

			$offset = isset( $match[0][1] ) ? absint( $match[0][1] ) : 0;
			$line   = 1 + substr_count( substr( $body, 0, $offset ), "\n" );

			$findings[] = $this->build_finding(
				'php',
				'phpcs_wpcs',
				'external_admin_menu_link',
				__( 'External admin menu URL detected', 'aegisify-audit-agent' ),
				'medium',
				84,
				self::relative_path( $php_file ),
				$line,
				$line,
				'plugin_review',
				__( 'The plugin uses an external URL as the top-level add_menu_page() menu slug, which disrupts the expected WordPress admin navigation experience.', 'aegisify-audit-agent' ),
				array(
					'category'    => 'plugin_repo',
					'type'        => 'error',
					'code'        => 'external_admin_menu_link',
					'remediation' => __( 'Use an internal admin page slug for add_menu_page() and place external documentation links inside the page content instead.', 'aegisify-audit-agent' ),
				)
			);
		}

		return $findings;
	}

	private function scan_block_json_api_version_checks( $plugin_dir ) {
		$findings    = array();
		$block_files = self::discover_plugin_files_by_filename( $plugin_dir, array( 'block.json' ) );

		foreach ( $block_files as $block_file ) {
			$relative = self::relative_path( $block_file );
			$body     = self::read_file( $block_file );
			if ( '' === $body ) {
				continue;
			}

			$data = json_decode( $body, true );
			$api_version = is_array( $data ) && isset( $data['apiVersion'] ) ? intval( $data['apiVersion'] ) : 0;
			if ( is_array( $data ) && $api_version >= 3 ) {
				continue;
			}

			$findings[] = $this->build_finding(
				'php',
				'phpcs_wpcs',
				'block_api_version_too_low',
				__( 'block.json apiVersion should be 3 or higher', 'aegisify-audit-agent' ),
				'medium',
				78,
				$relative,
				1,
				1,
				'plugin_review',
				__( 'Editor blocks should declare apiVersion 3 or higher in block.json for current WordPress editor compatibility.', 'aegisify-audit-agent' ),
				array(
					'category'    => 'plugin_repo',
					'type'        => 'warning',
					'code'        => 'block_api_version_too_low',
					'remediation' => __( 'Update block.json to declare apiVersion 3 or higher and validate the block metadata file.', 'aegisify-audit-agent' ),
				)
			);
		}

		return $findings;
	}

	private static function discover_plugin_files_by_filename( $plugin_dir, array $filenames ) {
		$found      = array();
		$plugin_dir = wp_normalize_path( (string) $plugin_dir );
		if ( '' === $plugin_dir || ! is_dir( $plugin_dir ) ) {
			return $found;
		}

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

		foreach ( $iterator as $file_info ) {
			if ( $iterator->getDepth() > self::MAX_ROOT_DEPTH || ! $file_info->isFile() ) {
				continue;
			}
			if ( in_array( $file_info->getFilename(), $filenames, true ) ) {
				$found[] = wp_normalize_path( $file_info->getPathname() );
			}
		}

		return array_values( array_unique( $found ) );
	}

	private static function has_direct_access_guard( $body ) {
		$body = self::strip_php_comments_and_open_tag( $body );
		$lines = explode( "\n", $body );
		$beginning = trim( implode( "\n", array_slice( $lines, 0, 50 ) ) );

		$patterns = array(
			"/defined\s*\(\s*['\"]ABSPATH['\"]\s*\)\s*(?:\|\||or)\s*(?:exit|die)\s*(?:\([^)]*\))?\s*;/i",
			"/defined\s*\(\s*['\"]WPINC['\"]\s*\)\s*(?:\|\||or)\s*(?:exit|die)\s*(?:\([^)]*\))?\s*;/i",
			"/if\s*\(\s*!\s*defined\s*\(\s*['\"]ABSPATH['\"]\s*\)\s*\)\s*(?:\{|exit|die)/i",
			"/if\s*\(\s*!\s*defined\s*\(\s*['\"]WPINC['\"]\s*\)\s*\)\s*(?:\{|exit|die)/i",
			"/if\s*\(\s*!\s*defined\s*\(\s*['\"](?:ABSPATH|WPINC)['\"]\s*\)\s*\)\s*\{[^}]{0,200}(?:exit|die)\s*(?:\([^)]*\))?/is",
		);

		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $beginning ) ) {
				return true;
			}
		}

		return false;
	}

	private static function requires_direct_access_guard( $body ) {
		$body = self::strip_php_comments_and_open_tag( $body );
		if ( '' === trim( $body ) ) {
			return false;
		}

		$has_object_declarations = preg_match( '/\b(?:final\s+|abstract\s+)?(?:class|interface|trait|enum)\b/i', $body );
		$has_procedural_markers  = preg_match(
			'/\b(?:add_action|add_filter|do_action|register_(?:activation_hook|deactivation_hook|uninstall_hook|setting|rest_route|post_type|taxonomy)|wp_enqueue_(?:script|style)|wp_remote_(?:get|post|request|head)|update_option|delete_option|set_transient|header|setcookie|include|require|require_once|include_once|echo|print|printf|exit|die)\b|\$_(?:GET|POST|REQUEST|FILES|SERVER|COOKIE)\b/i',
			$body
		);

		if ( $has_procedural_markers ) {
			return true;
		}

		if ( ! $has_object_declarations ) {
			return true;
		}

		return false;
	}

	private static function strip_php_comments_and_open_tag( $body ) {
		$body = preg_replace( '/^<\?php\s*/i', '', (string) $body );
		$body = preg_replace( '#/\*.*?\*/#s', '', $body );
		$body = preg_replace( '#//.*$#m', '', $body );
		$body = preg_replace( '#^\s*\*\s.*$#m', '', $body );
		$body = preg_replace( '/#.*$/m', '', $body );
		return is_string( $body ) ? $body : '';
	}

	private static function get_primary_rule_definition_count() {
		return count(
			array(
				'WordPress.DB.PreparedSQL',
				'WordPress.DB.PreparedSQLPlaceholders',
				'WordPress.Security.NonceVerification',
				'WordPress.Security.EscapeOutput',
				'WordPress.Security.ValidatedSanitizedInput',
				'PluginCheck.Security.VerifyNonce',
				'PluginCheck.Security.DirectDB',
				'WordPress.DB.DirectDatabaseQuery',
				'WordPress.Security.SafeRedirect',
				'PluginCheck.CodeAnalysis.SettingSanitization',
				'PluginCheck.CodeAnalysis.Localhost',
				'PluginCheck.CodeAnalysis.Offloading',
				'PluginCheck.CodeAnalysis.EnqueuedResourceOffloading',
				'WordPress.WP.I18n',
				'WordPress.WP.EnqueuedResources',
				'WordPress.WP.EnqueuedResourceParameters',
				'WordPress.DB.SlowDBQuery',
				'WordPressVIPMinimum.Performance.WPQueryParams',
				'PluginCheck.CodeAnalysis.RequiredFunctionParameters',
				'PluginCheck.CodeAnalysis.ShortURL',
				'PluginCheck.CodeAnalysis.Heredoc',
				'WordPress.PHP.DevelopmentFunctions',
				'WordPress.WP.AlternativeFunctions',
				'Generic.PHP.ForbiddenFunctions',
			)
		);
	}

	private function run_phpcs_wpcs_scans( array $files ) {
		$source_targets = array_merge(
			! empty( $files['php'] ) ? (array) $files['php'] : array(),
			! empty( $files['js'] ) ? (array) $files['js'] : array()
		);
		$source_targets = array_values( array_unique( array_filter( array_map( 'strval', $source_targets ) ) ) );
		if ( empty( $source_targets ) ) {
			return array(
				'findings' => array(),
				'status'   => array(
					'status'     => 'skipped',
					'reason'     => 'No PHP or JavaScript source files were discovered for PHPCS/WPCS.',
					'file_count' => 0,
				),
			);
		}

		$runner = $this->get_phpcs_runner_config();
		if ( empty( $runner['phpcs_path'] ) || empty( $runner['standard'] ) ) {
			return array(
				'findings' => array(),
				'status'   => array(
					'status'     => 'unavailable',
					'reason'     => ! empty( $runner['reason'] ) ? (string) $runner['reason'] : 'Bundled PHPCS/WPCS is missing or the required PHP tokenizer/xmlwriter/SimpleXML extensions are unavailable on the agent host.',
					'file_count' => count( $source_targets ),
				),
			);
		}

		$targets = ! empty( $runner['prefer_roots'] ) ? self::discover_phpcs_scan_roots( $this->plugin_scope ) : array();
		if ( empty( $targets ) ) {
			$targets = $source_targets;
		}
		$targets = array_values( array_unique( array_filter( array_map( 'strval', $targets ) ) ) );

		$command = array( PHP_BINARY, $runner['phpcs_path'], '--report=json', '--runtime-set', 'ignore_warnings_on_exit', '1', '--runtime-set', 'ignore_errors_on_exit', '1' );
		if ( ! empty( $runner['installed_paths'] ) ) {
			$command[] = '--runtime-set';
			$command[] = 'installed_paths';
			$command[] = implode( ',', array_map( 'strval', (array) $runner['installed_paths'] ) );
		}
		$command[] = '--standard=' . $runner['standard'];
		if ( ! empty( $runner['extensions'] ) ) {
			$command[] = '--extensions=' . $runner['extensions'];
		}
		$command = array_merge( $command, $targets );
		$run = self::run_command( $command, ! empty( $runner['working_dir'] ) ? $runner['working_dir'] : ABSPATH );
		$execution_message = ! empty( $runner['label'] ) ? sprintf( '%s execution', (string) $runner['label'] ) : 'bundled PHPCS/WPCS execution';
		$execution = class_exists( 'ASA_Local_Inventory' ) ? ASA_Local_Inventory::build_execution_summary( 'phpcs_wpcs', 0 === (int) ( $run['exit_code'] ?? 1 ) ? 'executed' : 'completed_with_findings', $execution_message, $run, array( 'file_count' => count( $targets ) ) ) : array();
		$this->phpcs_execution[] = $execution;

		$raw_report = array();
		if ( ! empty( $run['stdout'] ) ) {
			$raw_report = json_decode( (string) $run['stdout'], true );
		}
		if ( empty( $raw_report ) || ! is_array( $raw_report ) ) {
			return array(
				'findings' => array(),
				'status'   => array(
					'status'     => 'error',
					'reason'     => ! empty( $runner['parse_error_reason'] ) ? (string) $runner['parse_error_reason'] : 'Bundled PHPCS/WPCS did not return a parseable JSON report.',
					'file_count' => count( $targets ),
					'execution'  => $execution,
				),
			);
		}

		$findings = $this->parse_phpcs_report( $raw_report );
		$this->phpcs_issue_count = count( $findings );
		return array(
			'findings' => $findings,
			'status'   => array(
				'status'     => 'executed',
				'reason'     => ! empty( $runner['success_reason'] ) ? (string) $runner['success_reason'] : 'Bundled PHPCS/WPCS executed with WPCS rules and Aegisify custom sniffs for the Deep Code Analysis profile.',
				'file_count' => count( $targets ),
				'issues'     => count( $findings ),
				'execution'  => $execution,
			),
		);
	}

	private function parse_phpcs_report( array $report ) {
		$findings = array();
		$files    = ! empty( $report['files'] ) && is_array( $report['files'] ) ? $report['files'] : array();
		foreach ( $files as $file_path => $file_report ) {
			$messages = ! empty( $file_report['messages'] ) && is_array( $file_report['messages'] ) ? $file_report['messages'] : array();
			foreach ( $messages as $message ) {
				$finding = $this->build_finding_from_phpcs_message( $file_path, (array) $message );
				if ( ! empty( $finding ) ) {
					$findings[] = $finding;
				}
			}
		}
		return $findings;
	}

	private function build_finding_from_phpcs_message( $file_path, array $message ) {
		$source   = ! empty( $message['source'] ) ? sanitize_text_field( (string) $message['source'] ) : '';
		$line     = ! empty( $message['line'] ) ? absint( $message['line'] ) : 1;
		$column   = ! empty( $message['column'] ) ? absint( $message['column'] ) : 0;
		$type     = ! empty( $message['type'] ) ? strtoupper( sanitize_text_field( (string) $message['type'] ) ) : 'WARNING';
		$text     = ! empty( $message['message'] ) ? sanitize_text_field( (string) $message['message'] ) : __( 'PHPCS/WPCS finding', 'aegisify-audit-agent' );
		$relative = self::relative_path( (string) $file_path );
		$ext      = strtolower( pathinfo( (string) $relative, PATHINFO_EXTENSION ) );
		$language = 'php';
		if ( in_array( $ext, array( 'js', 'jsx', 'mjs', 'cjs' ), true ) ) {
			$language = 'javascript';
		}

		$engine      = 'phpcs_wpcs';
		$severity    = 'medium';
		$confidence  = 'ERROR' === $type ? 84 : 72;
		$remediation = __( 'Review the reported coding standard violation and remediate it in the affected file.', 'aegisify-audit-agent' );
		$title       = $text;
		$category    = self::map_phpcs_finding_category( $source );

		$normalized_source = strtolower( str_replace( '.', '_', $source ) );
		$rule_key          = self::normalize_rule_key( $normalized_source );

		if ( 0 === strpos( $source, 'Aegisify.' ) ) {
			$engine      = 'javascript' === $language ? 'aegisify_js_rules' : 'aegisify_php_rules';
			$confidence  = 88;
			$severity    = false !== stripos( $source, 'UnsafeEvalInclude' ) ? 'critical' : 'high';
			$remediation = false !== stripos( $source, 'UnsafeEvalInclude' )
				? __( 'Remove eval/include/require execution of untrusted input and replace unserialize with a safer parser or strict trust boundary.', 'aegisify-audit-agent' )
				: __( 'Replace dangerous browser sinks or storage patterns with safer DOM handling and avoid persisting sensitive tokens in browser storage.', 'aegisify-audit-agent' );
			$title = false !== stripos( $source, 'UnsafeEvalInclude' )
				? __( 'Aegisify custom PHP security sniff', 'aegisify-audit-agent' )
				: __( 'Aegisify custom JavaScript security sniff', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'PreparedSQL' ) ) {
			$severity    = 'high';
			$confidence  = 82;
			$title       = __( 'Potential unprepared or unsafe SQL usage', 'aegisify-audit-agent' );
			$remediation = __( 'Use placeholders and immediately prepared database calls instead of interpolated SQL.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'NonceVerification' ) || false !== stripos( $source, 'VerifyNonce' ) ) {
			$severity    = 'high';
			$confidence  = 80;
			$title       = __( 'Potential missing nonce verification', 'aegisify-audit-agent' );
			$remediation = __( 'Verify a nonce before processing any state-changing request.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'EscapeOutput' ) ) {
			$severity    = 'high';
			$confidence  = 78;
			$title       = __( 'Potential unescaped output', 'aegisify-audit-agent' );
			$remediation = __( 'Escape output for the final HTML, attribute, URL, or JavaScript context before rendering.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'ValidatedSanitizedInput' ) || false !== stripos( $source, 'SettingSanitization' ) ) {
			$severity    = 'medium';
			$confidence  = 76;
			$title       = __( 'Potential unsanitized or unvalidated input', 'aegisify-audit-agent' );
			$remediation = __( 'Sanitize and validate incoming data before it reaches privileged logic, database operations, or output contexts.', 'aegisify-audit-agent' );
		}
		elseif ( false !== stripos( $source, 'SafeRedirect' ) ) {
			$severity    = 'medium';
			$confidence  = 80;
			$title       = __( 'Potential unsafe redirect usage', 'aegisify-audit-agent' );
			$remediation = __( 'Use wp_safe_redirect() for redirects that can be influenced by request or plugin data.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'DirectDatabaseQuery' ) || false !== stripos( $source, 'DirectDB' ) ) {
			$severity    = 'high';
			$confidence  = 82;
			$title       = __( 'Potential direct database query risk', 'aegisify-audit-agent' );
			$remediation = __( 'Prefer WordPress database APIs, use prepared statements, and escape identifiers separately when direct database access is unavoidable.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'Tokenizer.Exception' ) ) {
			$severity    = 'medium';
			$confidence  = 86;
			$title       = __( 'Tokenizer or minified PHP issue detected', 'aegisify-audit-agent' );
			$remediation = __( 'Review the flagged PHP file for tokenization problems, unreadable minified PHP, or malformed syntax and replace it with maintainable source code.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'Localhost' ) ) {
			$severity    = 'medium';
			$confidence  = 84;
			$title       = __( 'Localhost reference detected', 'aegisify-audit-agent' );
			$remediation = __( 'Remove localhost or loopback URLs from shipping plugin code and use environment-aware configuration instead.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'Offloading' ) ) {
			$severity    = 'medium';
			$confidence  = 80;
			$title       = __( 'Remote offloading dependency detected', 'aegisify-audit-agent' );
			$remediation = __( 'Avoid unnecessary remote file loading or offloading dependencies in plugin-distributed code.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'I18n' ) ) {
			$severity    = 'low';
			$confidence  = 72;
			$title       = __( 'Internationalization best-practice issue', 'aegisify-audit-agent' );
			$remediation = __( 'Use the plugin text domain consistently and follow WordPress internationalization best practices.', 'aegisify-audit-agent' );
		} elseif ( false !== stripos( $source, 'EnqueuedResource' ) || false !== stripos( $source, 'SlowDBQuery' ) || false !== stripos( $source, 'WPQueryParams' ) ) {
			$severity    = 'medium';
			$confidence  = 74;
			$title       = __( 'Performance-oriented WordPress coding issue', 'aegisify-audit-agent' );
			$remediation = __( 'Review resource enqueue patterns and query parameters to reduce unnecessary frontend weight and slow database queries.', 'aegisify-audit-agent' );
		}

		return $this->build_finding( $language, $engine, $rule_key, $title, $severity, $confidence, $relative, $line, $line, 'phpcs', $text, array( 'remediation' => $remediation, 'category' => $category, 'subcategory' => self::normalize_rule_key( $engine ), 'type' => strtolower( $type ), 'code' => $source, 'column' => $column ) );
	}

	private static function map_phpcs_finding_category( $source ) {
		$source = (string) $source;
		if ( false !== stripos( $source, 'NonceVerification' ) || false !== stripos( $source, 'VerifyNonce' ) || false !== stripos( $source, 'EscapeOutput' ) || false !== stripos( $source, 'ValidatedSanitizedInput' ) || false !== stripos( $source, 'SettingSanitization' ) || false !== stripos( $source, 'PreparedSQL' ) || false !== stripos( $source, 'DirectDB' ) || false !== stripos( $source, 'DirectDatabaseQuery' ) || false !== stripos( $source, 'SafeRedirect' ) || false !== stripos( $source, 'PluginMenuSlug' ) ) {
			return 'security';
		}
		if ( false !== stripos( $source, 'Performance' ) || false !== stripos( $source, 'EnqueuedResource' ) || false !== stripos( $source, 'SlowDBQuery' ) || false !== stripos( $source, 'WPQueryParams' ) ) {
			return 'performance';
		}
		if ( false !== stripos( $source, 'Accessibility' ) ) {
			return 'accessibility';
		}
		if ( false !== stripos( $source, 'Localhost' ) || false !== stripos( $source, 'Offloading' ) || false !== stripos( $source, 'RequiredFunctionParameters' ) || false !== stripos( $source, 'ShortURL' ) || false !== stripos( $source, 'Heredoc' ) || false !== stripos( $source, 'Tokenizer.Exception' ) ) {
			return 'plugin_repo';
		}
		if ( false !== stripos( $source, 'I18n' ) || false !== stripos( $source, 'PluginCheck' ) || false !== stripos( $source, 'WordPress.WP.' ) || false !== stripos( $source, 'WordPress.DateTime' ) || false !== stripos( $source, 'WordPress.PHP.' ) || false !== stripos( $source, 'Generic.PHP.' ) || false !== stripos( $source, 'Generic.Files.' ) || false !== stripos( $source, 'Squiz.PHP.' ) ) {
			return 'general';
		}
		return 'code';
	}

	private function get_phpcs_runner_config() {
		if ( ! self::phpcs_available() || ! self::wpcs_available() ) {
			return array(
				'phpcs_path' => '',
				'standard'   => '',
				'reason'     => 'Bundled Aegisify Checker PHPCS/WPCS tooling is missing or the required PHP tokenizer/xmlwriter/SimpleXML extensions are unavailable on the agent host.',
			);
		}

		$phpcs_path                 = self::get_bundled_phpcs_path();
		$bundled_plugin_ruleset     = self::get_bundled_plugin_check_ruleset_path();
		$bundled_plugin_check_paths = self::get_bundled_plugin_check_installed_paths();
		if ( '' !== $phpcs_path && '' !== $bundled_plugin_ruleset && ! empty( $bundled_plugin_check_paths ) ) {
			return array(
				'phpcs_path'         => $phpcs_path,
				'standard'           => $bundled_plugin_ruleset,
				'installed_paths'    => $bundled_plugin_check_paths,
				'label'              => 'Bundled Plugin Check 1.9.0 extended ruleset',
				'working_dir'        => ABSPATH,
				'prefer_roots'       => true,
				'extensions'         => 'php,js',
				'reason'             => 'Bundled Plugin Check 1.9.0 extended rules with nonce, sanitization, validation, escaping, plugin-repo, and performance coverage could not be initialized on the agent host.',
				'parse_error_reason' => 'Bundled Plugin Check 1.9.0 extended rules did not return a parseable JSON report.',
				'success_reason'     => 'Bundled Plugin Check 1.9.0 rules copied into Aegisify Checker executed locally with nonce, sanitization, validation, escaping, plugin-repo, and performance coverage for the selected plugin scope and cached the detailed findings for this agent run without relying on any separate plugin-check installation.',
			);
		}

		$standard = wp_normalize_path( ASA_DIR . 'vendor/aegisify/phpcs-aegisify/Aegisify/ruleset.xml' );
		if ( '' === $phpcs_path || ! file_exists( $standard ) ) {
			return array(
				'phpcs_path' => '',
				'standard'   => '',
				'reason'     => 'Bundled Aegisify Checker standards could not be located in the agent package.',
			);
		}

		return array(
			'phpcs_path'         => $phpcs_path,
			'standard'           => $standard,
			'label'              => 'Bundled PHPCS/WPCS',
			'working_dir'        => ABSPATH,
			'prefer_roots'       => false,
			'extensions'         => 'php,js',
			'reason'             => 'Bundled PHPCS/WPCS is missing or the required PHP tokenizer/xmlwriter/SimpleXML extensions are unavailable on the agent host.',
			'parse_error_reason' => 'Bundled PHPCS/WPCS did not return a parseable JSON report.',
			'success_reason'     => 'Bundled PHPCS/WPCS executed with WPCS rules and Aegisify custom sniffs for the Deep Code Analysis profile.',
		);
	}

	private static function discover_phpcs_scan_roots( array $plugin_scope = array() ) {
		$plugin_scope = self::sanitize_plugin_scope( $plugin_scope );
		$roots        = array();
		foreach ( $plugin_scope as $plugin_file ) {
			$plugin_file    = plugin_basename( (string) $plugin_file );
			$absolute_path  = wp_normalize_path( trailingslashit( WP_PLUGIN_DIR ) . $plugin_file );
			$plugin_dirname = dirname( $plugin_file );
			if ( '.' !== $plugin_dirname && '' !== $plugin_dirname ) {
				$root = dirname( $absolute_path );
				if ( is_dir( $root ) ) {
					$roots[] = $root;
				}
			} elseif ( is_file( $absolute_path ) ) {
				$roots[] = $absolute_path;
			}
		}
		return array_values( array_unique( array_filter( array_map( 'strval', $roots ) ) ) );
	}

	private static function get_plugin_check_phpcs_path() {
		return self::get_bundled_phpcs_path();
	}

	private static function get_plugin_check_ruleset_path() {
		return self::get_bundled_aegisify_checker_ruleset_path();
	}

	private static function get_bundled_phpcs_path() {
		$candidate = wp_normalize_path( ASA_DIR . 'vendor/bin/phpcs' );
		if ( file_exists( $candidate ) && is_readable( $candidate ) ) {
			return $candidate;
		}
		return '';
	}

	private static function get_bundled_plugin_check_ruleset_path() {
		$candidate = wp_normalize_path( ASA_DIR . 'vendor/aegisify/plugin-check-rulesets/plugin-review.xml' );
		if ( file_exists( $candidate ) && is_readable( $candidate ) ) {
			return $candidate;
		}
		return '';
	}

	private static function get_bundled_aegisify_checker_ruleset_path() {
		$candidate = wp_normalize_path( ASA_DIR . 'vendor/aegisify/aegisify-checker-rules/aegisify-review.ruleset.xml' );
		if ( file_exists( $candidate ) && is_readable( $candidate ) ) {
			return $candidate;
		}
		return '';
	}

	private static function get_bundled_plugin_check_installed_paths() {
		$paths = array(
			wp_normalize_path( ASA_DIR . 'vendor/wp-coding-standards/wpcs' ),
			wp_normalize_path( ASA_DIR . 'vendor/phpcsstandards/phpcsextra' ),
			wp_normalize_path( ASA_DIR . 'vendor/phpcsstandards/phpcsutils' ),
			wp_normalize_path( ASA_DIR . 'vendor/plugin-check/phpcs-sniffs' ),
			wp_normalize_path( ASA_DIR . 'vendor/automattic/vipwpcs' ),
			wp_normalize_path( ASA_DIR . 'vendor/sirbrillig/phpcs-variable-analysis' ),
		);
		$paths = array_values( array_filter( $paths, 'is_dir' ) );
		return array_values( array_unique( array_map( 'strval', $paths ) ) );
	}

	private static function is_ignored_finding( array $finding ) {
		$rule_id = ! empty( $finding['rule_id'] ) ? self::normalize_rule_key( (string) $finding['rule_id'] ) : '';
		$title   = ! empty( $finding['title'] ) ? sanitize_text_field( (string) $finding['title'] ) : '';
		$code    = '';
		if ( ! empty( $finding['evidence']['code'] ) ) {
			$code = self::normalize_rule_key( (string) $finding['evidence']['code'] );
		}

		$ignored_rule_ids = array(
			'php_file_unsafe_upload',
			'wp_rest_permission_callback_missing',
		);
		$ignored_titles = array(
			'Potential unsafe file upload handling',
			'Potential unsafe register_rest_route() permission callback',
		);

		return in_array( $rule_id, $ignored_rule_ids, true )
			|| in_array( $code, $ignored_rule_ids, true )
			|| in_array( $title, $ignored_titles, true );
	}

	private static function has_phpcs_runtime_support() {
		return extension_loaded( 'tokenizer' ) && extension_loaded( 'xmlwriter' ) && extension_loaded( 'SimpleXML' );
	}

	private static function phpcs_available() {
		return '' !== self::get_bundled_phpcs_path() && self::has_phpcs_runtime_support();
	}

	private static function wpcs_available() {
		return is_dir( wp_normalize_path( ASA_DIR . 'vendor/wp-coding-standards/wpcs/WordPress' ) );
	}


	private static function discover_source_files( array $plugin_scope = array() ) {
		$plugin_scope = self::sanitize_plugin_scope( $plugin_scope );
		$roots        = array();

		if ( ! empty( $plugin_scope ) ) {
			foreach ( $plugin_scope as $plugin_file ) {
				$absolute_path = wp_normalize_path( trailingslashit( WP_PLUGIN_DIR ) . $plugin_file );
				$plugin_dir    = dirname( $absolute_path );
				if ( '.' !== dirname( $plugin_file ) && is_dir( $plugin_dir ) ) {
					$roots[] = array( 'path' => $plugin_dir, 'depth' => self::MAX_ROOT_DEPTH );
					continue;
				}
				if ( is_file( $absolute_path ) ) {
					$roots[] = array( 'path' => $absolute_path, 'depth' => 0 );
				}
			}
		}

		if ( empty( $roots ) ) {
			$roots = array(
				array( 'path' => ABSPATH, 'depth' => self::ROOT_DEPTH_SHALLOW ),
				array( 'path' => trailingslashit( ABSPATH ) . 'wp-admin', 'depth' => self::ROOT_DEPTH_SHALLOW ),
				array( 'path' => trailingslashit( ABSPATH ) . 'wp-includes', 'depth' => self::ROOT_DEPTH_SHALLOW ),
				array( 'path' => defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : '', 'depth' => self::MAX_ROOT_DEPTH ),
				array( 'path' => defined( 'WPMU_PLUGIN_DIR' ) ? WPMU_PLUGIN_DIR : '', 'depth' => self::ROOT_DEPTH_SHALLOW ),
				array( 'path' => function_exists( 'get_theme_root' ) ? get_theme_root() : '', 'depth' => self::MAX_ROOT_DEPTH ),
			);
		}

		$results = array(
			'php'    => array(),
			'js'     => array(),
			'python' => array(),
		);
		$seen = array();

		foreach ( $roots as $root ) {
			$base_path = ! empty( $root['path'] ) ? wp_normalize_path( (string) $root['path'] ) : '';
			$max_depth = ! empty( $root['depth'] ) ? absint( $root['depth'] ) : self::MAX_ROOT_DEPTH;
			if ( '' === $base_path ) {
				continue;
			}
			if ( is_file( $base_path ) ) {
				self::maybe_add_source_file( $base_path, $results, $seen );
			} elseif ( is_dir( $base_path ) ) {
				self::walk_source_tree( $base_path, $base_path, $max_depth, $results, $seen );
			}
			if ( count( $seen ) >= self::MAX_FILES ) {
				break;
			}
		}

		return $results;
	}

	private static function maybe_add_source_file( $full_path, array &$results, array &$seen ) {
		$full_path = wp_normalize_path( (string) $full_path );
		if ( '' === $full_path || ! is_file( $full_path ) || ! is_readable( $full_path ) ) {
			return;
		}
		if ( filesize( $full_path ) > self::MAX_FILE_BYTES ) {
			return;
		}
		$ext  = strtolower( pathinfo( $full_path, PATHINFO_EXTENSION ) );
		$type = '';
		if ( 'php' === $ext ) {
			$type = 'php';
		} elseif ( in_array( $ext, array( 'js', 'jsx', 'mjs', 'cjs' ), true ) ) {
			$type = 'js';
		} elseif ( 'py' === $ext ) {
			$type = 'python';
		}
		if ( '' === $type || isset( $seen[ $full_path ] ) ) {
			return;
		}
		$seen[ $full_path ] = true;
		$results[ $type ][] = $full_path;
	}

	private static function walk_source_tree( $root_path, $current_path, $max_depth, array &$results, array &$seen ) {
		if ( count( $seen ) >= self::MAX_FILES ) {
			return;
		}
		$root_path    = wp_normalize_path( (string) $root_path );
		$current_path = wp_normalize_path( (string) $current_path );
		$depth        = 0;
		if ( strlen( $current_path ) > strlen( $root_path ) ) {
			$relative = trim( substr( $current_path, strlen( $root_path ) ), '/' );
			$depth    = '' === $relative ? 0 : substr_count( $relative, '/' ) + 1;
		}
		if ( $depth > $max_depth ) {
			return;
		}
		$entries = @scandir( $current_path );
		if ( ! is_array( $entries ) ) {
			return;
		}
		foreach ( $entries as $entry ) {
			if ( '.' === $entry || '..' === $entry ) {
				continue;
			}
			$full_path = wp_normalize_path( $current_path . '/' . $entry );
			if ( is_dir( $full_path ) ) {
				if ( self::should_skip_dir( $entry ) ) {
					continue;
				}
				self::walk_source_tree( $root_path, $full_path, $max_depth, $results, $seen );
				if ( count( $seen ) >= self::MAX_FILES ) {
					return;
				}
				continue;
			}
			self::maybe_add_source_file( $full_path, $results, $seen );
			if ( count( $seen ) >= self::MAX_FILES ) {
				return;
			}
		}
	}

	private static function should_skip_dir( $dir_name ) {
		$dir_name = strtolower( (string) $dir_name );
		return in_array(
			$dir_name,
			array( 'uploads', 'upgrade', 'cache', 'caches', 'backup', 'backups', 'node_modules', 'vendor', '.git', '.svn', 'dist', 'build', 'coverage', 'tmp', 'temp' ),
			true
		);
	}

	private static function read_file( $file_path ) {
		$file_path = (string) $file_path;
		if ( '' === $file_path || ! is_readable( $file_path ) || ! is_file( $file_path ) ) {
			return '';
		}
		$body = @file_get_contents( $file_path, false, null, 0, self::MAX_FILE_BYTES );
		return is_string( $body ) ? $body : '';
	}

	private static function relative_path( $file_path ) {
		$file_path = wp_normalize_path( (string) $file_path );
		$base      = wp_normalize_path( ABSPATH );
		if ( 0 === strpos( $file_path, $base ) ) {
			return ltrim( substr( $file_path, strlen( $base ) ), '/' );
		}
		return basename( $file_path );
	}

	private static function line_from_offset( $body, $offset ) {
		$offset = max( 0, absint( $offset ) );
		return 1 + substr_count( substr( (string) $body, 0, $offset ), "\n" );
	}

	private static function first_line_for_pattern( $body, $pattern ) {
		if ( preg_match( $pattern, $body, $match, PREG_OFFSET_CAPTURE ) && ! empty( $match[0][1] ) ) {
			return self::line_from_offset( $body, (int) $match[0][1] );
		}
		return 1;
	}



	private static function run_command( array $command, $cwd = '' ) {
		$result = array(
			'exit_code' => 1,
			'stdout'    => '',
			'stderr'    => '',
		);
		if ( empty( $command ) || ! function_exists( 'proc_open' ) ) {
			return $result;
		}
		$descriptor_spec = array(
			0 => array( 'pipe', 'r' ),
			1 => array( 'pipe', 'w' ),
			2 => array( 'pipe', 'w' ),
		);
		// phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Agent-local static analysis requires a subprocess to invoke PHPCS/WPCS binaries.
		$process = @proc_open( $command, $descriptor_spec, $pipes, '' !== $cwd ? $cwd : null );
		if ( ! is_resource( $process ) ) {
			return $result;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing process pipes is required for subprocess cleanup and is not a filesystem write operation.
		fclose( $pipes[0] );
		$result['stdout'] = stream_get_contents( $pipes[1] );
		$result['stderr'] = stream_get_contents( $pipes[2] );
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing process pipes is required for subprocess cleanup and is not a filesystem write operation.
		fclose( $pipes[1] );
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing process pipes is required for subprocess cleanup and is not a filesystem write operation.
		fclose( $pipes[2] );
		$result['exit_code'] = proc_close( $process );
		return $result;
	}

	private static function binary_exists( $binary ) {
		$binary = sanitize_key( (string) $binary );
		if ( '' === $binary || ! function_exists( 'shell_exec' ) ) {
			return false;
		}
		$path = @shell_exec( 'command -v ' . escapeshellarg( $binary ) . ' 2>/dev/null' );
		return is_string( $path ) && '' !== trim( $path );
	}


	private static function is_deep_scan_profile( $scan_profile ) {
		$scan_profile = sanitize_key( (string) $scan_profile );
		return in_array( $scan_profile, array( 'defensive_deep_agent', 'static_code_analysis', 'deep_code_analysis' ), true );
	}

	private function build_profile_results( array $summary, $scan_profile = '' ) {
		$scan_profile = sanitize_key( (string) $scan_profile );
		if ( ! self::is_deep_scan_profile( $scan_profile ) ) {
			return array();
		}
		$total = ! empty( $summary['total_findings'] ) ? (int) $summary['total_findings'] : 0;
		$details = 0 === $total
			? __( 'PHPCS/WPCS and Aegisify custom Deep Code Analysis completed with no code findings returned by the agent for this run.', 'aegisify-audit-agent' )
			: '';
		if ( 0 !== $total ) {
			/* translators: %d: Total number of code findings returned by the agent. */
			$details = sprintf( __( 'PHPCS/WPCS and Aegisify custom Deep Code Analysis completed on the agent and returned %d code findings for this run.', 'aegisify-audit-agent' ), $total );
		}
		return array(
			array(
				'severity'       => 0 === $total ? 'info' : 'medium',
				'category'       => 'static-analysis',
				'title'          => __( 'Deep Code Analysis summary', 'aegisify-audit-agent' ),
				'details'        => $details,
				'recommendation' => 0 === $total ? __( 'Review the detailed static findings table only if future runs return matches.', 'aegisify-audit-agent' ) : __( 'Review the detailed static findings and apply the recommended remediations for the reported code issues.', 'aegisify-audit-agent' ),
				'evidence_json'  => wp_json_encode( array(
					'scan_profile' => $scan_profile,
					'summary'      => $summary,
				) ),
			)
		);
	}
}
