<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
	class Malcure_Advanced {

		private $wp_custom_dir  = false;
		public $log_marker      = '// MALCURE LOG MARKER ?>';
		private $logfile_header = '';
		private $wp_home_dir    = '';
		private $logging        = '';
		private $logfile        = '';


		function __construct() {

			$cdt                    = gmdate( 'Y' );
			$brand                  = <<<'EOD'
            

███████████████████████████████████████████████████████████████████████████████
███████████████████████████████████████████████████████████████████████████████
█████                                                                    ██████
█████    ███╗   ███╗ █████╗ ██╗      ██████╗██╗   ██╗██████╗ ███████╗    ██████
█████    ████╗ ████║██╔══██╗██║     ██╔════╝██║   ██║██╔══██╗██╔════╝    ██████
█████    ██╔████╔██║███████║██║     ██║     ██║   ██║██████╔╝█████╗      ██████
█████    ██║╚██╔╝██║██╔══██║██║     ██║     ██║   ██║██╔══██╗██╔══╝      ██████
█████    ██║ ╚═╝ ██║██║  ██║███████╗╚██████╗╚██████╔╝██║  ██║███████╗    ██████
█████    ╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝    ██████
█████                                                                    ██████
███████████████████████████████████████████████████████████████████████████████
█████                                                                    ██████
█████                     © Copyright %s MalCure                         ██████
█████                     Author —                                       ██████
█████                     Shiv / shiv@malcure.com                        ██████
█████                                                                    ██████
███████████████████████████████████████████████████████████████████████████████
███████████████████████████████████████████████████████████████████████████████

EOD;
			$brand                  = sprintf( $brand, $cdt );
			$allow_bootstrap_output = ! $this->is_machine_readable_stdout_request( $this->get_cli_arguments() );

			if ( $allow_bootstrap_output ) {
				WP_CLI::log( WP_CLI::colorize( $this->brand_color() . $brand . '%n' ) );
			}
			$this->init();
			if ( $allow_bootstrap_output ) {
				$this->sync( array(), array( 'silent' => 1 ) );
			}
		}

		/**
		 * Determine whether the active command expects machine-readable STDOUT.
		 *
		 * @param array<int,string> $argv Sanitized argv list.
		 * @return bool True when the active command writes machine-readable STDOUT.
		 */
		private function is_machine_readable_stdout_request( $argv ) {
			return $this->is_json_format_request( $argv ) || $this->is_stdout_definitions_export( $argv );
		}

		/**
		 * Check whether the active command requested JSON output.
		 *
		 * @param array<int,string> $argv Sanitized argv list.
		 * @return bool True when `--format=json` or `--format json` was requested.
		 */
		private function is_json_format_request( $argv ) {
			$format = $this->get_cli_argument_value( $argv, 'format' );

			return is_string( $format ) && 'json' === strtolower( $format );
		}

		/**
		 * Return sanitized CLI arguments from the current request.
		 *
		 * @return array<int,string> Sanitized argv list.
		 */
		private function get_cli_arguments() {
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Each argv element is unslashed and sanitized before this method returns it.
			$argv = isset( $_SERVER['argv'] ) && is_array( $_SERVER['argv'] ) ? $_SERVER['argv'] : array();

			return array_values(
				array_map(
					static function ( $arg ) {
						return sanitize_text_field( wp_unslash( (string) $arg ) );
					},
					$argv
				)
			);
		}

		/**
		 * Read an associative CLI argument from argv.
		 *
		 * Supports both `--flag=value` and `--flag value` forms.
		 *
		 * @param array<int,string> $argv          Sanitized argv list.
		 * @param string            $argument_name Argument name without leading dashes.
		 * @return string|null Argument value when present.
		 */
		private function get_cli_argument_value( $argv, $argument_name ) {
			$argument_name = ltrim( (string) $argument_name, '-' );
			$flag          = '--' . $argument_name;
			$prefix        = $flag . '=';
			$argc          = count( $argv );

			for ( $i = 0; $i < $argc; ++$i ) {
				if ( 0 === strpos( $argv[ $i ], $prefix ) ) {
					return substr( $argv[ $i ], strlen( $prefix ) );
				}

				if ( $flag === $argv[ $i ] && isset( $argv[ $i + 1 ] ) && 0 !== strpos( $argv[ $i + 1 ], '--' ) ) {
					return $argv[ $i + 1 ];
				}
			}

			return null;
		}

		/**
		 * Check whether the active command is exporting definitions to STDOUT.
		 *
		 * @param array<int,string> $argv Sanitized argv list.
		 * @return bool True when `export-definitions -` is being executed.
		 */
		private function is_stdout_definitions_export( $argv ) {
			$subcommand_index = array_search( 'export-definitions', $argv, true );

			if ( false === $subcommand_index ) {
				return false;
			}

			$argc = count( $argv );
			for ( $i = $subcommand_index + 1; $i < $argc; ++$i ) {
				if ( 0 === strpos( $argv[ $i ], '--' ) ) {
					continue;
				}

				return '-' === $argv[ $i ];
			}

			return false;
		}

		private function init() {
			$this->prerequisites();
			$wpmr = wp_malware_removal();
			$met  = @ini_get( 'max_execution_time' );
			$ml   = @ini_get( 'memory_limit' );
			if ( ! $wpmr->is_advanced_edition() ) {
				if ( ! array_intersect( array( 'help', 'activate', 'register', 'sync', 'status' ), isset( $_SERVER['argv'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_SERVER['argv'] ) ) : array() ) ) {
					WP_CLI::error( 'Full CLI integration is only possible with Malcure Advanced Edition. Please see: https://malcure.com/?p=116', false );
					if ( $wpmr->get_setting( 'license_key' ) ) {
						$this->license_req( 'check_license', $wpmr->get_setting( 'license_key' ) );
					}
					$this->aalert();
					die();
				}
			}
		}

		private function get_log_dir( $path ) {
			if ( empty( $path ) ) {
				return trailingslashit( $this->normalise_path( WP_CONTENT_DIR ) );
			}

			$pathinfo = pathinfo( $path );
			if ( ! empty( $pathinfo['dirname'] ) && is_dir( $pathinfo['dirname'] ) ) {
				return trailingslashit( $this->normalise_path( $pathinfo['dirname'] ) );
			} else {
				return trailingslashit( $this->normalise_path( WP_CONTENT_DIR ) );
			}
		}

		/**
		 * Run a malware scan from WP-CLI.
		 *
		 * Supports vulnerability, database, redirect, and file scanning, with
		 * optional JSON log export for reproducible troubleshooting.
		 *
		 * ## OPTIONS
		 *
		 * [--mcbatchsize=<size>]
		 * : Number of files to scan per batch. Defaults to 50.
		 *
		 * [--mcskipvuln]
		 * : Skip vulnerability scanning.
		 *
		 * [--mcsuspicious]
		 * : Include suspicious findings in the output.
		 *
		 * [--mcskipdb]
		 * : Skip database scanning.
		 *
		 * [--mcskipfiles]
		 * : Skip file scanning.
		 *
		 * [--mcskipredirects]
		 * : Skip redirect-hijack checks.
		 *
		 * [--mcskipusers]
		 * : Skip user account audit (application passwords, identity gaps, session anomalies).
		 *
		 * [--mcskipdirs=<dirs>]
		 * : Comma-separated relative directory names to skip.
		 *
		 * [--mcregex=<regex>]
		 * : Run an additional custom regex against scanned files.
		 *
		 * [--mcdbquery=<query>]
		 * : SQL LIKE-style selector used with a custom database regex.
		 *
		 * [--mcdbregex=<regex>]
		 * : Run an additional custom regex against matching database content.
		 *
		 * [--log=<path>]
		 * : Write a protected `.json.php` scan log to the given path.
		 *
		 * [--logformat=<format>]
		 * : Log format to use when `--log` is present.
		 * ---
		 * default: json
		 * options:
		 *   - json
		 * ---
		 *
		 * [--mcfiles=<paths>]
		 * : Comma-separated absolute file paths to scan.
		 *
		 * [--mcscanonlydirs=<dirs>]
		 * : Comma-separated relative directories to scan.
		 *
		 * [--mcdebug]
		 * : Show debug output instead of the progress bar.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure scan
		 *     wp malcure scan --mcskipdb --mcskipredirects
		 *     wp malcure scan --mcfiles=/var/www/html/wp-content/plugins/example/plugin.php
		 *     wp malcure scan --log=scan-log --logformat=json
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function scan( $args, $assoc_args ) {
			$this->info();
			$wpmr                    = wp_malware_removal();
			$infected                = false;
			$start_time              = time();
			$assoc_args['timestamp'] = time();

			// Log scan initiation event — matches stateful and browser paths.
			$wpmr->log_event(
				'malcure_scan_initiated',
				array(
					'scan_id'         => (string) $assoc_args['timestamp'],
					'scan_source'     => 'cli',
					'definitions_ver' => $wpmr->get_definition_version(),
				)
			);

			$assoc_args['mcskipvuln']      = $this->get_flag( $assoc_args, 'mcskipvuln' );
			$assoc_args['mcsuspicious']    = $this->get_flag( $assoc_args, 'mcsuspicious' );
			$assoc_args['mcskipdb']        = $this->get_flag( $assoc_args, 'mcskipdb' );
			$assoc_args['mcskipfiles']     = $this->get_flag( $assoc_args, 'mcskipfiles' );
			$assoc_args['mcskipredirects'] = $this->get_flag( $assoc_args, 'mcskipredirects' );
			// User scan is on by default (matching the stateless browser path). Disable with --mcskipusers.
			$assoc_args['mcskipusers'] = $this->get_flag( $assoc_args, 'mcskipusers' );
			$assoc_args['mcdebug']     = $this->get_flag( $assoc_args, 'mcdebug' );
			if ( ! empty( $assoc_args['log'] ) ) {
				$logfile = $assoc_args['log'];

				if ( ! empty( $assoc_args['logformat'] ) && in_array( $assoc_args['logformat'], $valid_formats ) ) {
					$logformat = $assoc_args['logformat'];
				} else {
					$logformat = 'json';
				}

				$logfile       = $this->get_log_dir( $logfile ) . pathinfo( $logfile )['filename'] . '.' . $logformat . '.php';
				$this->logfile = $logfile;

				WP_CLI::log( WP_CLI::colorize( $this->heading_format() . "\nLogging Scan Results To File :\t" . $logfile . '%n' ) );
				if ( file_exists( $logfile ) ) {
					WP_CLI::log( WP_CLI::colorize( $this->heading_format() . "\t★ Previous log-file will be overwritten :\t" . $logfile . '%n' ) );
				}
				$this->logging = true;

				$this->log( $this->logfile_header, 0 );

				$logcontents = array(
					'starttime' => $this->get_time(),
					'env'       => array(
						'website url'       => get_bloginfo( 'url' ),
						'wordpress url'     => get_bloginfo( 'wpurl' ),
						'wordpress version' => get_bloginfo( 'version' ),
						'multisite'         => is_multisite(),
						'active theme'      => get_bloginfo( 'stylesheet_directory' ),
						'parent theme'      => get_bloginfo( 'template_directory' ),
						'php version'       => phpversion(),
						'server'            => php_uname(),
						'users roles'       => array(),
						'must-use plugins'  => array(),
						'drop-in plugins'   => array(),
					),
					'malcure'   => array(
						'definition version' => $wpmr->get_definition_version(),
						'last updated'       => $wpmr->get_last_updated_ago(),
					),

				);

				global $wp_roles;
				foreach ( $wp_roles->roles as $role => $capabilities ) {
					$logcontents['env']['users roles'][] = $role;
				}

				$mu = get_mu_plugins();
				foreach ( $mu as $key => $value ) {
					$logcontents['env']['must-use plugins'][] = $key;
				}

				$dropins = get_dropins();
				foreach ( $dropins as $key => $value ) {
					$logcontents['env']['drop-in plugins'][] = $key;
				}

				$this->log( $logcontents );
			}
			$logcontents['messages']        = array();
			$assoc_args['do_vuln_scan']     = ! $assoc_args['mcskipvuln'];
			$assoc_args['do_db_scan']       = ! $assoc_args['mcskipdb'];
			$assoc_args['do_file_scan']     = ! $assoc_args['mcskipfiles'];
			$assoc_args['do_redirect_scan'] = ! $assoc_args['mcskipredirects'];
			$assoc_args['do_user_scan']     = ! $assoc_args['mcskipusers'];
			if ( ! empty( $assoc_args['mcfiles'] ) ) {
				$assoc_args['mcfiles'] = explode( ',', $assoc_args['mcfiles'] );
			}

			if ( empty( $assoc_args['mcbatchsize'] ) ) {
				$assoc_args['mcbatchsize'] = 50;
			}

			$batchsize                      = $assoc_args['mcbatchsize'];
			$logcontents['malcure']['args'] = $assoc_args;
			$this->log( $logcontents );

			// Truncate the issues table before bootstrap — matches the wpmr_init_scan()
			// AJAX convention via the shared reset_issues() surface, so browser and CLI
			// scans both start from a clean issues table. Must run BEFORE bootstrap()
			// because bootstrap() calls vulnerability_scan() which calls insert_issue().
			$wpmr->reset_issues();

			$bootstrap = $wpmr->bootstrap( $assoc_args );

			if ( $bootstrap['last_updated'] == 'Never' ) {
				WP_CLI::warning( 'Your site is using out of date definitions! Limited dection ability...' );
			}
			if ( ! empty( $assoc_args['mcfiles'] ) ) {
				$files = $assoc_args['mcfiles'];
			} else {
				$files = $bootstrap['files'];
			}
			$db_scan         = empty( $bootstrap['db_scan'] ) ? false : $bootstrap['db_scan'];
			$title_hack      = $bootstrap['title_hack'];
			$redirect_hijack = $bootstrap['redirect_hijack'];
			$vuln_scan       = $bootstrap['vulnerabilities_scan'];

			if ( ! empty( $vuln_scan ) ) {
				$infected                                  = true;
				$logcontents['results']['vulnerabilities'] = array();
				WP_CLI::log( "\n" . WP_CLI::colorize( $this->heading_format() . '  VULNERABILITY SCAN RESULTS  %n' ) );

				foreach ( $vuln_scan as $category => $items ) {
					if ( ! empty( $items ) && is_array( $items ) ) {
						foreach ( $items as $item_name => $details ) {
							$severity  = isset( $details['severity'] ) ? (string) $details['severity'] : 'unknown';
							$signature = isset( $details['signature'] ) ? (string) $details['signature'] : 'unknown';
							$message   = isset( $details['message'] ) ? (string) $details['message'] : 'No message available';

							WP_CLI::log( WP_CLI::colorize( '%r' . "\t" . strtoupper( $severity ) . "\t" . $signature . "\t" . $item_name . ' - ' . html_entity_decode( $message ) . '%n' ) );
							$logcontents['results']['vulnerabilities'][] = array(
								'category'  => $category,
								'name'      => $item_name,
								'severity'  => $severity,
								'reference' => trailingslashit( MALCURE_API ) . 'webscan/sig/' . $signature,
								'message'   => html_entity_decode( $message ),
							);
						}
					}
				}
				$this->log( $logcontents );
			}

			$logcontents['malcure']['workload'] = array(
				'files to scan' => $bootstrap['count'],
				'checksums'     => $bootstrap['checksums'],
			);
			$this->log( $logcontents );

			WP_CLI::log( "\nFiles To Scan :\t" . $bootstrap['count'] );
			WP_CLI::log( "Checksums     :\t" . $bootstrap['checksums'] );
			WP_CLI::log( "Batch-Size    :\t" . $batchsize );

			if ( ! empty( $title_hack ) ) {
				$infected = true;
				WP_CLI::log( "\n" . WP_CLI::colorize( $this->heading_format() . '  TITLE SCAN RESULTS  ' . '%n' ) );
				WP_CLI::log( WP_CLI::colorize( $this->get_severity_format( 'SEVERE' ) ) . '    ' . 'Site Title is Hacked' );
				$logcontents['results']['title_hack'] = array(
					'severity'  => 'severe',
					'reference' => 'N/A',
					'message'   => 'Site Title is Hacked',
				);
				$this->log( $logcontents );
			}
			if ( ! empty( $redirect_hijack ) ) {
				$infected = true;
				WP_CLI::log( "\n" . WP_CLI::colorize( $this->heading_format() . '  REDIRECT HIJACK SCAN RESULTS  ' . '%n' ) );
				WP_CLI::log( WP_CLI::colorize( $this->get_severity_format( 'SEVERE' ) ) . '    ' . 'Site redirect hijack dettected' );
				$logcontents['results']['redirect_hijack'] = array(
					'severity'  => 'severe',
					'reference' => 'N/A',
					'message'   => 'Site redirect hijack dettected',
				);
				$this->log( $logcontents );
			}
			// User Account Audit — runs after vuln/title/redirect, before db/file scan.
			// Matching the stateless browser flow. Bypasses when --mcskipusers is set.
			if ( empty( $assoc_args['mcskipusers'] ) && ! empty( $bootstrap['user_stats'] ) ) {
				$user_scan_results = $wpmr->scan_users_cli( $bootstrap['user_stats'] );
				WP_CLI::log( "\n" . WP_CLI::colorize( $this->heading_format() . '  USER ACCOUNT AUDIT  %n' ) );
				if ( ! empty( $user_scan_results ) ) {
					$severe                          = 0;
					$suspicious                      = 0;
					$skipped                         = 0;
					$logcontents['results']['users'] = array();
					foreach ( $user_scan_results as $user ) {
						switch ( $user['severity'] ) {
							case 'severe':
								++$severe;
								$infected = true;
								break;
							case 'suspicious':
								++$suspicious;
								break;
							case 'skipped':
								++$skipped;
								break;
						}
						// No username broadcast in the scan summary; no email addresses exposed.
						// The operator follows up in wp-admin/users.php or via the user-edit URL,
						// which is logged only when --log is set.
						// Canonical reason string — format_user_reasons() in WPMR_Scanner is the
						// single source of truth shared by scan_user() persistence, the WP-CLI
						// summary, and the browser tooltip. Do not re-implement here.
						$ip_spread_score = isset( $user['ip_spread_score'] ) ? (float) $user['ip_spread_score'] : 0.0;
						$summary         = $wpmr->format_user_reasons(
							array(
								'missing_email'      => ! empty( $user['missing_email'] ),
								'session_anomaly'    => $ip_spread_score >= $wpmr::IP_SPREAD_THRESHOLD,
								'missing_first_name' => ! empty( $user['missing_first_name'] ),
								'missing_last_name'  => ! empty( $user['missing_last_name'] ),
								'has_app_passwords'  => ! empty( $user['has_app_passwords'] ),
								'app_password_count' => isset( $user['app_password_count'] ) ? (int) $user['app_password_count'] : 0,
								'has_elevated_caps'  => ! empty( $user['has_elevated_caps'] ),
							)
						);
						WP_CLI::log(
							WP_CLI::colorize(
								$this->get_severity_format( strtoupper( $user['severity'] ) )
								. strtoupper( $user['severity'] ) . "\tUser ID " . (int) $user['ID'] . "\t" . $summary . '%n'
							)
						);
						$logcontents['results']['users'][] = array(
							'severity'  => $user['severity'],
							'user_id'   => (int) $user['ID'],
							'reference' => is_multisite()
								? network_admin_url( 'user-edit.php?user_id=' . (int) $user['ID'] )
								: admin_url( 'user-edit.php?user_id=' . (int) $user['ID'] ),
							'message'   => 'User ID ' . (int) $user['ID'] . "\t" . $summary,
						);
					}
					WP_CLI::log(
						WP_CLI::colorize( $this->heading_format() . "\tSeverity breakdown: %n" )
						. $severe . ' severe, ' . $suspicious . ' suspicious, ' . $skipped . ' skipped.'
					);
					$this->log( $logcontents );
				} else {
					WP_CLI::log( WP_CLI::colorize( '%g' . 'No suspicious users found.%n' ) );
				}
			}
			$total = $bootstrap['count'];

			if ( ! empty( $db_scan ) ) {
				$infected                           = true;
				$logcontents['results']['database'] = array();
				WP_CLI::log( "\n" . WP_CLI::colorize( $this->heading_format() . '  DATABASE SCAN RESULTS  %n' ) );
				foreach ( $db_scan as $key => $value ) {
					WP_CLI::log( WP_CLI::colorize( '%r' . "\t" . strtoupper( $value['severity'] ) . "\t" . $value['signature'] . "\t" . html_entity_decode( $value['message'] ) . '%n' ) );
					$logcontents['results']['database'][] = array(
						'severity'  => $value['severity'],
						'reference' => trailingslashit( MALCURE_API ) . 'webscan/sig/' . $value['signature'],
						'message'   => html_entity_decode( $value['message'] ),
					);
				}
				$this->log( $logcontents );
			}
			if ( empty( $assoc_args['mcdebug'] ) || $assoc_args['mcdebug'] != 'true' ) {
				$progress = WP_CLI\Utils\make_progress_bar( 'Progress:', count( $files ) );
			} else {
				$progress = false;
			}
			if ( count( $files ) ) {
				if ( $this->wp_custom_dir ) {
					WP_CLI::error( 'WordPress is installed in a custom directory ' . $this->wp_home_dir, 0 );
					WP_CLI::error( 'Malcure will only consider files in the above WordPress directory.', 0 );
					$logcontents['messages'][] = 'WordPress installed in custom directory. Excluding files above WordPress directory.';
					$this->log( $logcontents );
				}
				WP_CLI::log( "\n" . WP_CLI::colorize( $this->heading_format() . '  FILE SCAN RESULTS  %n' ) );
				do {
					$this->extend_cli_time_limit( 0 );
					$arrbatch = array_splice( $files, 0, $batchsize );
					if ( $progress ) {
						$last_file = str_replace( wp_normalize_path( ABSPATH ), '', wp_normalize_path( $arrbatch[ count( $arrbatch ) - 1 ] ) );
						$progress->tick( count( $arrbatch ), ( $total - count( $files ) ) . ' of ' . $total . ' ' . $last_file );
					}
					if ( ! $progress ) {
						$i = (string) number_format( round( ( ( ( $total - count( $files ) ) / $total ) * 100 ), 1 ), 1, '.', '' );
						WP_CLI::log( str_pad( $i, ( strlen( $i ) ), ' ' ) . "%\t" );
					}
					$batch                 = $arrbatch;
					$assoc_args['mcfiles'] = $batch; // array_map( array( $this, 'encode_filename' ), $batch );
					$result                = $wpmr->wpmr_scan_files( $assoc_args );
					$result                = $result['report'];
					foreach ( $result as $file => $report ) {
						if ( in_array( $report['severity'], array( 'severe', 'high' ) ) ) {
							$infected = true;
						}
						WP_CLI::log( WP_CLI::colorize( $this->get_severity_format( strtoupper( $report['severity'] ) ) . '    ' . strtoupper( $report['severity'] ) . '    ' . $report['signature'] . "\t\t" . $file . '%n' ) );
						$logcontents['results']['files'][] = array(
							'severity'  => $report['severity'],
							'reference' => trailingslashit( MALCURE_API ) . 'webscan/sig/' . $report['signature'],
							'message'   => $file,
						);
					}
					$this->log( $logcontents );
				} while ( count( $files ) );
			}
			if ( ! empty( $progress ) ) {
				$progress->finish();
			}
			echo "\n";
			$end_time       = time();
			$execution_time = ( $end_time - $start_time );

			// Log scan completion event — matches stateful and browser paths.
			$wpmr->log_event(
				'malcure_scan_completed',
				array(
					'scan_id'         => (string) $assoc_args['timestamp'],
					'scan_source'     => 'cli',
					'outcome'         => $infected ? 'infected' : 'clean',
					'issue_count'     => $wpmr->get_issue_count(),
					'duration_sec'    => max( 0, $execution_time ),
					'definitions_ver' => $wpmr->get_definition_version(),
				)
			);

			if ( ! $infected && $assoc_args['do_db_scan'] && empty( $assoc_args['mcskipdirs'] ) ) {
				$wpmr->delete_setting( 'infected' );
				$wpmr->ss_delete_setting( 'infected' );
			}
			WP_CLI::success( 'Malcure Scan Completed in ' . human_time_diff( $start_time, $end_time ) . "!\n" );
			$this->aalert();
		}

		/**
		 * Update definitions from the current freshness state.
		 *
		 * ## OPTIONS
		 *
		 * [--mcforce]
		 * : Force a live definitions check and apply the update even when automatic definition updates are disabled.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure sync
		 *     wp malcure sync --mcforce
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function sync( $args = array(), $assoc_args = array() ) {
			$wpmr  = wp_malware_removal();
			$force = $this->get_flag( $assoc_args, 'mcforce' );
			// mcforce disables cached skip logic so sync always performs a live availability check.
			$wpmr->check_definitions( ! $force );
			$updates    = $wpmr->definition_updates_available();
			$can_update = ( $wpmr->get_setting( 'def_auto_update_enabled' ) || $force );
			$silent     = $this->get_flag( $assoc_args, 'silent' );

			if ( ! $updates && ! $can_update ) {
				if ( ! $silent ) {
					WP_CLI::log( WP_CLI::colorize( '%GNo updates available.%R %nPlease enable definition-auto-updates in settings to auto-update definitions.' ) );
				}
				return;
			} elseif ( ! $updates && $can_update ) {
				if ( ! $silent ) {
					WP_CLI::log( WP_CLI::colorize( '%GNo updates available.%R %n Definitions are at version ' . $wpmr->get_definition_version() ) );
				}
				return;
			} elseif ( $updates && ! $can_update ) {
				if ( ! $silent ) {
					WP_CLI::log( WP_CLI::colorize( "%WNew Definition Updates Are Available.\t%1 INSTALLED: " . $updates['current'] . " \t%3 LATEST: " . $updates['new'] . ' %n' ) );
					WP_CLI::log( WP_CLI::colorize( '%WPlease enable definition-auto-updates in settings to auto-update definitions.%n' ) );
				}
				return;
			} elseif ( $updates && $can_update ) {
				$from_version = $wpmr->get_definition_version();
				$wpmr->update_definitions_cli( 1 );
				// Report the installed version transition, not the stale pre-update availability hint.
				$to_version = $wpmr->get_definition_version();
				if ( ! $silent ) {
					WP_CLI::log( WP_CLI::colorize( '%GUpdated to the latest definitions from version %R' . $from_version . '%n to %Y' . $to_version . '%n.' ) );
				}
				return;
			}
		}

		/**
		 * Force a full official checksum refresh for core, plugins, and themes.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure refresh-checksums
		 *
		 * @subcommand refresh-checksums
		 *
		 * @param array $args Positional arguments.
		 * @param array $assoc_args Associative arguments.
		 * @return void
		 */
		function refresh_checksums( $args = array(), $assoc_args = array() ) {
			unset( $args, $assoc_args );

			$wpmr     = wp_malware_removal();
			$manifest = $wpmr->build_checksums_manifest();

			$plugin_targets = ( ! empty( $manifest['plugins'] ) && is_array( $manifest['plugins'] ) ) ? count( $manifest['plugins'] ) : 0;
			$theme_targets  = ( ! empty( $manifest['themes'] ) && is_array( $manifest['themes'] ) ) ? count( $manifest['themes'] ) : 0;

			WP_CLI::log( 'Refreshing official checksums for the current installation...' );
			WP_CLI::log( 'Targets: core, ' . $plugin_targets . ' plugins, ' . $theme_targets . ' themes' );

			// A forced CLI refresh should bypass the short-lived fetch lock left by a previous failure.
			delete_transient( 'wpmr_checksum_fetch_lock' );

			$start_time = microtime( true );
			$checksums  = $wpmr->get_core_checksums( false );
			$elapsed    = microtime( true ) - $start_time;

			if ( empty( $checksums ) || ! is_array( $checksums ) ) {
				WP_CLI::error( 'Failed to refresh official checksums.' );
			}

			$core_files   = 0;
			$plugin_files = 0;
			$theme_files  = 0;

			foreach ( array_keys( $checksums ) as $path ) {
				$path = (string) $path;
				if ( 0 === strpos( $path, 'wp-content/plugins/' ) ) {
					++$plugin_files;
				} elseif ( 0 === strpos( $path, 'wp-content/themes/' ) ) {
					++$theme_files;
				} else {
					++$core_files;
				}
			}

			WP_CLI::log( 'Checksum files refreshed: core=' . $core_files . ', plugins=' . $plugin_files . ', themes=' . $theme_files );
			WP_CLI::success( 'Refreshed official checksums for ' . count( $checksums ) . ' files in ' . sprintf( '%.3f', $elapsed ) . ' seconds.' );
		}

		/**
		 * Export the active definitions payload to a JSON file.
		 *
		 * ## OPTIONS
		 *
		 * <path>
		 * : Output file path. Use `-` to write the JSON payload to STDOUT.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure export-definitions ./malcure-definitions.json
		 *     wp malcure export-definitions -
		 *
		 * @subcommand export-definitions
		 *
		 * @param array $args Positional arguments.
		 * @param array $assoc_args Associative arguments.
		 * @return void
		 */
		function export_definitions( $args, $assoc_args ) {
			unset( $assoc_args );

			if ( empty( $args[0] ) ) {
				WP_CLI::error( 'Please provide an output path. Example: wp malcure export-definitions ./malcure-definitions.json' );
			}

			$wpmr        = wp_malware_removal();
			$destination = $this->resolve_definitions_json_path( (string) $args[0] );
			$signatures  = $this->get_stored_definitions_payload();
			$validation  = $this->validate_definitions_payload( $signatures );

			if ( '' !== $validation ) {
				WP_CLI::error( 'Active definitions payload is invalid: ' . $validation );
			}

			$sig_time = $wpmr->get_setting( 'sig_time' );
			$sig_time = is_numeric( $sig_time ) ? absint( $sig_time ) : 0;

			$payload = array(
				'format'             => 'malcure-definitions-export-v1',
				'exported_at_gmt'    => gmdate( 'c' ),
				'plugin_version'     => isset( $wpmr->plugin_data['Version'] ) ? (string) $wpmr->plugin_data['Version'] : '',
				'definition_version' => isset( $signatures['v'] ) ? (string) $signatures['v'] : '',
				'definition_count'   => $wpmr->get_definition_count(),
				'sig_time'           => $sig_time,
				'sig_time_gmt'       => $this->format_utc_timestamp( $sig_time ),
				'signatures'         => $signatures,
			);

			$json = wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );

			if ( false === $json ) {
				WP_CLI::error( 'Could not encode the definitions payload to JSON.' );
			}

			if ( '-' === $destination ) {
				WP_CLI::line( $json );
				return;
			}

			if ( ! function_exists( 'WP_Filesystem' ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
			}

			global $wp_filesystem;
			if ( ! WP_Filesystem() || ! $wp_filesystem ) {
				WP_CLI::error( 'Could not initialize WordPress filesystem API for the export destination.' );
			}

			if ( file_exists( $destination ) && ! $wp_filesystem->is_writable( $destination ) ) {
				WP_CLI::error( 'Output file is not writable: ' . $destination );
			}

			$directory = dirname( $destination );

			if ( ! is_dir( $directory ) ) {
				WP_CLI::error( 'Output directory does not exist: ' . $directory );
			}

			if ( ! $wp_filesystem->is_writable( $directory ) ) {
				WP_CLI::error( 'Output directory is not writable: ' . $directory );
			}

			$bytes_written = file_put_contents( $destination, $json, LOCK_EX );

			if ( false === $bytes_written ) {
				WP_CLI::error( 'Could not write definitions JSON to ' . $destination );
			}

			WP_CLI::success( 'Exported definitions version ' . $payload['definition_version'] . ' (' . $payload['definition_count'] . ' signatures) to ' . $destination );
		}

		/**
		 * Import a definitions JSON file into the active environment.
		 *
		 * Accepts the raw stored definitions payload, the CLI export wrapper, the
		 * legacy `success -> data` response wrapper, and the SaaS
		 * `success -> data -> payload -> signatures` wrapper.
		 *
		 * ## OPTIONS
		 *
		 * <path>
		 * : Input file path. Use `-` to read the JSON payload from STDIN.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure import-definitions ./malcure-definitions.json
		 *     wp malcure import-definitions - < ./malcure-definitions.json
		 *
		 * @subcommand import-definitions
		 *
		 * @param array $args Positional arguments.
		 * @param array $assoc_args Associative arguments.
		 * @return void
		 */
		function import_definitions( $args, $assoc_args ) {
			unset( $assoc_args );

			if ( empty( $args[0] ) ) {
				WP_CLI::error( 'Please provide a definitions JSON file path. Example: wp malcure import-definitions ./malcure-definitions.json' );
			}

			$wpmr    = wp_malware_removal();
			$source  = $this->resolve_definitions_json_path( (string) $args[0], true );
			$payload = $this->read_definitions_json_source( $source );

			$normalized       = $this->normalize_definitions_import_payload( $payload );
			$definitions_data = $normalized['definitions_data'];
			$import_sig_time  = $normalized['import_sig_time'];

			$validation = $this->validate_definitions_payload( $definitions_data );

			if ( '' !== $validation ) {
				WP_CLI::error( 'Invalid definitions JSON: ' . $validation );
			}

			$persisted_definitions = $wpmr->persist_definitions_payload( $definitions_data );

			if ( is_wp_error( $persisted_definitions ) ) {
				WP_CLI::error( $persisted_definitions->get_error_message() );
			}

			$wpmr->update_setting( 'sig_time', $import_sig_time );
			$wpmr->purge_definition_update_version();
			$wpmr->delete_setting( 'sig_checked_time' );

			WP_CLI::success( 'Imported definitions version ' . (string) $persisted_definitions['v'] . ' (' . $wpmr->get_definition_count() . ' signatures) from ' . ( '-' === $source ? 'STDIN' : $source ) );
		}

		/**
		 * Register this install for free definitions updates.
		 *
		 * ## OPTIONS
		 *
		 * --mc-email=<email>
		 * : Registration email address.
		 *
		 * [--mc-fname=<first-name>]
		 * : First name. Defaults to the email local part.
		 *
		 * [--mc-lname=<last-name>]
		 * : Last name. Defaults to the derived first name.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure register --mc-email=ops@example.com
		 *     wp malcure register --mc-email=ops@example.com --mc-fname=Ops --mc-lname=Team
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function register( $args, $assoc_args ) {
			if ( empty( $assoc_args['mc-email'] ) ) {
				WP_CLI::error( 'Need email' );
			}
			if ( ! filter_var( $assoc_args['mc-email'], FILTER_VALIDATE_EMAIL ) ) {
				WP_CLI::error( 'Need authentic email.' );
			}
			$email = $assoc_args['mc-email'];
			$fn    = $ln = '';
			if ( empty( $assoc_args['mc-fname'] ) ) {
				$fn = explode( '@', $assoc_args['mc-email'] )[0];
			}
			if ( empty( $assoc_args['mc-lname'] ) ) {
				$ln = $fn;
			}
			$wpmr = wp_malware_removal();
			$wpmr->wpmr_cli_register( $email, $fn, $ln, 1 );
		}

		private function encode_filename( $filename ) {
			return base64_encode( urlencode( $filename ) );
		}

		/**
		 * Show environment, license, checksum, and definitions details for this install.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure info
		 *
		 * @return void
		 */
		function info() {
			$wpmr  = wp_malware_removal();
			$files = $wpmr->return_all_files();
			global $wp_version;
			WP_CLI::log( WP_CLI::colorize( '%n%wMalcure Advanced Edition ' . $wpmr->plugin_data['Version'] . '%B' ) );

			// Display clean registration details
			$user_details = $wpmr->get_setting( 'user' );
			if ( ! empty( $user_details ) ) {
				WP_CLI::log( WP_CLI::colorize( '%n%wRegistration Details:%n' ) );
				if ( isset( $user_details['ID'] ) ) {
					WP_CLI::log( "\tUser ID       : " . $user_details['ID'] );
				}
				if ( isset( $user_details['user_email'] ) ) {
					WP_CLI::log( "\tEmail         : " . $user_details['user_email'] );
				}
				if ( isset( $user_details['first_name'] ) ) {
					WP_CLI::log( "\tFirst Name    : " . $user_details['first_name'] );
				}
				if ( isset( $user_details['last_name'] ) ) {
					WP_CLI::log( "\tLast Name     : " . $user_details['last_name'] );
				}
			}

			$this->license_req();
			// WP_CLI::log( print_r( $wpmr->plugin_data, 1 ) );
			// WP_CLI::log( WP_CLI::colorize( '%n%wVersion              : %B' . $wpmr->plugin_data['Version'] ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wWebsite URL          : %B' . get_bloginfo( 'url' ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wWP URL               : %B' . get_bloginfo( 'wpurl' ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wWP Installation DIR  : %B' . ABSPATH ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wWP Version           : %B' . get_bloginfo( 'version' ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wWP Language          : %B' . get_bloginfo( 'language' ) ) );
			WP_CLI::log( WP_CLI::colorize( is_multisite() ? '%n%wMultisite            : %BYes' : '%n%wMultisite            : %BNo' ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wActive Theme         : %B' . get_bloginfo( 'stylesheet_directory' ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wParent Theme         : %B' . get_bloginfo( 'template_directory' ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wPHP                  : %B' . phpversion() ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wServer               : %B' . php_uname() ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wLast Updated         : %B' . $wpmr->get_last_updated_ago() ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wChecksum-Count       : %B' . count( $wpmr->get_all_checksums() ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wDefinition-Count     : %B' . $wpmr->get_definition_count() ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wDefinition-Version   : %B' . $wpmr->get_definition_version() ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wFiles                : %B' . count( $files ) ) );
			WP_CLI::log( WP_CLI::colorize( '%n%wUser Roles           :%B%n' ) );
			global $wp_roles;
			foreach ( $wp_roles->roles as $role => $capabilities ) {
				WP_CLI::log( "\t" . $role );
			}
			WP_CLI::log( WP_CLI::colorize( '%n%wMust-Use Plugins:%B' ) );
			$mu = get_mu_plugins();
			foreach ( $mu as $key => $value ) {
				WP_CLI::log( "\t" . $key );
			}
			WP_CLI::log( WP_CLI::colorize( '%n%wDrop-ins:%B' ) );
			$dropins = get_dropins();
			foreach ( $dropins as $key => $value ) {
				WP_CLI::log( "\t" . $key );
			}
			WP_CLI::log( WP_CLI::colorize( '%YHidden Files:%n' ) );
			$this->hidden();
		}

		private function get_time() {
			$time = time();
			$zone = function_exists( 'wp_timezone_string' ) ? wp_timezone_string() : $this->timezone_string_compat();
			$date = new DateTime( '@' . $time, new DateTimeZone( 'UTC' ) );
			$date->setTimezone( new DateTimeZone( $zone ) );
			return $date->format( 'c' );
		}

		private function timezone_string_compat() {
			$timezone_string = get_option( 'timezone_string' );
			if ( $timezone_string ) {
				return $timezone_string;
			}
			$offset    = (float) get_option( 'gmt_offset' );
			$hours     = (int) $offset;
			$minutes   = ( $offset - $hours );
			$sign      = ( $offset < 0 ) ? '-' : '+';
			$abs_hour  = abs( $hours );
			$abs_mins  = abs( $minutes * 60 );
			$tz_offset = sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins );
			return $tz_offset;
		}

		/**
		 * Show hidden-file coverage information for the current install.
		 *
		 * By default the CLI command prints a count and follow-up instruction.
		 * Internal callers can request the expanded list.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure hidden
		 *
		 * @param bool $count Whether to print only the count summary. Internal use.
		 * @return void
		 */
		function hidden( $count = true ) {
			$wpmr    = wp_malware_removal();
			$files   = $wpmr->return_all_files();
			$hidden  = array_filter(
				$files,
				function ( $v ) {
					return ( empty( explode( '.', basename( $v ) )[0] ) || empty( explode( '.', basename( dirname( $v ) ) )[0] ) ) ? true : false;
				}
			);
			$hidden  = array_values( $hidden );
			$newlist = array();
			foreach ( $hidden as $k => $v ) {
				$parts = explode( '.', basename( dirname( $v ) ) );
				if ( empty( $parts [0] ) ) {
					$newlist[ dirname( $v ) ] = "\t" . '[DIR] ' . dirname( $v );
				}
				$newlist[ $v ] = "\t" . '[FILE] ' . $v;
			}
			if ( $count ) {
				WP_CLI::log( count( $newlist ) . ' hidden files / folders. Run `wp malcure hidden` to get exact names.' );
			} else {
				WP_CLI::log( implode( "\n", $newlist ) . "\n" );
			}
		}

		/**
		 * Render the custom Malcure CLI help screen.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure help
		 *
		 * @return void
		 */
		function help() {
			WP_CLI::log( "\t" );
			WP_CLI::log( WP_CLI::colorize( '%N%W████████████████████████████' ) );
			WP_CLI::log( WP_CLI::colorize( '%N%W████    HELP & USAGE    ████' ) );
			WP_CLI::log( WP_CLI::colorize( '%N%W████████████████████████████' ) );
			// WP_CLI::log( WP_CLI::colorize( "%N%W╔═══════════════════════╗" ) );
			// WP_CLI::log( WP_CLI::colorize( "%N%W║════════ USAGE ════════║" ) );
			// WP_CLI::log( WP_CLI::colorize( "%N%W╚═══════════════════════╝" ) );
			WP_CLI::log( WP_CLI::colorize( "\t%Y" ) );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure help' ) );
			WP_CLI::log( WP_CLI::colorize( "\t%YThis help screen / information." ) );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure info%Y' ) );
			WP_CLI::log( WP_CLI::colorize( "\t%YDisplays WP info." ) );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure hidden%Y' ) );
			WP_CLI::log( WP_CLI::colorize( "\t%YList hidden files and directories." ) );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure register --mc-email=myemail@example.com --mc-fname="Firstname" --mc-lname="Lastname"' ) );
			WP_CLI::log( WP_CLI::colorize( "\t%YRegister (free) to get definition updates." ) );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure sync' ) );
			WP_CLI::log( WP_CLI::colorize( "\t%YUpdate definitions. Works only if you have enabled auto-update definitions in the web-UI; pass --mcforce=true to force sync." ) );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure refresh-checksums%Y' ) );
			WP_CLI::log( "\tForce a full official checksum refresh for core, plugins, and themes." );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure export-definitions <path>%Y' ) );
			WP_CLI::log( "\tExport the active definitions payload to JSON for reproducible troubleshooting." );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure import-definitions <path>%Y' ) );
			WP_CLI::log( "\tImport a definitions JSON payload into this environment without a live refresh." );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure status%Y' ) );
			WP_CLI::log( "\tDisplays license status." );
			WP_CLI::log( "\t" );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure activate licensekeyhere%Y' ) );
			WP_CLI::log( "\tActivates license key and sets up the install (including registration, definition update)." );
			WP_CLI::log( '' );
			WP_CLI::log( WP_CLI::colorize( '%rwp malcure deactivate%Y' ) );
			WP_CLI::log( "\tDeactivate license key." );
			WP_CLI::log( "\t" );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure extractlog --from=source_file --to=target_file%Y' ) );
			WP_CLI::log( "\tExtracts and saves JSON compliant output from log-file to target file." );
			WP_CLI::log( "\t" );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure show_file_type <file>%Y' ) );
			WP_CLI::log( "\tShow file information about the file." );
			WP_CLI::log( "\t" );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure reset%Y' ) );
			WP_CLI::log( "\tResets this plugin; pass --mcresetlogs to deletes scan-logs also." );
			WP_CLI::log( "\t" );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure scan%Y' ) );
			WP_CLI::log( "\tInitiate malware scan." );
			WP_CLI::log( "\t" );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure start-async-scan%Y' ) );
			WP_CLI::log( "\tStart the async scanner through the shared loopback orchestration path." );
			WP_CLI::log( '' );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure cancel-async-scan%Y' ) );
			WP_CLI::log( "\tRequest cancellation of the active async scan." );
			WP_CLI::log( '' );

			WP_CLI::log( WP_CLI::colorize( '%rwp malcure status-async-scan%Y' ) );
			WP_CLI::log( "\tShow async scan snapshot, the latest 10 dlog entries, and all issues; pass --format=json for machine-readable output." );
			WP_CLI::log( '' );

			WP_CLI::log( WP_CLI::colorize( "%N%W\tThe following options are supported with scan:" ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcbatchsize=10" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tSets number of files to scan per loop / iteration to 10. Default is 50." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcskipdb" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tSkip database scan." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcskipfiles" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tSkip file scan." ) );

			// Need option to skip scan for redirects

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcsuspicious" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tShow suspicious files. Default \"false\"" ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcskipdirs=\"themes,uploads\"" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tSkip themes and uploads directories. Expects directory name(s); comma-separated." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcregex=\"/find_.*_me/is\"" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tMatch custom regular expression (in addition to the existing malware definitions)." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcdbquery=\"%script%\" --mcdbregex=\"/href=\/malware/\"" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tScan database with custom query and regular expression (in addition to the existing malware definitions)." . '%n' ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--log=<path_to_log_file>" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tCreate a JSON scan-log and save it to file." ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tFor security reasons, .php will be added to the file extension." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcfiles=<absolute_path_to_file>" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tScan some specific file." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcscanonlydirs=<relative_path_to_the_specific_directory>" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tScan some specific directory." ) );

			WP_CLI::log( WP_CLI::colorize( "%B\t\t--mcdebug" ) );
			WP_CLI::log( WP_CLI::colorize( "%n%w\t\t\tShow debug output." ) );

			WP_CLI::runcommand( 'malcure' );
		}

		/**
		 * Reset Malcure settings for this install.
		 *
		 * ## OPTIONS
		 *
		 * [--mcforce]
		 * : Skip the confirmation prompt.
		 *
		 * [--mcresetlogs]
		 * : Delete stored scan logs as part of the reset.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure reset
		 *     wp malcure reset --mcforce --mcresetlogs
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function reset( $args = array(), $assoc_args = array() ) {
			$wpmr       = wp_malware_removal();
			$force      = $this->get_flag( $assoc_args, 'mcforce' );
			$reset_logs = $this->get_flag( $assoc_args, 'mcresetlogs' );
			if ( ! $force ) {
				WP_CLI::confirm( WP_CLI::colorize( "\n\n\t" . 'Resetting %W' . $wpmr->plugin_data['Name'] . '%n will require re-activation via license key like:' . "\n%8%9\t    wp malcure activate <your license key here>    \t%n\n\tYou'll find the license key in the email received at the time of purchase.\n" . "%1%_\t    Are you sure you want to reset " . $wpmr->plugin_data['Name'] . ' to factory defaults?    %N%n' ) );
			}
			WP_CLI::log( $wpmr->reset( $reset_logs ) );
			$this->aalert();
		}

		/**
		 * Show MIME encoding information for a file.
		 *
		 * ## OPTIONS
		 *
		 * [<file>]
		 * : File to inspect. Defaults to the current directory.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure show_file_type wp-config.php
		 *     wp malcure show_file_type wp-content/uploads/example.jpg
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function show_file_type( $args, $assoc_args ) {
			$file = ! empty( $args[0] ) ? $args[0] : '.';
			if ( function_exists( 'exec' ) && is_readable( $file ) ) {
				// exec(string $command, array &$output = null, int &$result_code = null): string|false
				$out = exec( 'file -b --mime-encoding ' . escapeshellarg( $file ), $output, $return );
				WP_CLI::line( $out . ' ' . $this->normalise_path( $file ) . ' ' . ( ! empty( $return ) ? $return : '' ) );
			} else {
				WP_CLI::line( 'Could not execute or access ' . $this->normalise_path( $file ) );
			}
		}

		/**
		 * Activate the saved Malcure license for this install.
		 *
		 * Saves the license key, persists the verified license state, registers the
		 * install when needed, and forces a silent definitions sync.
		 *
		 * ## OPTIONS
		 *
		 * [<license-key>]
		 * : License key to activate. Omit to be prompted on STDIN.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure activate YOUR-LICENSE-KEY
		 *     wp malcure activate
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function activate( $args, $assoc_args ) {
			$action = 'activate_license';
			if ( empty( $args[0] ) ) {
				WP_CLI::log( 'Enter your license key and hit ENTER...' );
				$key = sanitize_text_field( trim( fgets( STDIN ) ) );
				$this->license_req( $action, $key );
			} else {
				$this->license_req( $action, $args[0] );
			}
		}

		/**
		 * Deactivate the saved Malcure license for this install.
		 *
		 * ## OPTIONS
		 *
		 * [<license-key>]
		 * : License key to deactivate. Omit to use the saved license key.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure deactivate
		 *     wp malcure deactivate YOUR-LICENSE-KEY
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function deactivate( $args, $assoc_args ) {
			$action = 'deactivate_license';
			if ( empty( $args[0] ) ) {
				$this->license_req( $action, false );
			} else {
				$this->license_req( $action, $args[0] );
			}
		}

		/**
		 * Show current license status for this install.
		 *
		 * ## OPTIONS
		 *
		 * [<license-key>]
		 * : License key to check. Omit to use the saved license key.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure status
		 *     wp malcure status YOUR-LICENSE-KEY
		 *
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function status( $args, $assoc_args ) {
			$action = 'check_license';
			if ( empty( $args[0] ) ) {
				$this->license_req( $action, false );
			} else {
				$this->license_req( $action, $args[0] );
			}
		}

		/**
		 * Start the stateful scanner via the shared loopback orchestration path.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure start-async-scan
		 *
		 * @subcommand start-async-scan
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function start_async_scan( $args = array(), $assoc_args = array() ) {
			$wpmr   = wp_malware_removal();
			$before = $this->get_fresh_stateful_scan_snapshot( $wpmr );

			if ( ! empty( $before['running'] ) ) {
				$this->render_stateful_scan_snapshot( $before );
				if ( ! empty( $before['kill'] ) ) {
					WP_CLI::error( 'Async scan cancel is already pending. Wait for the current cycle to settle before starting a new scan.' );
				}

				WP_CLI::error( 'Async scan already running.' );
			}

			$response = $wpmr->request_stateful_scan_start(
				array(
					'timeout'  => 5,
					'blocking' => true,
					'body'     => array( 'wpmr_scan_source' => 'cli' ),
				)
			);

			if ( is_wp_error( $response ) ) {
				WP_CLI::error( $response->get_error_message() );
			}

			$launch = $this->wait_for_stateful_scan_launch( $wpmr, $before );

			if ( ! empty( $launch['verified'] ) ) {
				WP_CLI::success( 'Async scan started.' );
			} else {
				WP_CLI::warning( 'Async scan launch request was dispatched, but start markers did not update within 3 seconds.' );
			}

			$this->render_stateful_scan_snapshot( $launch['status'] );
		}

		/**
		 * Request cancellation of the active stateful scan.
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure cancel-async-scan
		 *
		 * @subcommand cancel-async-scan
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function cancel_async_scan( $args = array(), $assoc_args = array() ) {
			$wpmr   = wp_malware_removal();
			$result = $wpmr->request_stateful_scan_cancel();

			if ( ! empty( $result['cancelled'] ) ) {
				WP_CLI::success( $result['message'] );
			} else {
				WP_CLI::log( $result['message'] );
			}

			$this->render_stateful_scan_snapshot( $result['status'] );
		}

		/**
		 * Show the current async scan snapshot, dlog, and issues.
		 *
		 * ## OPTIONS
		 *
		 * [--format=<format>]
		 * : Output format.
		 * ---
		 * default: text
		 * options:
		 *   - text
		 *   - json
		 * ---
		 *
		 * ## EXAMPLES
		 *
		 *     wp malcure status-async-scan
		 *     wp malcure status-async-scan --format=json
		 *
		 * @subcommand status-async-scan
		 * @param array $args Positional CLI arguments.
		 * @param array $assoc_args Associative CLI arguments.
		 * @return void
		 */
		function status_async_scan( $args = array(), $assoc_args = array() ) {
			$wpmr   = wp_malware_removal();
			$format = empty( $assoc_args['format'] ) ? 'text' : strtolower( sanitize_key( $assoc_args['format'] ) );

			if ( ! in_array( $format, array( 'text', 'json' ), true ) ) {
				WP_CLI::error( 'Unsupported format. Use --format=text or --format=json.' );
			}

			$payload                     = $this->get_fresh_stateful_scan_status_payload(
				$wpmr,
				array(
					'include_internal'  => true,
					'include_issues'    => true,
					'include_dlog'      => true,
					'dlog_offset'       => null,
					'dlog_limit'        => 10,
					'include_whitelist' => false,
				)
			);
			$payload['state']            = $this->derive_async_scan_state( $payload );
			$payload['progress_summary'] = $this->summarize_stateful_scan_progress( empty( $payload['status'] ) ? '' : $payload['status'] );
			$payload['issue_count']      = empty( $payload['issues'] ) ? 0 : count( $payload['issues'] );
			$payload['dlog_count']       = empty( $payload['dlog']['logs'] ) ? 0 : count( $payload['dlog']['logs'] );

			if ( 'json' === $format ) {
				WP_CLI::log( wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
				return;
			}

			WP_CLI::log( 'Async scan status: ' . $payload['state'] );
			$this->render_stateful_scan_snapshot( $payload );
			$this->render_async_scan_dlog( empty( $payload['dlog'] ) ? array() : $payload['dlog'] );
			$this->render_async_scan_issues( empty( $payload['issues'] ) ? array() : $payload['issues'] );
		}

		private function license_req( $action = 'check_license', $key = false ) {
			$wpmr = wp_malware_removal();
			if ( $action == 'check_license' || $action == 'deactivate_license' ) {
				$key = $wpmr->get_setting( 'license_key' );
			}
			if ( empty( $key ) ) {
				if ( $action == 'check_license' ) {
					WP_CLI::error( 'Not activated. No saved license key.' );
				}
				WP_CLI::error( 'Please run with your license key like: ' . WP_CLI::colorize( '%Rwp malcure [activate|deactivate|status] <license key>%n' . "\n%Reg: %Ywp malcure activate yourlicensekey" . '%n' ) );
			}
			$key         = trim( $key );
			$url         = MALCURE_API . '?edd_action=' . $action . '&item_id=1725&license=' . $key . '&url=' . site_url();
			$response    = wp_safe_remote_request( $url, array( 'timeout' => $wpmr->timeout ) );
			$headers     = wp_remote_retrieve_headers( $response );
			$status_code = wp_remote_retrieve_response_code( $response );
			if ( 200 != $status_code ) {
				WP_CLI::error( 'Error ' . $status_code . ' fetching Update.' );
			}
			if ( is_wp_error( $response ) ) {
				WP_CLI::error( $response->get_error_message() );
			}
			$body   = wp_remote_retrieve_body( $response );
			$status = json_decode( $body, true );

			if ( is_null( $status ) ) {
				WP_CLI::error( 'Unparsable response data.' );
			}
			if ( $status['success'] != true ) {
				WP_CLI::error( sanitize_text_field( $status['license'] ) );
			}
			if ( ! empty( $status['success'] ) && $status['success'] == true ) {

				if ( $action == 'deactivate_license' ) {
					$wpmr->delete_setting( 'license_key' );
					$wpmr->clear_license_status();
					$name = $status['customer_name'];
					$name = array_filter( explode( ' ', $name ) );
					$fn   = empty( $name ) ? explode( '@', $email )[0] : array_shift( $name );
					WP_CLI::success( 'Deactivated!' );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '********************************************************************************%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G\t" . 'You are still a proud owner of the Malcure Advanced Edition. %N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G\t" . 'Thank you ' . $fn . '! Catch ya on some other WP install.%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '********************************************************************************%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N%n' ) );
				}

				if ( $action == 'activate_license' ) {
					WP_CLI::success( 'Activated! We are setting up everything...' );
					$wpmr->update_setting( 'license_key', $key );
					$wpmr->save_license_status( $status );
					$email = $status['customer_email'];
					$wpmr  = wp_malware_removal();
					$name  = $status['customer_name'];
					$name  = array_filter( explode( ' ', $name ) );
					$fn    = empty( $name ) ? explode( '@', $email )[0] : array_shift( $name );
					$ln    = empty( $name ) ? explode( '@', $email )[0] : array_shift( $name );
					if ( ! $wpmr->is_registered() ) {
						$wpmr->wpmr_cli_register( $email, $fn, $ln, true );
					}
					// Licensee notifications default ON — they paid, it should work out of the box.
					$wpmr->update_setting( 'wpmr_scan_notify_licensee', true );
					$wpmr->update_setting( 'wpmr_pulse_notify_licensee', true );
					// Anchor the 7-day pulse clock at activation time.
					$wpmr->update_setting( 'wpmr_last_pulse_sent', time() );
					$this->sync(
						array(),
						array(
							'mcforce' => 1,
							'silent'  => 1,
						)
					);
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '********************************************************************************%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G\t" . 'voilà! You are a proud owner of the professional plan. %N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G\t" . 'Thank you ' . $fn . '! We\'ve set up everything for you!%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '********************************************************************************%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N%n' ) );
					// $this->help();
					WP_CLI::log( WP_CLI::colorize( "\t%G" . '%N' ) );
					WP_CLI::log( WP_CLI::colorize( "\t%Y" . 'Run \'wp malcure help\' for documentation. %N' ) );
				}

				if ( $action == 'check_license' ) {
					WP_CLI::log( WP_CLI::colorize( '%n%wLicense Info:%n' ) );
					foreach ( $status as $key => $value ) {
						if (
							in_array(
								$key,
								array( 'license', 'customer_email', 'site_count', 'activations_left', 'license_limit', 'expires' )
							) ) {
							$formatted_key   = ucwords( str_replace( '_', ' ', $key ) );
							$formatted_value = $value;
							// Format specific values for better readability
							if ( $key === 'license' ) {
								$formatted_key   = 'License';
								$formatted_value = ucfirst( $value );
							} elseif ( $key === 'expires' ) {
								$formatted_key = 'Expires';
							} elseif ( $key === 'customer_email' ) {
								$formatted_key = 'Customer Email';
							} elseif ( $key === 'license_limit' ) {
								$formatted_key   = 'License Limit';
								$formatted_value = ( $value == 0 ) ? 'unlimited' : $value;
							} elseif ( $key === 'site_count' ) {
								$formatted_key = 'Site Count';
							} elseif ( $key === 'activations_left' ) {
								$formatted_key   = 'Activations Left';
								$formatted_value = ( $value == 999999 || $value == 0 ) ? 'unlimited' : $value;
							}

							WP_CLI::log( "\t" . str_pad( $formatted_key, 18, ' ', STR_PAD_RIGHT ) . ': ' . $formatted_value );
						}
					}
				}
			}
		}

		private function brand_color() {
			return '%B';
		}

		private function heading_format() {
			return '%W%9';
		}

		/**
		 * Check whether a PHP runtime-tuning function is callable in CLI.
		 *
		 * @param string $function_name Function name to test.
		 * @return bool True when the function exists and is not disabled.
		 */
		private function can_adjust_cli_runtime( $function_name ) {
			if ( ! function_exists( $function_name ) ) {
				return false;
			}

			$disabled_functions = @ini_get( 'disable_functions' );
			if ( false === $disabled_functions || '' === $disabled_functions ) {
				return true;
			}

			$disabled_functions = array_filter( array_map( 'trim', explode( ',', strtolower( (string) $disabled_functions ) ) ) );

			return ! in_array( strtolower( $function_name ), $disabled_functions, true );
		}

		/**
		 * Extend the CLI execution time limit when the host allows it.
		 *
		 * @param int $seconds Target execution time limit in seconds.
		 * @return bool True when the limit change was accepted.
		 */
		private function extend_cli_time_limit( $seconds ) {
			if ( ! $this->can_adjust_cli_runtime( 'set_time_limit' ) ) {
				return false;
			}

			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- WP-CLI scan batches need an unlimited time budget on hosts that permit it.
			return @set_time_limit( (int) $seconds );
		}

		private function cli_value_to_string( $value ) {
			if ( is_string( $value ) ) {
				return $value;
			}

			if ( is_bool( $value ) ) {
				return $value ? 'true' : 'false';
			}

			if ( null === $value ) {
				return 'null';
			}

			if ( is_scalar( $value ) ) {
				return (string) $value;
			}

			if ( is_wp_error( $value ) ) {
				$value = array(
					'errors'     => $value->errors,
					'error_data' => $value->error_data,
				);
			}

			$this->extend_cli_time_limit( 0 );

			$encoded = wp_json_encode(
				$value,
				JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR
			);

			if ( false !== $encoded ) {
				return $encoded;
			}

			if ( is_object( $value ) ) {
				return '[object ' . get_class( $value ) . ']';
			}

			return '[' . gettype( $value ) . ']';
		}

		private function llog( $str ) {
			WP_CLI::line( $this->cli_value_to_string( $str ) );
		}

		private function get_severity_format( $status ) {
			$status = strtoupper( $status );
			switch ( $status ) {
				case 'CLEAN':
					return '%g'; // green
				case 'SKIPPED':
					return '%b'; // blue
				case 'SUSPICIOUS':
					return '%y'; // yellow
				case 'HIGH':
					return '%m'; // magenta
				case 'SEVERE':
					return '%r'; // red
				default:
					return '%w'; // grey
			}
		}

		private function mc_get_bool( $var ) {
			return filter_var( $var, FILTER_VALIDATE_BOOLEAN );
		}

		private function memory_human( $size ) {
			$unit = array( 'B', 'KB', 'MB', 'GB', 'TB', 'PB' );
			return @round( $size / pow( 1024, ( $i = floor( log( $size, 1024 ) ) ) ), 2 ) . ' ' . $unit[ $i ];
		}

		private function log( $message, $append = true ) {
			if ( ! $this->logging ) {
				return;
			}

			$logfile = $this->logfile;
			$flags   = LOCK_EX;
			if ( $append ) {
				// $flags = FILE_APPEND | $flags;
				$contents = $this->get_log( $this->logfile );
				if ( ! empty( $contents ) ) {
					$contents = json_decode( $contents, 1 );
					if ( is_null( $contents ) ) {
						WP_CLI::warning( 'Could not decode log contents!' );
						WP_CLI::warning( 'Malcure logging may be corrupt.' );
						$contents = array();
					}
				} else {
					$contents = array();
				}
				$message = array_merge( $contents, $message );

				$message = $this->logfile_header . json_encode( $message, JSON_PRETTY_PRINT );
			}

			file_put_contents( $logfile, $message, $flags );
		}

		private function get_log( $file ) {
			$contents = file_get_contents( $file );
			if ( empty( $contents ) ) {
				return '';
			}
			$replace  = '/.*' . preg_quote( $this->log_marker, '/' ) . '[\n\r]*' . '/s';
			$contents = preg_replace( $replace, '', $contents );
			return $contents;
		}

		function extractlog( $args = array(), $assoc_args = array() ) {
			if ( empty( $assoc_args['from'] ) ) {
				WP_CLI::error( 'No "from" argument passed. Pass the path of the logfile like wp malcure putlog --from=log.json.php' );
			}
			if ( empty( $assoc_args['to'] ) ) {
				WP_CLI::error( 'No "to" argument passed. Pass the path of the logfile like wp malcure putlog --to=log.json' );
			}
			$from     = trailingslashit( $this->normalise_path( pathinfo( $assoc_args['from'] )['dirname'] ) ) . pathinfo( $assoc_args['from'] )['basename'];
			$to       = trailingslashit( $this->normalise_path( pathinfo( $assoc_args['to'] )['dirname'] ) ) . pathinfo( $assoc_args['to'] )['basename'];
			$contents = file_get_contents( $from );
			if ( empty( $contents ) ) {
				WP_CLI::error( 'Malcure Scan Log file ' . $to . ' is empty.' );
			}
			$replace  = '/.*' . preg_quote( $this->log_marker, '/' ) . '[\n\r]*' . '/s';
			$contents = preg_replace( $replace, '', $contents );
			if ( file_put_contents( $to, $contents ) ) {
				WP_CLI::success( "Malcure Scan Log Successfully Extracted JSON format\n\tFrom:\t$from\n\tTo\t$to" );
			} else {
				WP_CLI::error( 'Could not write to file ' . $to );
			}
		}

		private function get_stored_definitions_payload() {
			$wpmr       = wp_malware_removal();
			$signatures = $wpmr->get_setting( 'signatures' );

			if ( empty( $signatures ) || ! is_array( $signatures ) ) {
				$signatures = $wpmr->maybe_load_default_definitions();
			}

			return is_array( $signatures ) ? $signatures : array();
		}

		private function resolve_definitions_json_path( $input_path, $must_exist = false ) {
			$input_path = trim( $input_path );

			if ( '' === $input_path ) {
				WP_CLI::error( 'Definitions JSON path cannot be empty.' );
			}

			if ( '-' === $input_path ) {
				return '-';
			}

			$resolved = $input_path;

			if ( ! preg_match( '#^([a-zA-Z]:)?/#', $resolved ) ) {
				$resolved = rtrim( (string) getcwd(), DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR . $resolved;
			}

			$real = realpath( $resolved );
			if ( false !== $real ) {
				$resolved = $real;
			}

			$resolved = $this->normalise_path( $resolved );

			if ( $must_exist ) {
				if ( ! file_exists( $resolved ) ) {
					WP_CLI::error( 'Definitions JSON file does not exist: ' . $resolved );
				}

				if ( ! is_file( $resolved ) ) {
					WP_CLI::error( 'Definitions JSON path is not a file: ' . $resolved );
				}

				if ( ! is_readable( $resolved ) ) {
					WP_CLI::error( 'Definitions JSON file is not readable: ' . $resolved );
				}
			}

			return $resolved;
		}

		private function read_definitions_json_source( $source ) {
			if ( '-' === $source ) {
				$contents = stream_get_contents( STDIN );
				$label    = 'STDIN';
			} else {
				$contents = file_get_contents( $source );
				$label    = $source;
			}

			if ( false === $contents || '' === trim( $contents ) ) {
				WP_CLI::error( 'Definitions JSON source is empty or unreadable: ' . $label );
			}

			$decoded = json_decode( $contents, true );

			if ( JSON_ERROR_NONE !== json_last_error() ) {
				WP_CLI::error( 'Could not decode definitions JSON from ' . $label . ': ' . json_last_error_msg() );
			}

			if ( ! is_array( $decoded ) ) {
				WP_CLI::error( 'Definitions JSON must decode to an object.' );
			}

			return $decoded;
		}

		/**
		 * Normalize supported definitions import envelopes to the raw definitions payload.
		 *
		 * Supports these shapes:
		 * - raw definitions payload (`definitions` + `v`)
		 * - CLI export wrapper (`signatures`)
		 * - legacy wrapper (`success -> data`)
		 * - SaaS wrapper (`success -> data -> payload -> signatures`)
		 *
		 * @param array $payload Decoded JSON payload.
		 * @return array{definitions_data:array,import_sig_time:int}
		 */
		private function normalize_definitions_import_payload( $payload ) {
			$import_sig_time = time();
			$candidates      = array(
				array(
					'definitions_data' => $payload,
					'import_sig_time'  => $import_sig_time,
				),
			);

			if ( isset( $payload['signatures'] ) && is_array( $payload['signatures'] ) ) {
				if ( array_key_exists( 'sig_time', $payload ) && is_numeric( $payload['sig_time'] ) ) {
					$import_sig_time = absint( $payload['sig_time'] );
				}

				if ( ! empty( $payload['definition_version'] ) && ! empty( $payload['signatures']['v'] ) && (string) $payload['definition_version'] !== (string) $payload['signatures']['v'] ) {
					WP_CLI::error( 'Definitions JSON metadata does not match the embedded definitions version.' );
				}

				$candidates[] = array(
					'definitions_data' => $payload['signatures'],
					'import_sig_time'  => $import_sig_time,
				);
			}

			if ( isset( $payload['data'] ) && is_array( $payload['data'] ) ) {
				$candidates[] = array(
					'definitions_data' => $payload['data'],
					'import_sig_time'  => $import_sig_time,
				);

				if ( isset( $payload['data']['payload']['signatures'] ) && is_array( $payload['data']['payload']['signatures'] ) ) {
					$candidates[] = array(
						'definitions_data' => $payload['data']['payload']['signatures'],
						'import_sig_time'  => $import_sig_time,
					);
				}
			}

			foreach ( $candidates as $candidate ) {
				if ( '' === $this->validate_definitions_payload( $candidate['definitions_data'] ) ) {
					return $candidate;
				}
			}

			return $candidates[0];
		}

		private function validate_definitions_payload( $definitions_data ) {
			if ( ! is_array( $definitions_data ) ) {
				return 'payload must decode to an object.';
			}

			if ( empty( $definitions_data['v'] ) || ! is_scalar( $definitions_data['v'] ) ) {
				return 'missing top-level definitions version `v`.';
			}

			if ( empty( $definitions_data['definitions'] ) || ! is_array( $definitions_data['definitions'] ) ) {
				return 'missing top-level `definitions` map.';
			}

			if ( ! array_key_exists( 'files', $definitions_data['definitions'] ) || ! is_array( $definitions_data['definitions']['files'] ) ) {
				return 'missing `definitions.files` map.';
			}

			if ( ! array_key_exists( 'db', $definitions_data['definitions'] ) || ! is_array( $definitions_data['definitions']['db'] ) ) {
				return 'missing `definitions.db` map.';
			}

			return '';
		}

		private function format_utc_timestamp( $timestamp ) {
			$timestamp = absint( $timestamp );

			if ( ! $timestamp ) {
				return '';
			}

			return gmdate( 'c', $timestamp );
		}

		private function format_stateful_scan_timestamp( $timestamp, $empty = 'not set' ) {
			if ( false === $timestamp || null === $timestamp || '' === $timestamp ) {
				return $empty;
			}

			if ( is_numeric( $timestamp ) ) {
				$unix_timestamp = (int) floor( (float) $timestamp );

				if ( $unix_timestamp > 0 ) {
					return gmdate( 'c', $unix_timestamp ) . ' (' . $timestamp . ' UTC)';
				}
			}

			return (string) $timestamp;
		}

		private function summarize_stateful_scan_progress( $status ) {
			if ( empty( $status ) ) {
				return 'not set';
			}

			if ( is_array( $status ) ) {
				if ( ! empty( $status['message'] ) && is_scalar( $status['message'] ) ) {
					return (string) $status['message'];
				}

				if ( ! empty( $status['phase'] ) && is_scalar( $status['phase'] ) ) {
					return (string) $status['phase'];
				}

				return wp_json_encode( $status );
			}

			return (string) $status;
		}

		private function render_stateful_scan_snapshot( $snapshot ) {
			$lines = array(
				'Running'           => ! empty( $snapshot['running'] ) ? 'yes' : 'no',
				'Cancel pending'    => ! empty( $snapshot['kill'] ) ? 'yes' : 'no',
				'Scan identifier'   => empty( $snapshot['scan_handshake_key'] ) ? 'not set' : (string) $snapshot['scan_handshake_key'],
				'Progress'          => $this->summarize_stateful_scan_progress( empty( $snapshot['status'] ) ? '' : $snapshot['status'] ),
				'Initiated'         => $this->format_stateful_scan_timestamp( empty( $snapshot['scan_initiated'] ) ? '' : $snapshot['scan_initiated'] ),
				'Completed'         => $this->format_stateful_scan_timestamp( empty( $snapshot['scan_completed'] ) ? '' : $snapshot['scan_completed'] ),
				'Terminated'        => $this->format_stateful_scan_timestamp( empty( $snapshot['scan_terminated'] ) ? '' : $snapshot['scan_terminated'] ),
				'Next watchdog run' => $this->format_stateful_scan_timestamp( isset( $snapshot['monitor_next'] ) ? $snapshot['monitor_next'] : '', 'not scheduled' ),
				'Recovery attempts' => isset( $snapshot['recovery_attempts'] ) && null !== $snapshot['recovery_attempts'] && '' !== (string) $snapshot['recovery_attempts'] ? (string) $snapshot['recovery_attempts'] : 'n/a',
			);

			WP_CLI::log( 'Async scan snapshot:' );

			foreach ( $lines as $label => $value ) {
				WP_CLI::log( sprintf( '  %-18s %s', $label . ':', $value ) );
			}
		}

		private function normalize_async_scan_cli_text( $text ) {
			if ( ! is_scalar( $text ) ) {
				return '';
			}

			$text = html_entity_decode( wp_strip_all_tags( (string) $text ), ENT_QUOTES, 'UTF-8' );
			$text = preg_replace( '/\s+/', ' ', $text );

			return trim( (string) $text );
		}

		/**
		 * Format a normalized async issue pointer for text-mode CLI output.
		 *
		 * File pointers stay as plain paths. Structured database pointers preserve
		 * their operator-usable table and row identifier as `table#id`.
		 *
		 * @param mixed $pointer Normalized issue pointer from the shared status payload.
		 * @return string Human-readable pointer text.
		 */
		private function format_async_scan_issue_pointer_text( $pointer ) {
			if ( is_array( $pointer ) ) {
				$table = isset( $pointer['table'] ) ? $this->normalize_async_scan_cli_text( $pointer['table'] ) : '';
				$id    = isset( $pointer['id'] ) && is_scalar( $pointer['id'] ) ? absint( $pointer['id'] ) : 0;

				if ( '' !== $table && $id > 0 ) {
					return $table . '#' . $id;
				}

				if ( '' !== $table ) {
					return $table;
				}

				if ( $id > 0 ) {
					return '#' . $id;
				}
			}

			$pointer = $this->normalize_async_scan_cli_text( $pointer );

			return '' === $pointer ? 'pointer unavailable' : $pointer;
		}

		private function summarize_async_scan_log_comment( $comment ) {
			if ( empty( $comment ) || ! is_scalar( $comment ) ) {
				return '';
			}

			$decoded = json_decode( (string) $comment, true );
			if ( is_array( $decoded ) ) {
				$parts = array();
				if ( ! empty( $decoded['time'] ) ) {
					$parts[] = $this->normalize_async_scan_cli_text( $decoded['time'] );
				}
				if ( ! empty( $decoded['comment'] ) ) {
					$parts[] = $this->normalize_async_scan_cli_text( $decoded['comment'] );
				}

				return implode( ' | ', array_filter( $parts ) );
			}

			return $this->normalize_async_scan_cli_text( $comment );
		}

		private function render_async_scan_dlog( $dlog ) {
			$logs        = empty( $dlog['logs'] ) || ! is_array( $dlog['logs'] ) ? array() : $dlog['logs'];
			$total_count = isset( $dlog['total_count'] ) ? intval( $dlog['total_count'] ) : count( $logs );
			$shown_count = isset( $dlog['shown_count'] ) ? intval( $dlog['shown_count'] ) : count( $logs );

			if ( $total_count > $shown_count ) {
				WP_CLI::log( 'Async dlog entries: showing latest ' . $shown_count . ' of ' . $total_count );
			} else {
				WP_CLI::log( 'Async dlog entries: ' . $total_count );
			}

			if ( empty( $logs ) ) {
				WP_CLI::log( '  No dlog entries.' );
				return;
			}

			foreach ( $logs as $log ) {
				$message = $this->normalize_async_scan_cli_text( empty( $log['msg'] ) ? '' : $log['msg'] );
				$comment = $this->summarize_async_scan_log_comment( empty( $log['comment'] ) ? '' : $log['comment'] );
				$line    = sprintf(
					'  [%s] #%d %s',
					strtoupper( empty( $log['severity'] ) ? 'i' : (string) $log['severity'] ),
					intval( empty( $log['log_id'] ) ? 0 : $log['log_id'] ),
					empty( $message ) ? '(empty message)' : $message
				);

				if ( '' !== $comment ) {
					$line .= ' | ' . $comment;
				}

				WP_CLI::log( $line );
			}
		}

		private function render_async_scan_issues( $issues ) {
			$issues = is_array( $issues ) ? $issues : array();
			WP_CLI::log( 'Issues: ' . count( $issues ) );

			if ( empty( $issues ) ) {
				WP_CLI::log( '  No issues found.' );
				return;
			}

			foreach ( $issues as $issue ) {
				$severity     = strtoupper( empty( $issue['severity'] ) ? 'unknown' : (string) $issue['severity'] );
				$type         = empty( $issue['type'] ) ? 'issue' : $this->normalize_async_scan_cli_text( $issue['type'] );
				$pointer      = $this->format_async_scan_issue_pointer_text( isset( $issue['pointer'] ) ? $issue['pointer'] : '' );
				$infection_id = empty( $issue['infection_id'] ) ? '' : ' (infection ' . $this->normalize_async_scan_cli_text( $issue['infection_id'] ) . ')';

				WP_CLI::log( sprintf( '  [%s] %s %s%s', $severity, $type, $pointer, $infection_id ) );
			}
		}

		private function derive_async_scan_state( $snapshot ) {
			if ( ! empty( $snapshot['running'] ) ) {
				return ! empty( $snapshot['kill'] ) ? 'cancelling' : 'running';
			}

			if ( ! empty( $snapshot['scan_terminated'] ) ) {
				return 'cancelled';
			}

			if ( ! empty( $snapshot['scan_completed'] ) ) {
				return 'completed';
			}

			return 'idle';
		}

		private function get_fresh_stateful_scan_status_payload( $wpmr, $payload_args = array() ) {
			wp_cache_delete( 'alloptions', 'options' );
			wp_cache_delete( WPMRNS . 'S_', 'options' );
			wp_cache_delete( WPMRNS . 'S_scanner_progress', 'options' );

			return $wpmr->get_stateful_scan_status_payload( $payload_args );
		}

		private function get_fresh_stateful_scan_snapshot( $wpmr ) {
			return $this->get_fresh_stateful_scan_status_payload(
				$wpmr,
				array(
					'include_internal'  => true,
					'include_issues'    => false,
					'include_dlog'      => false,
					'include_whitelist' => false,
				)
			);
		}

		private function is_stateful_scan_launch_observed( $before_snapshot, $after_snapshot ) {
			if ( ! empty( $after_snapshot['running'] ) ) {
				return true;
			}

			if ( ! empty( $after_snapshot['scan_handshake_key'] ) && $after_snapshot['scan_handshake_key'] !== $before_snapshot['scan_handshake_key'] ) {
				return true;
			}

			if ( ! empty( $after_snapshot['scan_initiated'] ) && (string) $after_snapshot['scan_initiated'] !== (string) $before_snapshot['scan_initiated'] ) {
				return true;
			}

			return false;
		}

		private function wait_for_stateful_scan_launch( $wpmr, $before_snapshot, $timeout_seconds = 3 ) {
			$deadline = microtime( true ) + max( 0.2, (float) $timeout_seconds );
			$status   = $this->get_fresh_stateful_scan_snapshot( $wpmr );

			do {
				if ( $this->is_stateful_scan_launch_observed( $before_snapshot, $status ) ) {
					return array(
						'verified' => true,
						'status'   => $status,
					);
				}

				usleep( 200000 );
				$status = $this->get_fresh_stateful_scan_snapshot( $wpmr );
			} while ( microtime( true ) < $deadline );

			return array(
				'verified' => false,
				'status'   => $status,
			);
		}

		private function prerequisites() {
			$this->logfile_header = '<?php' . PHP_EOL . 'error_log( \'⥳⥳⥳⥳ MALCURE SCAN LOG ⥴⥴⥴⥴\' );' . PHP_EOL . 'echo \'<!-- M -->\';' . PHP_EOL . 'exit;' . PHP_EOL . '// This file is intentionally output as .php for protection from prying eyes && security reasons.' . PHP_EOL . '// You can programatically handle the contents in supported format following the following marker:' . PHP_EOL . $this->log_marker . PHP_EOL;
			$wpmr                 = wp_malware_removal();
			$home                 = set_url_scheme( get_option( 'home' ), 'http' );
			$siteurl              = set_url_scheme( get_option( 'siteurl' ), 'http' );
			if ( ! empty( $home ) && 0 !== strcasecmp( $home, $siteurl ) ) {
				$this->wp_custom_dir = true;
				$this->wp_home_dir   = trailingslashit( $this->normalise_path( ABSPATH ) );
			}
		}

		private function test_index_php( $path = '' ) {
			$wpmr = wp_malware_removal();
			if ( ! $path ) {
				$path = $wpmr->get_home_dir();
			}
			$parent = dirname( $path );
			$this->llog( PHP_EOL );
			$this->llog( 'path :' . $path );
			$this->llog( 'parent :' . $parent );
			$index_path = '';
			$this->llog( 'Searching for index.php in :' . $parent );
			if ( is_readable( trailingslashit( $parent ) . 'index.php' ) ) {
				$index_path = trailingslashit( $parent ) . 'index.php';
			} else {
				$this->llog( 'cannot locate index.php in :' . $parent );
				return;
			}

			$index_code = file_get_contents( $index_path );

			$regex = '/^\s*\brequire\b.*?[\'\"]\/?(' . basename( $path ) . ')\/wp\-blog\-header\.php[\'\"]/m';
			$this->llog( 'Regex: ' . $regex );

			if ( preg_match( $regex, $index_code, $matches ) ) {
				$path = trailingslashit( $this->normalise_path( $parent ) );
				$this->llog( $matches );
				$this->llog( $path );
				return $path;
			} else {
				return $this->test_index_php( $parent );
			}
		}

		private function normalise_path( $path ) {
			$realpath = wp_normalize_path( realpath( $path ) );
			if ( $realpath ) {
				return $realpath;
			}
			return wp_normalize_path( $path );
		}

		private function get_flag( $a = array(), $k = false ) {
			if ( is_array( $a ) && array_key_exists( $k, $a ) ) {
				return true;
			}
			return false;
		}

		private function aalert() {
			fprintf( STDOUT, '%s', "\x07" );
		}

		function saas_test_api() {
			$wpmr   = wp_malware_removal();
			$result = $wpmr->saas_request( 'saas_test_api', array( 'log' => true ) );

			WP_CLI::log( 'Check output in ' . trailingslashit( $wpmr->dir ) . 'log.log.' );
		}

		function debug( $args = array(), $assoc_args = array() ) {
			$wpmr = wp_malware_removal();
		}
	}

	WP_CLI::add_command( 'malcure', 'Malcure_Advanced' );
}
