<?php

declare(strict_types=1);

namespace SiteFort\Modules\Scanner\Queue;

use SiteFort\Includes\DataDirectoryProtection;
use SiteFort\Modules\Scanner\FileIntegrity\FSMEngine;
use SiteFort\Modules\Scanner\Repository\IssueRepository;
use SiteFort\Modules\Scanner\Service\IssueType;
use SiteFort\Modules\Scanner\Service\MalwareFindingFormatter;

// If this file is called directly, abort.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Cloud Result Processor.
 *
 * Handles all post-scan result processing: applying Intel Engine verdicts to
 * file states, staging malware issues, merging hash-check payloads, and
 * marking failed job files as errors.
 *
 * Extracted from ScanOrchestrator to give this responsibility a single home.
 *
 */
class CloudResultProcessor {

	/**
	 * Escape a plugin-owned table identifier for direct interpolation.
	 */
	private static function table_identifier( string $table ): string {
		if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
			throw new \InvalidArgumentException( 'Invalid table identifier.' );
		}

		return '`' . str_replace( '`', '``', $table ) . '`';
	}

	/**
	 * Current scan ID.
	 *
	 * @var string
	 */
	private string $scan_id;

	/**
	 * FSM Engine.
	 *
	 * @var FSMEngine
	 */
	private FSMEngine $fsm;

	/**
	 * Constructor.
	 *
	 * @param string    $scan_id Current scan UUID.
	 * @param FSMEngine $fsm     FSM engine instance shared with ScanOrchestrator.
	 */
	public function __construct( string $scan_id, FSMEngine $fsm ) {
		$this->scan_id = $scan_id;
		$this->fsm     = $fsm;
	}

	/**
	 * @param string $file_path
	 * @return bool
	 */
	public static function is_runtime_artifact_path( string $file_path ): bool {
		$normalized = '/' . ltrim( str_replace( '\\', '/', $file_path ), '/' );
		foreach ( self::get_runtime_artifact_prefixes() as $prefix ) {
			if ( $normalized === $prefix || strpos( $normalized, $prefix . '/' ) === 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Resolve runtime-managed file prefixes relative to ABSPATH.
	 *
	 * @return array<int,string>
	 */
	private static function get_runtime_artifact_prefixes(): array {
		static $prefixes = null;
		if ( is_array( $prefixes ) ) {
			return $prefixes;
		}

		$prefixes = [];
		$data_dir = wp_normalize_path( DataDirectoryProtection::get_data_dir_path() );
		$abspath  = trailingslashit( wp_normalize_path( ABSPATH ) );

		if ( strpos( $data_dir, $abspath ) === 0 ) {
			$relative = '/' . ltrim( substr( $data_dir, strlen( $abspath ) ), '/' );
			$prefixes[] = rtrim( $relative, '/' );
		}

		return $prefixes;
	}

	/**
	 * Process scan results from Intel Engine.
	 *
	 * Uses batch fetch with WHERE IN to eliminate N+1 queries.
	 *
	 * @param string $job_id  Job ID.
	 * @param array  $results Scan results.
	 */
	public function process_scan_results( string $job_id, array $results ): void {
		// Normalize IntelEngine result wrapper: {"issues":[...], "retry":[...]}
		$issues      = $results;
		$retry_items = [];
		if ( isset( $results['issues'] ) && is_array( $results['issues'] ) ) {
			$issues = $results['issues'];
			if ( isset( $results['retry'] ) && is_array( $results['retry'] ) ) {
				$retry_items = $results['retry'];
			}
		}
		$safe_items = [];
		if ( isset( $results['safe'] ) && is_array( $results['safe'] ) ) {
			$safe_items = $results['safe'];
		}

		global $wpdb;
		$table_name = self::table_identifier( $wpdb->prefix . 'sitefort_file_states' );

		// Fetch ALL files associated with this job (including safe ones).
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$files = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$table_name} WHERE scan_id = %s AND cloud_job_id = %s",
				$this->scan_id,
				$job_id
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		if ( empty( $files ) ) {
			return;
		}

		// Build results map for O(1) lookups.
		$results_map = [];
		if ( is_array( $issues ) ) {
			foreach ( $issues as $result ) {
				$path = $result['file'] ?? $result['path'] ?? '';
				if ( ! empty( $path ) ) {
					$results_map[ $path ] = $result;
				}
			}
		}

		$retry_map = [];
		foreach ( $retry_items as $item ) {
			$path = $item['path'] ?? $item['file'] ?? '';
			if ( ! empty( $path ) ) {
				$retry_map[ $path ] = $item;
			}
		}

		$safe_map = [];
		foreach ( $safe_items as $item ) {
			$path = $item['file'] ?? $item['path'] ?? '';
			if ( ! empty( $path ) ) {
				$safe_map[ $path ] = $item;
			}
		}

		$pinned_signature = null;
		if ( class_exists( '\SiteFort\\Modules\\Scanner\\Queue\\ScanQueue' ) ) {
			$pinned_signature = \SiteFort\Modules\Scanner\Queue\ScanQueue::get_pinned_signature_version( $this->scan_id );
		}

		$hash_cache_repo = null;
		if ( ! empty( $safe_map ) ) {
			if ( ! class_exists( '\SiteFort\\Modules\\Scanner\\Repository\\HashCacheRepository' ) ) {
				require_once dirname( __DIR__ ) . '/Repository/HashCacheRepository.php';
			}
			if ( class_exists( '\SiteFort\\Modules\\Scanner\\Repository\\HashCacheRepository' ) ) {
				$hash_cache_repo = new \SiteFort\Modules\Scanner\Repository\HashCacheRepository();
			}
		}

		// Process ALL files in the job.
		foreach ( $files as $file ) {
			$file_path     = $file['file_path'];
			$result        = $results_map[ $file_path ] ?? null;
			$retry         = $retry_map[ $file_path ] ?? null;
			$cached_result = null;
			if ( ! empty( $file['scan_result'] ) ) {
				$decoded = json_decode( (string) $file['scan_result'], true );
				if ( is_array( $decoded ) ) {
					$cached_result = $decoded;
				}
			}

			if ( $retry ) {
				$retry_code    = sanitize_key( (string) ( $retry['code'] ?? '' ) );
				$retry_error_code = $retry_code !== '' ? $retry_code : 'upload_retry';
				$is_hash_mismatch = ( $retry_error_code === 'hash_mismatch' );
				$is_missing_hash  = ( $retry_error_code === 'missing_hash' );
				$is_runtime_artifact = self::is_runtime_artifact_path( $file_path );

				$retry_exhausted_message = $is_hash_mismatch
					? 'Upload retry limit exceeded after hash mismatch'
					: ( $is_missing_hash
						? 'Upload retry limit exceeded after missing cloud batch entry'
						: 'Upload retry limit exceeded after cloud retry failures' );
				$retry_issue_message = $is_hash_mismatch
					? 'Upload failed repeatedly due to hash mismatch'
					: ( $is_missing_hash
						? 'Upload failed repeatedly due to missing cloud batch entry'
						: 'Upload failed repeatedly during cloud retry' );
				$rehash_message = $is_hash_mismatch
					? 'File hash changed during upload; re-queued for re-check'
					: ( $is_missing_hash
						? 'File changed before missing batch recovery; re-queued for re-check'
						: 'File changed before cloud retry; re-queued for re-check' );
				$reupload_message = $is_hash_mismatch
					? 'Hash mismatch during cloud scan; re-queued for upload'
					: ( $is_missing_hash
						? 'Cloud batch missed this file; re-queued for upload'
						: 'Cloud retry requested file re-upload' );

				$expected_hash = (string) ( $retry['expected_hash'] ?? $file['file_hash'] ?? '' );
				$abs_path      = ABSPATH . $file_path;
				$max_retries   = 3;
				$attempts      = (int) ( $file['upload_attempt'] ?? 0 );

				if ( $attempts >= $max_retries ) {
					if ( $is_runtime_artifact ) {
						$this->fsm->transition(
							(int) $file['id'],
							(string) $file['state'],
							'error',
							[
								'error_code'    => 'runtime_artifact_ignored',
								'error_message' => 'SiteFort runtime data file changed during scan and was ignored',
								'scan_result'   => wp_json_encode( $retry ),
							]
						);
						self::stage_skipped_issue(
							$file_path,
							'SiteFort runtime data file changed during scan and was ignored',
							[ 'error_code' => 'runtime_artifact_ignored' ]
						);
						continue;
					}

					$this->fsm->transition(
						(int) $file['id'],
						(string) $file['state'],
						'error',
						[
							'error_code'    => 'upload_retry_exhausted',
							'error_message' => $retry_exhausted_message,
							'scan_result'   => wp_json_encode( $retry ),
						]
					);
					if ( class_exists( '\SiteFort\Modules\Scanner\Repository\IssueRepository' ) ) {
						self::stage_skipped_issue( $file_path, $retry_issue_message, $retry );
					}
					continue;
				}

				if ( file_exists( $abs_path ) && is_readable( $abs_path ) ) {
					// For wp-config.php use the redacted content hash (same approach as
					// HashService) so the re-queued hash stays consistent with what
					// check-hash will register and upload_zip_batches will upload.
					if ( basename( $abs_path ) === 'wp-config.php' ) {
						$_raw          = @file_get_contents( $abs_path );
						$current_hash  = ( $_raw !== false )
							? hash( 'sha256', \SiteFort\Modules\Scanner\FileIntegrity\FileCollector::sanitize_wp_config_content( $_raw ) )
							: false;
						$current_size  = ( $_raw !== false )
							? strlen( \SiteFort\Modules\Scanner\FileIntegrity\FileCollector::sanitize_wp_config_content( $_raw ) )
							: 0;
					} else {
						$current_hash = hash_file( 'sha256', $abs_path );
						$current_size = filesize( $abs_path ) ?: 0;
					}
					if ( $current_hash && $current_hash !== $expected_hash ) {
						$this->fsm->transition(
							(int) $file['id'],
							(string) $file['state'],
							'hash_complete',
							[
								'file_hash'              => $current_hash,
								'file_size'              => $current_size,
								'hash_status'            => 'unknown',
								'scanned_with_signature' => null,
								'cloud_job_id'           => null,
								'error_code'             => $retry_error_code,
								'error_message'          => $rehash_message,
							]
						);
					} else {
						$this->fsm->transition(
							(int) $file['id'],
							(string) $file['state'],
							'pending_upload',
							[
								'cloud_job_id'  => null,
								'error_code'    => $retry_error_code,
								'error_message' => $reupload_message,
							]
						);
					}
				} else {
					$this->fsm->transition(
						(int) $file['id'],
						(string) $file['state'],
						'error',
						[
							'error_code'    => 'file_missing',
							'error_message' => 'File missing during retry after hash mismatch',
							'scan_result'   => wp_json_encode( $retry ),
						]
					);
					if ( class_exists( '\SiteFort\Modules\Scanner\Repository\IssueRepository' ) ) {
						self::stage_skipped_issue( $file_path, 'File missing during retry after hash mismatch', $retry );
					}
				}
				continue;
			}

			// Per-file cloud error: re-queue for recovery.
			if ( $result && ( $result['type'] ?? '' ) === 'error' ) {
				$msg = (string) ( $result['description'] ?? 'Cloud scan error' );

				$transition = $this->fsm->transition(
					(int) $file['id'],
					(string) $file['state'],
					'pending_upload',
					[
						'cloud_job_id'  => null,
						'error_code'    => 'cloud_scan_error',
						'error_message' => $msg,
						'scan_result'   => wp_json_encode( $result ),
					]
				);
				if ( is_wp_error( $transition ) ) {
					$this->fsm->transition(
						(int) $file['id'],
						(string) $file['state'],
						'error',
						[
							'error_code'    => 'cloud_scan_error',
							'error_message' => $msg,
							'scan_result'   => wp_json_encode( $result ),
						]
					);
				}
				continue;
			}

			$is_malware = false;
			if ( $result ) {
				$is_malware = ! empty( $result['match'] ) || ( $result['type'] ?? '' ) === 'malware';
			}

			if ( $is_malware ) {
				$result = self::merge_scan_results( $cached_result, $result );

				$malware_message = (string) ( $result['description'] ?? '' );
				$transition      = $this->fsm->transition(
					(int) $file['id'],
					$file['state'],
					'flagged',
					[
						'scan_result'   => wp_json_encode( $result ),
						'error_message' => $malware_message,
					]
				);
				if ( is_wp_error( $transition ) ) {
					sitefort_log( '[SiteFort] Failed to flag file after scan results: ' . $transition->get_error_message() );
				}
				$this->create_scan_issue( $file, $result );
			} else {
				// Clean scan (SAFE).
				$safe_entry = $safe_map[ $file_path ] ?? null;
				if ( $safe_entry && $hash_cache_repo && ! empty( $file['file_hash'] ) ) {
					$proof_b64  = $safe_entry['proof'] ?? '';
					$proof      = is_string( $proof_b64 ) ? base64_decode( $proof_b64, true ) : false;
					$entry_sig  = (string) ( $safe_entry['signature_version'] ?? '' );
					$expected_sig = $pinned_signature ?: $entry_sig;
					if ( $proof && $expected_sig !== '' ) {
						if ( $entry_sig !== '' && $entry_sig !== $expected_sig ) {
							$proof = false;
						}
					}
					if ( $proof && $expected_sig !== '' ) {
						$hash_cache_repo->upsert( $file['file_hash'], 'safe', $proof, $expected_sig );
					}
				}

				$transition = $this->fsm->transition(
					(int) $file['id'],
					$file['state'],
					'completed',
					[
						'scan_result' => $result ? wp_json_encode( $result ) : '{"status":"clean"}',
					]
				);
				if ( is_wp_error( $transition ) ) {
					sitefort_log( '[SiteFort] Failed to complete file after scan results: ' . $transition->get_error_message() );
				}
			}
		}

		$issue_count = is_array( $issues ) ? count( $issues ) : 0;
		do_action( 'sitefort_scan_results_processed', $this->scan_id, $job_id, $issue_count );
	}

	/**
	 * Create a malware issue from a scan result.
	 *
	 * @param array $file   File state record.
	 * @param array $result Scan result from Intel Engine.
	 */
	public function create_scan_issue( array $file, array $result ): void {
		$issue_threat  = MalwareFindingFormatter::resolveThreatLabel( $result );
		$issue_message = MalwareFindingFormatter::buildDescription( $result, (string) ( $file['file_path'] ?? '' ) );

		$issue_title = $issue_threat !== '' ? 'Malware Detected: ' . $issue_threat : 'Malware Detected';

		$data                     = $result;
		$data['threat_severity']  = $data['threat_severity']  ?? $result['severity']  ?? 'critical';
		$data['threat_type_raw']  = $data['threat_type_raw']  ?? ( $data['threat_type'] ?? '' );
		$data['threat_type']      = $issue_threat;
		$data['description']      = $issue_message;
		$data['threat_description'] = $issue_message;
		$data['summary']          = $issue_message;
		$data['detection_type']   = $data['detection_type']   ?? 'deep_scan';
		$data['file_hash']        = $file['file_hash']        ?? ( $data['file_hash'] ?? null );
		$data['repair_available'] = array_key_exists( 'repair_available', $data )
			? wp_validate_boolean( $data['repair_available'] )
			: false;

		if ( ! class_exists( 'SiteFort\\Modules\\Scanner\\FileIntegrity\\FileCollector' ) ) {
			require_once dirname( dirname( __FILE__ ) ) . '/FileIntegrity/FileCollector.php';
		}
		if ( class_exists( 'SiteFort\\Modules\\Scanner\\FileIntegrity\\FileCollector' ) ) {
			$file_info = ( new \SiteFort\Modules\Scanner\FileIntegrity\FileCollector() )->get_file_info_from_state( $file );
			if ( ! empty( $file_info['type'] ) && empty( $data['component_type'] ) ) {
				$data['component_type'] = (string) $file_info['type'];
			}
			if ( ! empty( $file_info['slug'] ) && empty( $data['slug'] ) ) {
				$data['slug'] = $file_info['slug'];
			}
			if ( ! empty( $file_info['version'] ) && empty( $data['version'] ) ) {
				$data['version'] = (string) $file_info['version'];
			}
			if ( ! empty( $file_info['locale'] ) && empty( $data['locale'] ) ) {
				$data['locale'] = (string) $file_info['locale'];
			}
			if ( ! isset( $data['last_modified'] ) && ! empty( $file_info['last_modified'] ) ) {
				$data['last_modified'] = (int) $file_info['last_modified'];
			}
		}
		if ( empty( $data['hash_status'] ) && ! empty( $file['hash_status'] ) ) {
			$data['hash_status'] = sanitize_key( (string) $file['hash_status'] );
		}

		$issue = [
			'scan_id'     => $this->scan_id,
			'file_path'   => $file['file_path'],
			'type'        => 'malware',
			'severity'    => 'critical',
			'title'       => $issue_title,
			'description' => $issue_message,
			'message'     => $issue_message,
			'data'        => $data,
			'status'      => 'open',
			'detected_at' => time(),
			'file_hash'   => $file['file_hash'],
		];

		if ( class_exists( 'SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' ) ) {
			$issue['hash'] = IssueRepository::generateHash( 'malware', $file['file_path'] );
		} else {
			$issue['hash'] = hash( 'sha256', 'malware:' . $file['file_path'] );
		}

		if ( class_exists( 'SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' ) ) {
			IssueRepository::stage( $issue );

			$modified_hash  = IssueRepository::generateHash( 'modified', $file['file_path'] );
			IssueRepository::delete( $modified_hash );
			IssueRepository::deleteStagedByHash( $this->scan_id, $modified_hash );
			$integrity_hash = IssueRepository::generateHash( 'integrity', $file['file_path'] );
			IssueRepository::delete( $integrity_hash );
			IssueRepository::deleteStagedByHash( $this->scan_id, $integrity_hash );
			$fake_image_hash = IssueRepository::generateHash( IssueType::FAKE_IMAGE, $file['file_path'] );
			IssueRepository::delete( $fake_image_hash );
			IssueRepository::deleteStagedByHash( $this->scan_id, $fake_image_hash );
		}
	}

	/**
	 * Merge scan results to preserve hash-check repair eligibility.
	 *
	 * @param array|null $cached_result   Hash check verdict payload.
	 * @param array|null $incoming_result Deep scan result payload.
	 * @return array
	 */
	public static function merge_scan_results( ?array $cached_result, ?array $incoming_result ): array {
		$merged = is_array( $incoming_result ) ? $incoming_result : [];
		if ( is_array( $cached_result ) ) {
			$has_cached_repair = array_key_exists( 'repair_available', $cached_result );
			$has_merged_repair = array_key_exists( 'repair_available', $merged );
			$cached_repair     = $has_cached_repair ? wp_validate_boolean( $cached_result['repair_available'] ) : null;

			if ( $has_cached_repair && true === $cached_repair ) {
				$merged['repair_available'] = true;
			} elseif ( $has_merged_repair ) {
				$merged['repair_available'] = wp_validate_boolean( $merged['repair_available'] );
			} elseif ( $has_cached_repair ) {
				$merged['repair_available'] = $cached_repair;
			}
			if ( ( ! array_key_exists( 'expected_hash', $merged ) || $merged['expected_hash'] === null )
				&& array_key_exists( 'expected_hash', $cached_result ) ) {
				$merged['expected_hash'] = $cached_result['expected_hash'];
			}
			if ( empty( $merged['hash_status'] ) ) {
				$hash_status = '';
				if ( ! empty( $cached_result['hash_status'] ) ) {
					$hash_status = (string) $cached_result['hash_status'];
				} elseif ( ! empty( $cached_result['status'] ) ) {
					$hash_status = (string) $cached_result['status'];
				}
				if ( $hash_status !== '' ) {
					$merged['hash_status'] = sanitize_key( $hash_status );
				}
			}
		}
		return $merged;
	}

	/**
	 * Mark all files belonging to a cloud job as error state.
	 *
	 * @param string $job_id        Intel Engine job ID.
	 * @param string $error_code    Machine-readable error code.
	 * @param string $error_message Human-readable error message.
	 */
	public function mark_job_files_as_error( string $job_id, string $error_code, string $error_message ): void {
		global $wpdb;
		$table_name = self::table_identifier( $wpdb->prefix . 'sitefort_file_states' );

		// Fetch affected files before updating (for issue staging).
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$affected_files = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT file_path, file_hash FROM {$table_name}
				WHERE scan_id = %s
				AND cloud_job_id = %s
				AND state NOT IN ('completed', 'flagged', 'error')",
				$this->scan_id,
				$job_id
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table_name}
				SET state = 'error',
					error_code = %s,
					error_message = %s,
					updated_at = %d
				WHERE scan_id = %s
				AND cloud_job_id = %s
				AND state NOT IN ('completed', 'flagged', 'error')",
				$error_code,
				$error_message,
				time(),
				$this->scan_id,
				$job_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		if ( ! empty( $affected_files ) && class_exists( 'SiteFort\\Modules\\Scanner\\Repository\\IssueRepository' ) ) {
			foreach ( $affected_files as $file ) {
				self::stage_skipped_issue(
					$file['file_path'] ?? '(unknown)',
					'Upload failed: ' . $error_message,
					[
						'error_code' => $error_code,
						'file_hash'  => $file['file_hash'] ?? null,
					]
				);
			}
		}
	}

	/**
	 * Log a skipped file to the scan activity log.
	 *
	 * Routes skipped items to the scan log instead of the findings list so
	 * they are visible without inflating issue counts.
	 *
	 * @param string $file_path File path (pass '(scan)' for non-file events).
	 * @param string $message   Human-readable message.
	 * @param array  $data      Extra context for log suffix.
	 */
	public static function stage_skipped_issue( string $file_path, string $message, array $data = [] ): void {
		if ( ! class_exists( '\SiteFort\Modules\Scanner\Service\ScanLogger' ) ) {
			require_once dirname( dirname( __FILE__ ) ) . '/Service/ScanLogger.php';
		}

		if ( self::is_runtime_artifact_path( $file_path ) ) {
			\SiteFort\Modules\Scanner\Service\ScanLogger::log(
				'[Ignored] ' . $file_path . ' - SiteFort runtime data file changed during scan and was ignored',
				1,
				'info'
			);
			return;
		}

		if ( $file_path && $file_path !== '(scan)' ) {
			$log_msg = '[Skipped] ' . $file_path . ' - ' . $message;
		} else {
			$log_msg = '[Skipped] ' . $message;
		}

		if ( ! empty( $data ) ) {
			$code = $data['error_code'] ?? $data['reason'] ?? null;
			if ( $code ) {
				$log_msg .= ' (' . $code . ')';
			}
		}

		\SiteFort\Modules\Scanner\Service\ScanLogger::log( $log_msg, 2, 'warning' );
	}
}
