<?php
/**
 * Human-readable finding explanations.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Admin;

/**
 * Maps scanner-owned evidence keys to bounded, translated guidance.
 */
final class FindingExplanationCatalog {
	const MAX_EVIDENCE_ITEMS = 20;

	/**
	 * Explain one scanner evidence key.
	 *
	 * @param string $key Evidence message key.
	 * @return array{title:string,detected:string,importance:string,steps:string[],context:string,known:bool}
	 */
	public function explain( string $key ): array {
		$catalog = $this->catalog();
		if ( isset( $catalog[ $key ] ) ) {
			return $catalog[ $key ];
		}

		return $this->entry(
			__( 'Suspicious local evidence', 'localguard-scanner' ),
			__( 'LocalGuard recorded a signal that this version cannot explain in more detail.', 'localguard-scanner' ),
			__( 'Unknown evidence should be reviewed, but it does not confirm that the site is infected.', 'localguard-scanner' ),
			array(
				__( 'Run a new scan with the latest LocalGuard version.', 'localguard-scanner' ),
				__( 'Review the relative location and compare it with a known-clean source.', 'localguard-scanner' ),
			),
			__( 'No inspected content is shown or included in this explanation.', 'localguard-scanner' ),
			false
		);
	}

	/**
	 * Explain and deduplicate a bounded evidence list.
	 *
	 * @param mixed $evidence Decoded evidence rows.
	 * @return array<int,array{title:string,detected:string,importance:string,steps:string[],context:string,known:bool}>
	 */
	public function explain_many( $evidence ): array {
		if ( ! is_array( $evidence ) ) {
			return array( $this->explain( '' ) );
		}

		$results = array();
		$seen    = array();
		foreach ( array_slice( $evidence, 0, self::MAX_EVIDENCE_ITEMS ) as $item ) {
			$key = isset( $item['message_key'] ) && is_string( $item['message_key'] ) && strlen( $item['message_key'] ) <= 128 ? $item['message_key'] : '';
			if ( isset( $seen[ $key ] ) ) {
				continue;
			}
			$seen[ $key ] = true;
			$results[]    = $this->explain( $key );
		}

		return ! empty( $results ) ? $results : array( $this->explain( '' ) );
	}

	/** @return string[] */
	public function known_keys(): array {
		return array_keys( $this->catalog() );
	}

	/**
	 * @return array<string,array{title:string,detected:string,importance:string,steps:string[],context:string,known:bool}>
	 */
	private function catalog(): array {
		$compare_file   = array(
			__( 'Do not delete the file before preserving a backup or forensic copy.', 'localguard-scanner' ),
			__( 'Compare it with the same version from the official plugin or WordPress package.', 'localguard-scanner' ),
		);
		$review_account = array(
			__( 'Confirm the change with the account owner or another trusted administrator.', 'localguard-scanner' ),
			__( 'If it was not expected, revoke access and rotate administrator and hosting credentials.', 'localguard-scanner' ),
		);
		$review_content = array(
			__( 'Preserve a backup before editing the affected content.', 'localguard-scanner' ),
			__( 'Review the item in WordPress and remove it only after confirming it is not intentional.', 'localguard-scanner' ),
		);

		return array(
			'known_webshell_marker'                       => $this->entry(
				__( 'Known webshell marker', 'localguard-scanner' ),
				__( 'The file contains a marker associated with known webshell families.', 'localguard-scanner' ),
				__( 'Webshells can give an attacker remote control of the site, so this signal deserves urgent verification.', 'localguard-scanner' ),
				$compare_file,
				__( 'A marker is strong evidence, but the complete file still needs manual verification.', 'localguard-scanner' )
			),
			'decoder_execution_sequence'                  => $this->entry(
				__( 'Possible execution of decoded code', 'localguard-scanner' ),
				__( 'Decoded data reaches an operation capable of executing it as PHP code.', 'localguard-scanner' ),
				__( 'Backdoors often hide code this way, although some legitimate plugins may use similar techniques.', 'localguard-scanner' ),
				$compare_file,
				__( 'This pattern raises review priority; it does not prove that the file is malware.', 'localguard-scanner' )
			),
			'request_to_file_write_sequence'              => $this->entry(
				__( 'Request data reaches a file write', 'localguard-scanner' ),
				__( 'Data received from a web request can reach a file-writing operation.', 'localguard-scanner' ),
				__( 'Without strict validation, this flow can sometimes be used to place or alter files on the server.', 'localguard-scanner' ),
				$compare_file,
				__( 'Logging and upload features can be legitimate examples of this pattern.', 'localguard-scanner' )
			),
			'request_controlled_file_path'                => $this->entry(
				__( 'Request data controls a file path', 'localguard-scanner' ),
				__( 'A value from a web request influences which local path a file operation uses.', 'localguard-scanner' ),
				__( 'An insufficient path allowlist can permit reading, overwriting or creating unintended files.', 'localguard-scanner' ),
				$compare_file,
				__( 'Confirm that the surrounding code restricts destinations to an expected directory and file type.', 'localguard-scanner' )
			),
			'request_data_to_executable_file'             => $this->entry(
				__( 'Request data may be written to an executable file', 'localguard-scanner' ),
				__( 'Data received from a request reaches a destination that can be executed as PHP.', 'localguard-scanner' ),
				__( 'If reachable without appropriate authorization, this could allow remote code execution.', 'localguard-scanner' ),
				$compare_file,
				__( 'Verify authentication, allowed extensions and the final destination before deciding whether it is malicious.', 'localguard-scanner' )
			),
			'php_file_in_uploads'                         => $this->entry(
				__( 'Executable PHP file inside uploads', 'localguard-scanner' ),
				__( 'A PHP-capable file exists below the WordPress uploads directory.', 'localguard-scanner' ),
				__( 'Uploads normally contain media, and attackers often place executable backdoors there.', 'localguard-scanner' ),
				$compare_file,
				__( 'Some plugins create harmless index.php placeholders, so inspect or compare the file before deleting it.', 'localguard-scanner' )
			),
			'safe_upload_index_marker'                    => $this->entry(
				__( 'Inert uploads PHP file verified', 'localguard-scanner' ),
				__( 'The uploads PHP file contains only the small inert structure LocalGuard permits.', 'localguard-scanner' ),
				__( 'The file is currently unable to perform an action and is informational rather than evidence of infection.', 'localguard-scanner' ),
				array( __( 'No immediate action is required; keep monitoring it for future changes.', 'localguard-scanner' ) ),
				__( 'Any executable modification will cause LocalGuard to reassess the file.', 'localguard-scanner' )
			),
			'active_plugin_known_unpatched_vulnerability' => $this->entry(
				__( 'Active plugin has a known unpatched vulnerability', 'localguard-scanner' ),
				__( 'The installed active version matches an advisory in LocalGuard’s signed local catalog.', 'localguard-scanner' ),
				__( 'A publicly known vulnerability can be exploited even when the plugin files themselves are clean.', 'localguard-scanner' ),
				array(
					__( 'Check the plugin vendor for a fixed release or mitigation.', 'localguard-scanner' ),
					__( 'If no safe update exists, replace or deactivate the plugin after testing.', 'localguard-scanner' ),
				),
				__( 'The bundled advisory catalog is a dated offline snapshot, not a live vulnerability service.', 'localguard-scanner' )
			),
			'inactive_plugin_known_unpatched_vulnerability' => $this->entry(
				__( 'Inactive plugin has a known unpatched vulnerability', 'localguard-scanner' ),
				__( 'An installed but inactive plugin matches an advisory in the signed local catalog.', 'localguard-scanner' ),
				__( 'Inactive plugin files remain on disk and may still increase the attack surface.', 'localguard-scanner' ),
				array(
					__( 'Remove the plugin if it is no longer needed.', 'localguard-scanner' ),
					__( 'Otherwise, update or replace it before activating it again.', 'localguard-scanner' ),
				),
				__( 'The bundled advisory catalog is a dated offline snapshot, not a live vulnerability service.', 'localguard-scanner' )
			),
			'wordpress_core_file_modified'                => $this->entry(
				__( 'WordPress core file differs from the official hash', 'localguard-scanner' ),
				__( 'The file hash does not match the signed local manifest for this exact WordPress version.', 'localguard-scanner' ),
				__( 'Unexpected core changes can indicate corruption, a failed update or malicious modification.', 'localguard-scanner' ),
				array(
					__( 'Preserve a backup and confirm the installed WordPress version.', 'localguard-scanner' ),
					__( 'Replace altered core files with a clean package from WordPress.org.', 'localguard-scanner' ),
				),
				__( 'Custom changes to WordPress core are also reported because they cannot be trusted automatically.', 'localguard-scanner' )
			),
			'wordpress_core_file_missing'                 => $this->entry(
				__( 'WordPress core file is missing', 'localguard-scanner' ),
				__( 'A file expected for this exact WordPress version was not observed during a complete file pass.', 'localguard-scanner' ),
				__( 'Missing core files can break security controls or indicate an incomplete update.', 'localguard-scanner' ),
				array( __( 'Restore the file from the matching clean WordPress package after taking a backup.', 'localguard-scanner' ) ),
				__( 'This conclusion is produced only when the file scan has complete coverage.', 'localguard-scanner' )
			),
			'unexpected_file_in_wordpress_core'           => $this->entry(
				__( 'Unexpected file inside WordPress core', 'localguard-scanner' ),
				__( 'A file not listed in the exact manifest exists inside wp-admin or wp-includes.', 'localguard-scanner' ),
				__( 'Attackers sometimes hide files in trusted-looking core directories.', 'localguard-scanner' ),
				$compare_file,
				__( 'Maintenance files added by hosting tools may be legitimate, but they should have a documented owner.', 'localguard-scanner' )
			),
			'recent_administrator_account'                => $this->entry(
				__( 'Recently created administrator account', 'localguard-scanner' ),
				__( 'An administrator account was registered during the recent-account window.', 'localguard-scanner' ),
				__( 'Unexpected administrators are a common sign of account or site compromise.', 'localguard-scanner' ),
				$review_account,
				__( 'A recent account can be legitimate if it was created by a trusted operator.', 'localguard-scanner' )
			),
			'user_privileges_increased'                   => $this->entry(
				__( 'User privileges increased', 'localguard-scanner' ),
				__( 'The account now has administrator privileges that were absent from the previous baseline.', 'localguard-scanner' ),
				__( 'Privilege escalation gives the account control over plugins, users and site configuration.', 'localguard-scanner' ),
				$review_account,
				__( 'The baseline records the change but cannot determine who authorized it.', 'localguard-scanner' )
			),
			'administrator_email_changed'                 => $this->entry(
				__( 'Administrator email changed', 'localguard-scanner' ),
				__( 'The hashed email for an administrator differs from the previous baseline.', 'localguard-scanner' ),
				__( 'An attacker may change an email address to retain password-reset access.', 'localguard-scanner' ),
				$review_account,
				__( 'LocalGuard stores only a one-way hash of the email in its baseline.', 'localguard-scanner' )
			),
			'sensitive_option_changed'                    => $this->entry(
				__( 'Sensitive WordPress option changed', 'localguard-scanner' ),
				__( 'A security-relevant option differs from the previous local baseline.', 'localguard-scanner' ),
				__( 'Unexpected URL, registration or role settings can redirect users or weaken access controls.', 'localguard-scanner' ),
				array(
					__( 'Review the option in WordPress and confirm the change with a trusted administrator.', 'localguard-scanner' ),
					__( 'If unauthorized, restore the expected value and rotate privileged credentials.', 'localguard-scanner' ),
				),
				__( 'A baseline change records a difference, not the identity of the person who made it.', 'localguard-scanner' )
			),
			'executable_content_in_autoloaded_option'     => $this->entry(
				__( 'Executable-looking content in an autoloaded option', 'localguard-scanner' ),
				__( 'An automatically loaded option contains text that resembles executable PHP.', 'localguard-scanner' ),
				__( 'Injected code in autoloaded data can be loaded on many requests or used by another compromised component.', 'localguard-scanner' ),
				array( __( 'Identify the owning plugin and inspect the option through a safe database or WordPress administration workflow.', 'localguard-scanner' ) ),
				__( 'Do not run or paste the option value into another tool.', 'localguard-scanner' )
			),
			'large_autoloaded_option'                     => $this->entry(
				__( 'Very large autoloaded option', 'localguard-scanner' ),
				__( 'An option loaded on normal requests is unusually large.', 'localguard-scanner' ),
				__( 'This is usually a performance concern, but unexpected growth can also hide injected data.', 'localguard-scanner' ),
				array( __( 'Identify the owning plugin and confirm that the size is expected before changing it.', 'localguard-scanner' ) ),
				__( 'Large values are sampled for detection; LocalGuard records their full database hash.', 'localguard-scanner' )
			),
			'random_looking_cron_hook'                    => $this->entry(
				__( 'Scheduled event has an unusual name', 'localguard-scanner' ),
				__( 'A WordPress cron hook has a long random-looking identifier.', 'localguard-scanner' ),
				__( 'Malware may use obscure scheduled tasks for persistence, although generated plugin names can be legitimate.', 'localguard-scanner' ),
				array( __( 'Identify which plugin or theme registered the event before removing it.', 'localguard-scanner' ) ),
				__( 'Deleting an unknown cron event without identifying its owner can break legitimate automation.', 'localguard-scanner' )
			),
			'suspicious_cron_arguments'                   => $this->entry(
				__( 'Scheduled event contains suspicious arguments', 'localguard-scanner' ),
				__( 'A WordPress cron event includes encoded or executable-looking argument data.', 'localguard-scanner' ),
				__( 'Scheduled arguments can preserve payloads and reactivate malicious behavior.', 'localguard-scanner' ),
				array( __( 'Identify the hook owner and compare the event with the plugin’s expected schedule.', 'localguard-scanner' ) ),
				__( 'LocalGuard does not display or execute the stored arguments.', 'localguard-scanner' )
			),
			'php_code_in_wordpress_content'               => $this->entry(
				__( 'PHP code found in WordPress content', 'localguard-scanner' ),
				__( 'A post or other WordPress content row contains a PHP opening sequence.', 'localguard-scanner' ),
				__( 'Normal content rarely needs PHP and injected code may be used by a vulnerable renderer or plugin.', 'localguard-scanner' ),
				$review_content,
				__( 'The scanner reports the content location without storing the full content.', 'localguard-scanner' )
			),
			'scripted_redirect_in_content'                => $this->entry(
				__( 'Scripted redirect found in WordPress content', 'localguard-scanner' ),
				__( 'Stored content contains a script-based redirect pattern.', 'localguard-scanner' ),
				__( 'Injected redirects are often used to send visitors to phishing, advertising or malware sites.', 'localguard-scanner' ),
				$review_content,
				__( 'Some intentional landing pages redirect visitors, so verify the destination and owner.', 'localguard-scanner' )
			),
			'hidden_iframe_in_content'                    => $this->entry(
				__( 'Hidden iframe found in WordPress content', 'localguard-scanner' ),
				__( 'Stored content includes an iframe hidden through markup or styling.', 'localguard-scanner' ),
				__( 'Hidden frames can load unwanted third-party pages without being visible to visitors.', 'localguard-scanner' ),
				$review_content,
				__( 'Some embeds hide frames intentionally during loading; confirm the source and purpose.', 'localguard-scanner' )
			),
			'long_encoded_payload'                        => $this->entry(
				__( 'Unusually long encoded payload', 'localguard-scanner' ),
				__( 'The file contains a long sequence that resembles encoded data.', 'localguard-scanner' ),
				__( 'Attackers encode payloads to conceal readable commands, but bundled assets may also contain long encoded data.', 'localguard-scanner' ),
				$compare_file,
				__( 'Treat this as supporting evidence and review it together with the other signals.', 'localguard-scanner' )
			),
			'repeated_character_construction'             => $this->entry(
				__( 'Code builds text character by character', 'localguard-scanner' ),
				__( 'The file repeatedly constructs strings from individual character values.', 'localguard-scanner' ),
				__( 'This can hide function names or commands from simple inspection, although generated code may do it legitimately.', 'localguard-scanner' ),
				$compare_file,
				__( 'Treat this as supporting evidence and review it together with the other signals.', 'localguard-scanner' )
			),
			'executable_double_extension'                 => $this->entry(
				__( 'Executable file uses a misleading double extension', 'localguard-scanner' ),
				__( 'The file name combines a harmless-looking extension with a PHP-capable extension.', 'localguard-scanner' ),
				__( 'Double extensions are commonly used to disguise executable uploads.', 'localguard-scanner' ),
				$compare_file,
				__( 'Confirm both the real file type and why the application created this name.', 'localguard-scanner' )
			),
			'core_filename_imitation'                     => $this->entry(
				__( 'File name imitates WordPress core', 'localguard-scanner' ),
				__( 'A non-core location contains a file whose name resembles a WordPress core file.', 'localguard-scanner' ),
				__( 'Attackers may use familiar names to make malicious files look trustworthy.', 'localguard-scanner' ),
				$compare_file,
				__( 'The name alone is not proof; verify the file’s owner and expected package.', 'localguard-scanner' )
			),
			'world_writable_file'                         => $this->entry(
				__( 'File is writable by every local user', 'localguard-scanner' ),
				__( 'The file permissions allow any local server account to modify it.', 'localguard-scanner' ),
				__( 'On shared systems, overly broad permissions can let another compromised account alter the site.', 'localguard-scanner' ),
				array(
					__( 'Confirm the permission requirement with the hosting provider or application owner.', 'localguard-scanner' ),
					__( 'Apply the least permissive mode that still lets WordPress operate correctly.', 'localguard-scanner' ),
				),
				__( 'LocalGuard reports the permission but does not change it.', 'localguard-scanner' )
			),
		);
	}

	/**
	 * @param string   $title      Title.
	 * @param string   $detected   Detection explanation.
	 * @param string   $importance Importance explanation.
	 * @param string[] $steps      Safe review steps.
	 * @param string   $context    Context note.
	 * @param bool     $known      Whether the key is known.
	 * @return array{title:string,detected:string,importance:string,steps:string[],context:string,known:bool}
	 */
	private function entry( string $title, string $detected, string $importance, array $steps, string $context, bool $known = true ): array {
		return array(
			'title'      => $title,
			'detected'   => $detected,
			'importance' => $importance,
			'steps'      => array_values( array_filter( $steps, 'is_string' ) ),
			'context'    => $context,
			'known'      => $known,
		);
	}
}
