<?php
/**
 * LocalGuard Scanner administration pages.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Admin;

use LocalGuardScanner\Application\Coverage\CoverageReporter;
use LocalGuardScanner\Application\Diagnostics\EnvironmentDiagnostics;
use LocalGuardScanner\Application\Scan\ScanOrchestrator;
use LocalGuardScanner\Infrastructure\WordPress\Capabilities;
use LocalGuardScanner\Infrastructure\WordPress\ManualScanController;
use LocalGuardScanner\Infrastructure\WordPress\Settings;
use Throwable;

/**
 * Renders four compact native administration surfaces.
 */
final class AdminController {
	const GARAJE_IMAGINA_PRODUCT_URL = 'https://garajeimagina.com/cms/localguard-scanner/';

	/** @var ScanOrchestrator */
	private $orchestrator;
	/** @var Settings */
	private $settings;
	/** @var CoverageReporter */
	private $coverage;
	/** @var EnvironmentDiagnostics */
	private $diagnostics;
	/** @var FindingPresenter */
	private $finding_presenter;
	/** @var FindingSourceCatalog */
	private $finding_sources;

	/**
	 * @param ScanOrchestrator       $orchestrator     Orchestrator.
	 * @param Settings               $settings    Settings.
	 * @param CoverageReporter       $coverage    Coverage reporter.
	 * @param EnvironmentDiagnostics $diagnostics Environment diagnostics.
	 * @param FindingPresenter|null     $finding_presenter Safe finding presenter.
	 * @param FindingSourceCatalog|null $finding_sources   Closed finding source catalog.
	 */
	public function __construct( ScanOrchestrator $orchestrator, Settings $settings, CoverageReporter $coverage, EnvironmentDiagnostics $diagnostics, ?FindingPresenter $finding_presenter = null, ?FindingSourceCatalog $finding_sources = null ) {
		$this->orchestrator      = $orchestrator;
		$this->settings          = $settings;
		$this->coverage          = $coverage;
		$this->diagnostics       = $diagnostics;
		$this->finding_presenter = null !== $finding_presenter ? $finding_presenter : new FindingPresenter( new FindingExplanationCatalog() );
		$this->finding_sources   = null !== $finding_sources ? $finding_sources : new FindingSourceCatalog();
	}

	/** @return void */
	public function register() {
		add_action( is_multisite() ? 'network_admin_menu' : 'admin_menu', array( $this, 'menu' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) );
	}

	/** @return void */
	public function menu() {
		$capability = is_multisite() ? 'manage_network_options' : Capabilities::MANAGE;
		add_menu_page( __( 'LocalGuard Scanner', 'localguard-scanner' ), __( 'LocalGuard', 'localguard-scanner' ), $capability, 'localguard-scanner', array( $this, 'overview' ), 'dashicons-shield-alt', 81 );
		add_submenu_page( 'localguard-scanner', __( 'Overview', 'localguard-scanner' ), __( 'Overview', 'localguard-scanner' ), $capability, 'localguard-scanner', array( $this, 'overview' ) );
		add_submenu_page( 'localguard-scanner', __( 'Scan', 'localguard-scanner' ), __( 'Scan', 'localguard-scanner' ), $capability, 'localguard-scanner-scan', array( $this, 'scan' ) );
		add_submenu_page( 'localguard-scanner', __( 'Findings', 'localguard-scanner' ), __( 'Findings', 'localguard-scanner' ), $capability, 'localguard-scanner-findings', array( $this, 'findings' ) );
		add_submenu_page( 'localguard-scanner', __( 'Settings', 'localguard-scanner' ), __( 'Settings', 'localguard-scanner' ), $capability, 'localguard-scanner-settings', array( $this, 'settings' ) );
	}

	/** @param string $hook Current hook. @return void */
	public function assets( string $hook ): void {
		if ( false === strpos( $hook, 'localguard-scanner' ) ) {
			return;
		}
		wp_enqueue_style( 'localguard-scanner-admin', LOCALGUARD_SCANNER_URL . 'assets/admin.css', array(), LOCALGUARD_SCANNER_VERSION );
		wp_enqueue_script( 'localguard-scanner-admin', LOCALGUARD_SCANNER_URL . 'assets/admin.js', array(), LOCALGUARD_SCANNER_VERSION, true );
		wp_localize_script(
			'localguard-scanner-admin',
			'LocalGuardScannerAdmin',
			array(
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( ManualScanController::NONCE_ACTION ),
				'labels'  => array(
					'running'          => __( 'Scanning…', 'localguard-scanner' ),
					'complete'         => __( 'Scan complete.', 'localguard-scanner' ),
					'partial'          => __( 'Scan completed with incomplete coverage.', 'localguard-scanner' ),
					'cancelled'        => __( 'Scan cancelled.', 'localguard-scanner' ),
					'failed'           => __( 'Scan failed safely. Review diagnostics.', 'localguard-scanner' ),
					'provisional'      => __( 'Provisional data', 'localguard-scanner' ),
					'elements'         => __( 'elements', 'localguard-scanner' ),
					'files'            => __( 'files reviewed', 'localguard-scanner' ),
					'database'         => __( 'database elements', 'localguard-scanner' ),
					'review'           => __( 'require review', 'localguard-scanner' ),
					'informational'    => __( 'informational', 'localguard-scanner' ),
					'previousChecked'  => __( 'previous file findings checked', 'localguard-scanner' ),
					'movedToHistory'   => __( 'moved to History', 'localguard-scanner' ),
					'activity'         => __( 'Last activity', 'localguard-scanner' ),
					'now'              => __( 'now', 'localguard-scanner' ),
					'area'             => __( 'Current area', 'localguard-scanner' ),
					'batch'            => __( 'batch', 'localguard-scanner' ),
					'noSiteChange'     => __( 'The scanner did not modify the site.', 'localguard-scanner' ),
					'lastCheckpoint'   => __( 'Last safe checkpoint', 'localguard-scanner' ),
					'stage'            => __( 'Stage', 'localguard-scanner' ),
					'operation'        => __( 'Operation', 'localguard-scanner' ),
					'code'             => __( 'Code', 'localguard-scanner' ),
					'copyDiagnostic'   => __( 'Copy diagnostic', 'localguard-scanner' ),
					'diagnosticCopied' => __( 'Diagnostic copied.', 'localguard-scanner' ),
					'diagnosticSafe'   => __( 'The diagnostic excludes paths, inspected content, SQL, and secrets.', 'localguard-scanner' ),
					'requestFailed'    => __( 'The scanner did not answer in time. Its last safe checkpoint is preserved.', 'localguard-scanner' ),
					'copied'           => __( 'Path copied.', 'localguard-scanner' ),
					'copyFailed'       => __( 'Select and copy the path manually.', 'localguard-scanner' ),
					'coverageTitle'    => __( 'Coverage by area', 'localguard-scanner' ),
					'coverageNote'     => __( 'A verified area may resolve its previous findings even when another area has incomplete coverage.', 'localguard-scanner' ),
					'verified'         => __( 'Verified', 'localguard-scanner' ),
					'incomplete'       => __( 'Incomplete', 'localguard-scanner' ),
					'coverageScopes'   => array(
						'files'    => __( 'Files', 'localguard-scanner' ),
						'database' => __( 'Database', 'localguard-scanner' ),
						'plugins'  => __( 'Plugin advisory catalog', 'localguard-scanner' ),
					),
					'coverageReasons'  => array(
						'unreadable_directory'         => __( 'Directory could not be read', 'localguard-scanner' ),
						'unreadable_metadata'          => __( 'File metadata changed or could not be read', 'localguard-scanner' ),
						'unsafe_path_name'             => __( 'Path name could not be represented safely across platforms', 'localguard-scanner' ),
						'excluded_directory'           => __( 'Directory excluded by configuration', 'localguard-scanner' ),
						'unreadable_hash'              => __( 'File could not be hashed safely', 'localguard-scanner' ),
						'hash_size_limit'              => __( 'File exceeded the hashing size limit', 'localguard-scanner' ),
						'symlink_not_followed'         => __( 'Symbolic link was recorded but not followed', 'localguard-scanner' ),
						'unreadable_content'           => __( 'File could not be read safely for deep inspection', 'localguard-scanner' ),
						'deep_scan_size_limit'         => __( 'File exceeded the deep-inspection size limit', 'localguard-scanner' ),
						'core_manifest_unavailable'    => __( 'No exact local WordPress core manifest was available', 'localguard-scanner' ),
						'core_missing_results_limited' => __( 'Missing core-file results exceeded the safe display limit', 'localguard-scanner' ),
						'db_value_size_limit'          => __( 'A large database value was sampled instead of read in full', 'localguard-scanner' ),
					),
					'activities'       => array(
						'working'  => __( 'Working', 'localguard-scanner' ),
						'waiting'  => __( 'Waiting for the next scheduled batch', 'localguard-scanner' ),
						'inactive' => __( 'No activity for more than two minutes', 'localguard-scanner' ),
						'stopped'  => __( 'Stopped safely', 'localguard-scanner' ),
						'finished' => __( 'Finished', 'localguard-scanner' ),
					),
					'phases'           => array(
						'discovering'       => __( 'Preparing inventory', 'localguard-scanner' ),
						'scanning_files'    => __( 'Scanning files', 'localguard-scanner' ),
						'scanning_database' => __( 'Scanning database', 'localguard-scanner' ),
						'classifying'       => __( 'Checking previous findings', 'localguard-scanner' ),
						'completed'         => __( 'Complete', 'localguard-scanner' ),
						'partial'           => __( 'Partial coverage', 'localguard-scanner' ),
						'failed'            => __( 'Failed safely', 'localguard-scanner' ),
						'queued'            => __( 'Queued', 'localguard-scanner' ),
					),
				),
			)
		);
	}

	/** @return void */
	public function overview() {
		$this->guard();
		try {
			$scan      = $this->orchestrator->scans()->latest();
			$counts    = $this->orchestrator->findings()->active_severity_counts();
			$detection = $this->orchestrator->detection_metadata();
		} catch ( Throwable $error ) {
			$this->render_storage_error( __( 'Security overview', 'localguard-scanner' ), 'localguard-scanner' );
			return;
		}
		$this->header( __( 'Security overview', 'localguard-scanner' ), 'localguard-scanner' );
		echo '<p class="lgs-lead">' . esc_html__( 'A prioritized view of this WordPress installation. Automated findings require verification and do not prove that malware is present.', 'localguard-scanner' ) . '</p>';
		echo '<div class="lgs-summary" aria-label="' . esc_attr__( 'Finding counts', 'localguard-scanner' ) . '">';
		foreach ( array( 'critical', 'high', 'medium', 'low', 'info' ) as $severity ) {
			echo '<p><strong class="lgs-count lgs-' . esc_attr( $severity ) . '">' . esc_html( (string) max( 0, (int) $counts[ $severity ] ) ) . '</strong> ' . esc_html( $this->severity_label( $severity ) ) . '</p>';
		}
		echo '</div>';
		$priority_count = max( 0, (int) $counts['critical'] ) + max( 0, (int) $counts['high'] );
		if ( $priority_count > 0 ) {
			/* translators: %d: number of active critical and high-risk findings. */
			echo '<div class="lgs-callout lgs-callout-danger"><span class="dashicons dashicons-warning" aria-hidden="true"></span><p><strong>' . esc_html__( 'Urgent review recommended.', 'localguard-scanner' ) . '</strong> ' . esc_html( sprintf( __( '%d critical or high-risk findings should be checked first.', 'localguard-scanner' ), $priority_count ) ) . '</p></div>';
		} elseif ( (int) $counts['medium'] > 0 ) {
			echo '<div class="lgs-callout lgs-callout-warning"><span class="dashicons dashicons-flag" aria-hidden="true"></span><p><strong>' . esc_html__( 'Review recommended.', 'localguard-scanner' ) . '</strong> ' . esc_html__( 'There are medium-risk findings, but no active critical or high-risk findings.', 'localguard-scanner' ) . '</p></div>';
		} else {
			echo '<div class="lgs-callout lgs-callout-success"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span><p><strong>' . esc_html__( 'No priority findings are active.', 'localguard-scanner' ) . '</strong> ' . esc_html__( 'This result is meaningful only when the latest scan completed with full coverage.', 'localguard-scanner' ) . '</p></div>';
		}
		if ( '' !== $detection['advisory_date'] ) {
			/* translators: %s: compilation date of the packaged offline advisory catalog. */
			echo '<p class="description">' . esc_html( sprintf( __( 'Local plugin advisory catalog compiled on %s. This packaged snapshot does not replace a continuously updated vulnerability service.', 'localguard-scanner' ), $detection['advisory_date'] ) ) . '</p>';
		}
		if ( null === $scan ) {
			echo '<p>' . esc_html__( 'No scan has been completed yet.', 'localguard-scanner' ) . '</p>';
		} else {
			echo '<h2>' . esc_html__( 'Latest scan', 'localguard-scanner' ) . '</h2>';
			$status     = $this->scan_status_key( isset( $scan['status'] ) && is_string( $scan['status'] ) ? $scan['status'] : '' );
			$created_at = isset( $scan['created_at'] ) && is_string( $scan['created_at'] ) ? $scan['created_at'] : '';
			echo '<p><span class="lgs-status lgs-' . esc_attr( $status ) . '">' . esc_html( $this->status_label( $status ) ) . '</span>' . ( '' !== $created_at ? ' <span class="lgs-muted">· ' . esc_html( $created_at ) . '</span>' : '' ) . '</p>';
			if ( 'partial' === $status ) {
				echo '<div class="lgs-callout lgs-callout-warning"><span class="dashicons dashicons-visibility" aria-hidden="true"></span><p><strong>' . esc_html__( 'Coverage was incomplete.', 'localguard-scanner' ) . '</strong> ' . esc_html__( 'Review the limitations below before treating this result as clean.', 'localguard-scanner' ) . '</p></div>';
			}
			$coverage = $this->coverage->from_json( isset( $scan['coverage_json'] ) ? $scan['coverage_json'] : '' );
			$this->render_coverage( $coverage );
		}
		$findings_url = is_multisite() ? network_admin_url( 'admin.php?page=localguard-scanner-findings' ) : admin_url( 'admin.php?page=localguard-scanner-findings' );
		$scan_url     = is_multisite() ? network_admin_url( 'admin.php?page=localguard-scanner-scan' ) : admin_url( 'admin.php?page=localguard-scanner-scan' );
		echo '<p class="lgs-actions"><a class="button button-primary" href="' . esc_url( $findings_url ) . '">' . esc_html__( 'Review findings', 'localguard-scanner' ) . '</a> <a class="button" href="' . esc_url( $scan_url ) . '">' . esc_html__( 'Open scanner', 'localguard-scanner' ) . '</a></p>';
		$this->render_overview_attribution();
		$this->footer();
	}

	/** @return void */
	private function render_overview_attribution(): void {
		echo '<section class="lgs-attribution" aria-labelledby="lgs-attribution-title">';
		echo '<span class="dashicons dashicons-admin-home" aria-hidden="true"></span><div>';
		echo '<h2 id="lgs-attribution-title">' . esc_html__( 'Built by Garaje Imagina', 'localguard-scanner' ) . '</h2>';
		echo '<p>' . esc_html__( 'LocalGuard Scanner is an independent Garaje Imagina product, created to make WordPress security reviews more private, understandable, and controlled.', 'localguard-scanner' ) . '</p>';
		echo '<p><a href="' . esc_url( self::GARAJE_IMAGINA_PRODUCT_URL ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Discover LocalGuard Scanner on Garaje Imagina ↗', 'localguard-scanner' ) . '</a></p>';
		echo '</div></section>';
	}

	/** @return void */
	public function scan() {
		$this->guard();
		try {
			$active       = $this->orchestrator->scans()->active();
			$display_scan = null !== $active ? $active : $this->orchestrator->scans()->latest();
		} catch ( Throwable $error ) {
			$this->render_storage_error( __( 'Scan this WordPress installation', 'localguard-scanner' ), 'localguard-scanner-scan' );
			return;
		}
		$this->header( __( 'Scan this WordPress installation', 'localguard-scanner' ), 'localguard-scanner-scan' );
		echo '<p class="lgs-lead">' . esc_html__( 'Inspect files and supported WordPress database data in short batches designed for shared VPS servers.', 'localguard-scanner' ) . '</p>';
		echo '<div class="lgs-callout lgs-callout-info"><span class="dashicons dashicons-lock" aria-hidden="true"></span><p><strong>' . esc_html__( 'The inspection is local.', 'localguard-scanner' ) . '</strong> ' . esc_html__( 'No files or database content leave this server. Alert emails contain counts only, never paths or inspected content.', 'localguard-scanner' ) . '</p></div>';
		echo '<p class="lgs-actions"><button type="button" class="button button-primary button-hero" id="lgs-start-scan"' . ( null !== $active ? ' disabled' : '' ) . '>' . esc_html__( 'Scan now', 'localguard-scanner' ) . '</button> ';
		echo '<button type="button" class="button" id="lgs-cancel-scan"' . ( null === $active ? ' hidden' : '' ) . '>' . esc_html__( 'Cancel scan', 'localguard-scanner' ) . '</button></p>';
		echo '<div id="lgs-progress" class="lgs-progress" data-scan-id="' . esc_attr( null !== $display_scan ? (string) $display_scan['id'] : '' ) . '" role="status" aria-live="polite" aria-atomic="true"></div>';
		echo '<p class="description lgs-scan-help">' . esc_html__( 'You may close this page after starting a scan. WP-Cron will continue it when the site receives an opportunity to run scheduled work.', 'localguard-scanner' ) . '</p>';
		$this->render_diagnostics();
		$this->footer();
	}

	/** @return void */
	public function findings() {
		$this->guard();
		$page             = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only pagination.
		$requested_view   = isset( $_GET['view'] ) && is_string( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'active'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- closed read-only view selector.
		$requested_source = isset( $_GET['source'] ) && is_string( $_GET['source'] ) ? sanitize_key( wp_unslash( $_GET['source'] ) ) : FindingSourceCatalog::ALL; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- closed read-only source selector.
		$view             = in_array( $requested_view, array( 'active', 'history' ), true ) ? $requested_view : 'active';
		$source           = $this->finding_sources->normalize( $requested_source );
		$types            = $this->finding_sources->types( $source );
		$per_page         = 50;
		try {
			$active_total   = $this->orchestrator->findings()->active_count( $types );
			$resolved_total = $this->orchestrator->findings()->resolved_count( $types );
			$total          = 'history' === $view ? $resolved_total : $active_total;
			$source_counts  = array();
			foreach ( $this->finding_sources->sources() as $candidate_source ) {
				$candidate_types                    = $this->finding_sources->types( $candidate_source );
				$source_counts[ $candidate_source ] = 'history' === $view
					? $this->orchestrator->findings()->resolved_count( $candidate_types )
					: $this->orchestrator->findings()->active_count( $candidate_types );
			}
			$findings = 'history' === $view
				? $this->orchestrator->findings()->resolved_findings( $per_page, ( $page - 1 ) * $per_page, $types )
				: $this->orchestrator->findings()->latest_findings( $per_page, ( $page - 1 ) * $per_page, $types );
		} catch ( Throwable $error ) {
			$this->render_storage_error( __( 'Security findings', 'localguard-scanner' ), 'localguard-scanner-findings' );
			return;
		}
		$presented     = $this->finding_presenter->present_many( $findings );
		$priority_open = 'active' === $view ? $this->finding_presenter->first_priority_index( $presented ) : null;
		$this->header( __( 'Security findings', 'localguard-scanner' ), 'localguard-scanner-findings' );
		$active_url  = $this->findings_url( 'active', $source );
		$history_url = $this->findings_url( 'history', $source );
		echo '<nav class="nav-tab-wrapper lgs-finding-tabs" aria-label="' . esc_attr__( 'Finding views', 'localguard-scanner' ) . '">';
		echo '<a class="nav-tab' . ( 'active' === $view ? ' nav-tab-active' : '' ) . '" href="' . esc_url( $active_url ) . '">' . esc_html__( 'Active', 'localguard-scanner' ) . ' <span class="lgs-tab-count">' . esc_html( (string) max( 0, (int) $active_total ) ) . '</span></a>';
		echo '<a class="nav-tab' . ( 'history' === $view ? ' nav-tab-active' : '' ) . '" href="' . esc_url( $history_url ) . '">' . esc_html__( 'History', 'localguard-scanner' ) . ' <span class="lgs-tab-count">' . esc_html( (string) max( 0, (int) $resolved_total ) ) . '</span></a>';
		echo '</nav>';
		echo '<nav class="nav-tab-wrapper lgs-source-tabs" aria-label="' . esc_attr__( 'Finding sources', 'localguard-scanner' ) . '">';
		foreach ( $this->finding_sources->sources() as $candidate_source ) {
			echo '<a class="nav-tab' . ( $candidate_source === $source ? ' nav-tab-active' : '' ) . '" href="' . esc_url( $this->findings_url( $view, $candidate_source ) ) . '">' . esc_html( $this->finding_sources->label( $candidate_source ) ) . ' <span class="lgs-tab-count">' . esc_html( (string) max( 0, (int) $source_counts[ $candidate_source ] ) ) . '</span></a>';
		}
		echo '</nav>';
		if ( 'history' === $view ) {
			/* translators: %d: total number of resolved findings. */
			echo '<p class="lgs-lead">' . esc_html( sprintf( __( '%d resolved findings retained for audit history.', 'localguard-scanner' ), max( 0, (int) $total ) ) ) . '</p>';
			echo '<div class="lgs-callout lgs-callout-info"><span class="dashicons dashicons-backup" aria-hidden="true"></span><p>' . esc_html__( 'These findings no longer require action. They were not observed again by a later verified scan of their area.', 'localguard-scanner' ) . '</p></div>';
		} else {
			/* translators: %d: total number of active findings. */
			echo '<p class="lgs-lead">' . esc_html( sprintf( __( '%d active findings, ordered by review priority.', 'localguard-scanner' ), max( 0, (int) $total ) ) ) . '</p>';
			echo '<div class="lgs-callout lgs-callout-info"><span class="dashicons dashicons-info-outline" aria-hidden="true"></span><p>' . esc_html__( 'A finding is a reason to investigate, not automatic confirmation of malware. Preserve a backup before changing files or database data.', 'localguard-scanner' ) . '</p></div>';
		}
		if ( empty( $presented ) ) {
			if ( 'history' === $view ) {
				echo '<div class="lgs-empty-state"><span class="dashicons dashicons-backup" aria-hidden="true"></span><h2>' . esc_html__( 'No resolved findings are stored yet.', 'localguard-scanner' ) . '</h2><p>' . esc_html__( 'Findings that disappear after a verified scan of their area will be kept here.', 'localguard-scanner' ) . '</p></div>';
			} else {
				echo '<div class="lgs-empty-state"><span class="dashicons dashicons-yes-alt" aria-hidden="true"></span><h2>' . esc_html__( 'No active findings are stored.', 'localguard-scanner' ) . '</h2><p>' . esc_html__( 'Run a complete scan regularly and review any coverage limitations before considering the site clean.', 'localguard-scanner' ) . '</p></div>';
			}
			$this->footer();
			return;
		}
		$list_label = 'history' === $view ? __( 'Resolved security findings', 'localguard-scanner' ) : __( 'Active security findings', 'localguard-scanner' );
		echo '<div class="lgs-findings" aria-label="' . esc_attr( $list_label ) . '">';
		foreach ( $presented as $index => $finding ) {
			$this->render_finding( $finding, null !== $priority_open && $index === $priority_open );
		}
		echo '</div>';
		$total_pages = (int) ceil( $total / $per_page );
		if ( $total_pages > 1 ) {
			$base = $this->findings_url( $view, $source, '%#%' );
			echo '<nav class="lgs-pagination" aria-label="' . esc_attr__( 'Findings pages', 'localguard-scanner' ) . '">';
			echo wp_kses_post(
				paginate_links(
					array(
						'base'    => $base,
						'current' => $page,
						'total'   => $total_pages,
						'type'    => 'plain',
					)
				)
			);
			echo '</nav>';
		}
		$this->footer();
	}

	/**
	 * @param string          $view   Finding state view.
	 * @param string          $source Finding source.
	 * @param int|string|null $page   Page number placeholder.
	 * @return string
	 */
	private function findings_url( string $view, string $source, $page = null ): string {
		$path = 'admin.php?page=localguard-scanner-findings&view=' . $view . '&source=' . $source;
		if ( null !== $page ) {
			$path .= '&paged=' . $page;
		}
		return is_multisite() ? network_admin_url( $path ) : admin_url( $path );
	}

	/** @return void */
	public function settings() {
		$this->guard();
		$settings = $this->settings->all();
		$this->header( __( 'LocalGuard Scanner settings', 'localguard-scanner' ), 'localguard-scanner-settings' );
		if ( isset( $_GET['updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only success notice after a protected save.
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'localguard-scanner' ) . '</p></div>';
		}
		echo '<p class="lgs-lead">' . esc_html__( 'Control the scan schedule and resource use without sending inspected data to an external service.', 'localguard-scanner' ) . '</p>';
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		wp_nonce_field( 'localguard_scanner_settings' );
		echo '<input type="hidden" name="action" value="localguard_save_settings">';
		echo '<table class="form-table" role="presentation"><tbody>';
		echo '<tr><th scope="row">' . esc_html__( 'Automatic scan', 'localguard-scanner' ) . '</th><td><label><input type="checkbox" name="localguard_scanner[daily_enabled]" value="1"' . checked( ! empty( $settings['daily_enabled'] ), true, false ) . '> ' . esc_html__( 'Run once a day', 'localguard-scanner' ) . '</label><p class="description">' . esc_html__( 'Automatic scans use WP-Cron and continue in small resumable batches.', 'localguard-scanner' ) . '</p></td></tr>';
		echo '<tr><th scope="row"><label for="lgs-daily-hour">' . esc_html__( 'Local start hour', 'localguard-scanner' ) . '</label></th><td><input id="lgs-daily-hour" type="number" min="0" max="23" name="localguard_scanner[daily_hour]" value="' . esc_attr( (string) $settings['daily_hour'] ) . '"><p class="description">' . esc_html__( 'This is the earliest local hour. WP-Cron may start later if the site has no traffic or scheduled runner.', 'localguard-scanner' ) . '</p></td></tr>';
		echo '<tr><th scope="row"><label for="lgs-batch-size">' . esc_html__( 'Batch size', 'localguard-scanner' ) . '</label></th><td><input id="lgs-batch-size" type="number" min="25" max="250" name="localguard_scanner[batch_size]" value="' . esc_attr( (string) $settings['batch_size'] ) . '"><p class="description">' . esc_html__( 'Use 25–100 on constrained shared VPS servers. Smaller batches reduce peak load but can take longer to finish.', 'localguard-scanner' ) . '</p></td></tr>';
		echo '<tr><th scope="row"><label for="lgs-recipients">' . esc_html__( 'Alert recipients', 'localguard-scanner' ) . '</label></th><td><textarea id="lgs-recipients" rows="3" class="large-text" name="localguard_scanner[recipients]">' . esc_textarea( implode( "\n", $settings['recipients'] ) ) . '</textarea><p class="description">' . esc_html__( 'Emails contain severity counts and an authenticated dashboard link, never finding paths or inspected content.', 'localguard-scanner' ) . '</p></td></tr>';
		echo '<tr><th scope="row"><label for="lgs-exclusions">' . esc_html__( 'Excluded directories', 'localguard-scanner' ) . '</label></th><td><textarea id="lgs-exclusions" rows="4" class="large-text code" name="localguard_scanner[exclusions]">' . esc_textarea( implode( "\n", $settings['exclusions'] ) ) . '</textarea><p class="description">' . esc_html__( 'Relative paths only, one per line. An excluded directory is never inspected: it is reported as skipped coverage, and findings already stored inside it are never resolved automatically.', 'localguard-scanner' ) . '</p></td></tr>';
		echo '</tbody></table>';
		submit_button();
		echo '</form>';
		$this->footer();
	}

	/** @return void */
	private function guard() {
		if ( ! Capabilities::current_user_can_manage() ) {
			wp_die( esc_html__( 'You are not allowed to view LocalGuard Scanner.', 'localguard-scanner' ), '', array( 'response' => 403 ) );
		}
	}

	/** @return void */
	private function header( string $title, string $active_slug ): void {
		echo '<div class="wrap lgs-wrap"><h1>' . esc_html( $title ) . '</h1><nav class="nav-tab-wrapper">';
		$tabs = array(
			'localguard-scanner'          => __( 'Overview', 'localguard-scanner' ),
			'localguard-scanner-scan'     => __( 'Scan', 'localguard-scanner' ),
			'localguard-scanner-findings' => __( 'Findings', 'localguard-scanner' ),
			'localguard-scanner-settings' => __( 'Settings', 'localguard-scanner' ),
		);
		foreach ( $tabs as $slug => $label ) {
			$url = is_multisite() ? network_admin_url( 'admin.php?page=' . $slug ) : admin_url( 'admin.php?page=' . $slug );
			echo '<a class="nav-tab' . ( $active_slug === $slug ? ' nav-tab-active' : '' ) . '" href="' . esc_url( $url ) . '">' . esc_html( $label ) . '</a>';
		}
		echo '</nav><main class="lgs-main">';
	}

	/** @return void */
	private function footer() {
		echo '</main></div>';
	}

	/** @return string */
	private function severity_label( string $severity ): string {
		$labels = array(
			'critical' => __( 'Critical', 'localguard-scanner' ),
			'high'     => __( 'High', 'localguard-scanner' ),
			'medium'   => __( 'Medium', 'localguard-scanner' ),
			'low'      => __( 'Low', 'localguard-scanner' ),
			'info'     => __( 'Information', 'localguard-scanner' ),
		);
		return isset( $labels[ $severity ] ) ? $labels[ $severity ] : __( 'Unknown', 'localguard-scanner' );
	}

	/** @return string */
	private function status_label( string $status ): string {
		$labels = array(
			'completed'         => __( 'Complete', 'localguard-scanner' ),
			'partial'           => __( 'Partial coverage', 'localguard-scanner' ),
			'failed'            => __( 'Failed safely', 'localguard-scanner' ),
			'cancelled'         => __( 'Cancelled', 'localguard-scanner' ),
			'scanning_files'    => __( 'Scanning files', 'localguard-scanner' ),
			'scanning_database' => __( 'Scanning database', 'localguard-scanner' ),
			'classifying'       => __( 'Checking previous findings', 'localguard-scanner' ),
		);
		return isset( $labels[ $status ] ) ? $labels[ $status ] : __( 'Queued', 'localguard-scanner' );
	}

	/** @return string */
	private function finding_status_label( string $status ): string {
		$labels = array(
			'new'        => __( 'New', 'localguard-scanner' ),
			'persistent' => __( 'Persistent', 'localguard-scanner' ),
			'ignored'    => __( 'Ignored', 'localguard-scanner' ),
			'resolved'   => __( 'Resolved', 'localguard-scanner' ),
		);
		return isset( $labels[ $status ] ) ? $labels[ $status ] : __( 'Unknown', 'localguard-scanner' );
	}

	/**
	 * @param array<string,mixed> $finding Presented finding.
	 * @param bool                $open    Open by default.
	 * @return void
	 */
	private function render_finding( array $finding, bool $open ): void {
		$severity = $finding['severity'];
		$location = $finding['location'];
		echo '<details class="lgs-finding lgs-finding-' . esc_attr( $severity ) . '"' . ( $open ? ' open' : '' ) . '>';
		echo '<summary><span class="lgs-finding-summary">';
		echo '<span class="lgs-finding-badges"><span class="lgs-risk-badge lgs-' . esc_attr( $severity ) . '"><span class="lgs-risk-symbol" aria-hidden="true">' . esc_html( $this->severity_symbol( $severity ) ) . '</span>' . esc_html( $this->severity_label( $severity ) ) . ' · ' . esc_html( (string) $finding['score'] ) . '</span>';
		echo '<span class="lgs-source-badge lgs-source-' . esc_attr( $location['source'] ) . '">' . esc_html( $location['kind_label'] ) . '</span></span>';
		echo '<strong class="lgs-finding-title">' . esc_html( $finding['title'] ) . '</strong>';
		echo '<code class="lgs-finding-locator">' . esc_html( $location['display'] ) . '</code>';
		echo '<span class="lgs-finding-meta"><span>' . esc_html( $this->finding_status_label( $finding['status'] ) ) . '</span><span>';
		/* translators: %d: detector confidence from 0 to 100. */
		echo esc_html( sprintf( __( 'Confidence %d%%', 'localguard-scanner' ), $finding['confidence'] ) ) . '</span>';
		if ( is_multisite() && $finding['site_id'] > 0 ) {
			/* translators: %d: WordPress Multisite site ID. */
			echo '<span>' . esc_html( sprintf( __( 'Site %d', 'localguard-scanner' ), $finding['site_id'] ) ) . '</span>';
		}
		if ( 'resolved' === $finding['status'] ) {
			if ( '' !== $finding['last_seen_at'] ) {
				/* translators: %s: UTC date when the finding was last observed. */
				echo '<span>' . esc_html( sprintf( __( 'Last observed: %s UTC', 'localguard-scanner' ), $finding['last_seen_at'] ) ) . '</span>';
			}
			if ( '' !== $finding['resolved_at'] ) {
				/* translators: %s: UTC date when the finding was resolved. */
				echo '<span>' . esc_html( sprintf( __( 'Resolved: %s UTC', 'localguard-scanner' ), $finding['resolved_at'] ) ) . '</span>';
			}
			if ( $finding['scan_id'] > 0 ) {
				/* translators: %d: scan ID where the finding was last observed. */
				echo '<span>' . esc_html( sprintf( __( 'Last seen in scan #%d', 'localguard-scanner' ), $finding['scan_id'] ) ) . '</span>';
			}
		}
		echo '</span></span></summary>';
		echo '<div class="lgs-finding-body">';
		echo '<section class="lgs-explanation"><h3>' . esc_html__( 'Evidence found', 'localguard-scanner' ) . '</h3>';
		foreach ( $finding['explanations'] as $explanation ) {
			echo '<div class="lgs-evidence-item"><h4>' . esc_html( $explanation['title'] ) . '</h4>';
			echo '<p><strong>' . esc_html__( 'What LocalGuard detected:', 'localguard-scanner' ) . '</strong> ' . esc_html( $explanation['detected'] ) . '</p>';
			echo '<p><strong>' . esc_html__( 'Why it deserves attention:', 'localguard-scanner' ) . '</strong> ' . esc_html( $explanation['importance'] ) . '</p></div>';
		}
		echo '</section>';
		echo '<section class="lgs-action-plan"><h3>' . esc_html__( 'Investigation and response plan', 'localguard-scanner' ) . '</h3>';
		echo '<h4>' . esc_html__( 'Where it is located', 'localguard-scanner' ) . '</h4><p><strong>' . esc_html( $location['kind_label'] ) . ':</strong> <code>' . esc_html( $location['display'] ) . '</code></p>';
		$this->render_step_list( __( 'How to check it safely', 'localguard-scanner' ), $finding['remediation']['inspection'] );
		if ( '' !== $location['sql'] ) {
			echo '<h4>' . esc_html__( 'Read-only SQL query', 'localguard-scanner' ) . '</h4>';
			echo '<p class="description">' . esc_html__( 'Run this SELECT only in a trusted database administration session. Its result may contain sensitive data; LocalGuard does not execute or store the result.', 'localguard-scanner' ) . '</p>';
			echo '<pre class="lgs-sql-query"><code>' . esc_html( $location['sql'] ) . '</code></pre>';
		}
		$this->render_step_list( __( 'What to correct if it is confirmed', 'localguard-scanner' ), $finding['remediation']['correction'] );
		$this->render_step_list( __( 'How to verify the cleanup', 'localguard-scanner' ), $finding['remediation']['verification'] );
		echo '<h4>' . esc_html__( 'When it could be legitimate', 'localguard-scanner' ) . '</h4><ul class="lgs-context-list">';
		foreach ( $finding['explanations'] as $explanation ) {
			if ( '' !== $explanation['context'] ) {
				echo '<li>' . esc_html( $explanation['context'] ) . '</li>';
			}
		}
		echo '</ul>';
		$copy_value = '' !== $location['sql'] ? $location['sql'] : $location['copy_value'];
		if ( '' !== $copy_value && '' !== $location['copy_label'] ) {
			echo '<p class="lgs-copy-row"><button type="button" class="button lgs-copy-value" data-copy-value="' . esc_attr( $copy_value ) . '" hidden>' . esc_html( $location['copy_label'] ) . '</button> <span class="lgs-copy-status" aria-live="polite"></span></p>';
		}
		echo '</section>';
		echo '</div></details>';
	}

	/**
	 * @param string   $title Section title.
	 * @param string[] $steps Safe translated steps.
	 * @return void
	 */
	private function render_step_list( string $title, array $steps ): void {
		echo '<h4>' . esc_html( $title ) . '</h4><ol>';
		foreach ( $steps as $step ) {
			echo '<li>' . esc_html( $step ) . '</li>';
		}
		echo '</ol>';
	}

	/** @return string */
	private function severity_symbol( string $severity ): string {
		$symbols = array(
			'critical' => '!',
			'high'     => '!',
			'medium'   => '▲',
			'low'      => '●',
			'info'     => 'i',
		);
		return isset( $symbols[ $severity ] ) ? $symbols[ $severity ] : 'i';
	}

	/** @return string */
	private function scan_status_key( string $status ): string {
		$allowed = array( 'queued', 'discovering', 'completed', 'partial', 'failed', 'cancelled', 'scanning_files', 'scanning_database', 'classifying' );
		return in_array( $status, $allowed, true ) ? $status : 'queued';
	}

	/** @return void */
	private function render_diagnostics(): void {
		echo '<section class="lgs-diagnostics"><h2>' . esc_html__( 'Local diagnostics', 'localguard-scanner' ) . '</h2>';
		echo '<p>' . esc_html__( 'These checks run on this server only and do not contact external services.', 'localguard-scanner' ) . '</p><ul>';
		foreach ( $this->diagnostics->run( $this->settings->all() ) as $check ) {
			echo '<li><span class="lgs-check lgs-check-' . esc_attr( $check['status'] ) . '" aria-hidden="true"></span><strong>' . esc_html( $this->diagnostic_label( $check['message_key'] ) ) . '</strong>';
			if ( '' !== $check['detail'] ) {
				echo ' <span class="description">(' . esc_html( $check['detail'] ) . ')</span>';
			}
			echo '</li>';
		}
		echo '</ul></section>';
	}

	/** @return void */
	private function render_storage_error( string $title, string $slug ): void {
		$this->header( $title, $slug );
		echo '<div class="notice notice-error inline"><p>' . esc_html__( 'Local scan data could not be read. Review the diagnostics and database permissions.', 'localguard-scanner' ) . '</p></div>';
		$this->render_diagnostics();
		$this->footer();
	}

	/** @param array<string,mixed> $coverage Safe coverage summary. @return void */
	private function render_coverage( array $coverage ): void {
		if ( ! empty( $coverage['structured'] ) && isset( $coverage['scopes'] ) && is_array( $coverage['scopes'] ) ) {
			echo '<h3>' . esc_html__( 'Coverage by area', 'localguard-scanner' ) . '</h3>';
			echo '<p class="description">' . esc_html__( 'A verified area may resolve its previous findings even when another area has incomplete coverage.', 'localguard-scanner' ) . '</p>';
			echo '<ul class="lgs-coverage lgs-coverage-scopes">';
			foreach ( array( 'files', 'database', 'plugins' ) as $scope ) {
				if ( ! isset( $coverage['scopes'][ $scope ] ) || ! is_array( $coverage['scopes'][ $scope ] ) ) {
					continue;
				}
				$summary  = $coverage['scopes'][ $scope ];
				$verified = ! empty( $summary['verified'] );
				echo '<li><strong>' . esc_html( $this->coverage_scope_label( $scope ) ) . ':</strong> ' . esc_html( $verified ? __( 'Verified', 'localguard-scanner' ) : __( 'Incomplete', 'localguard-scanner' ) );
				if ( isset( $summary['reasons'] ) && is_array( $summary['reasons'] ) && ! empty( $summary['reasons'] ) ) {
					echo '<ul>';
					foreach ( $summary['reasons'] as $reason => $count ) {
						echo '<li>' . esc_html( $this->coverage_label( (string) $reason ) ) . ': ' . esc_html( (string) max( 0, (int) $count ) ) . '</li>';
					}
					echo '</ul>';
				}
				echo '</li>';
			}
			echo '</ul>';
			return;
		}

		if ( ! empty( $coverage['involuntary'] ) && is_array( $coverage['involuntary'] ) ) {
			echo '<h3>' . esc_html__( 'Areas that could not be inspected', 'localguard-scanner' ) . '</h3>';
			echo '<p class="description">' . esc_html__( 'The scan tried to read these and failed, so they are unverified.', 'localguard-scanner' ) . '</p><ul class="lgs-coverage">';
			foreach ( $coverage['involuntary'] as $reason => $count ) {
				echo '<li>' . esc_html( $this->coverage_label( (string) $reason ) ) . ': ' . esc_html( (string) max( 0, (int) $count ) ) . '</li>';
			}
			echo '</ul>';
		}
		if ( ! empty( $coverage['deliberate'] ) && is_array( $coverage['deliberate'] ) ) {
			echo '<h3>' . esc_html__( 'Areas skipped by configuration', 'localguard-scanner' ) . '</h3>';
			echo '<p class="description">' . esc_html__( 'Excluded directories are never inspected, so findings inside them keep their previous status instead of being resolved.', 'localguard-scanner' ) . '</p><ul class="lgs-coverage">';
			foreach ( $coverage['deliberate'] as $reason => $count ) {
				echo '<li>' . esc_html( $this->coverage_label( (string) $reason ) ) . ': ' . esc_html( (string) max( 0, (int) $count ) ) . '</li>';
			}
			echo '</ul>';
		}
	}

	/** @return string */
	private function coverage_scope_label( string $scope ): string {
		$labels = array(
			'files'    => __( 'Files', 'localguard-scanner' ),
			'database' => __( 'Database', 'localguard-scanner' ),
			'plugins'  => __( 'Plugin advisory catalog', 'localguard-scanner' ),
		);
		return isset( $labels[ $scope ] ) ? $labels[ $scope ] : __( 'Unknown area', 'localguard-scanner' );
	}

	/** @return string */
	private function coverage_label( string $reason ): string {
		$labels = array(
			'discovering'                  => __( 'Preparing inventory', 'localguard-scanner' ),
			'unreadable_directory'         => __( 'Directory could not be read', 'localguard-scanner' ),
			'unreadable_metadata'          => __( 'File metadata changed or could not be read', 'localguard-scanner' ),
			'unsafe_path_name'             => __( 'Path name could not be represented safely across platforms', 'localguard-scanner' ),
			'excluded_directory'           => __( 'Directory excluded by configuration', 'localguard-scanner' ),
			'unreadable_hash'              => __( 'File could not be hashed safely', 'localguard-scanner' ),
			'hash_size_limit'              => __( 'File exceeded the hashing size limit', 'localguard-scanner' ),
			'symlink_not_followed'         => __( 'Symbolic link was recorded but not followed', 'localguard-scanner' ),
			'unreadable_content'           => __( 'File could not be read safely for deep inspection', 'localguard-scanner' ),
			'deep_scan_size_limit'         => __( 'File exceeded the deep-inspection size limit', 'localguard-scanner' ),
			'core_manifest_unavailable'    => __( 'No exact local WordPress core manifest was available', 'localguard-scanner' ),
			'core_missing_results_limited' => __( 'Missing core-file results exceeded the safe display limit', 'localguard-scanner' ),
			'db_value_size_limit'          => __( 'A large database value was sampled instead of read in full', 'localguard-scanner' ),
		);
		return isset( $labels[ $reason ] ) ? $labels[ $reason ] : __( 'Part of the site could not be fully inspected', 'localguard-scanner' );
	}

	/** @return string */
	private function diagnostic_label( string $key ): string {
		$labels = array(
			'environment_supported'   => __( 'WordPress and PHP versions are supported', 'localguard-scanner' ),
			'environment_unsupported' => __( 'WordPress or PHP version is unsupported', 'localguard-scanner' ),
			'php_recommended'         => __( 'PHP version meets the security recommendation', 'localguard-scanner' ),
			'php_upgrade_recommended' => __( 'PHP 8.3 or newer is recommended', 'localguard-scanner' ),
			'memory_sufficient'       => __( 'PHP memory limit is suitable', 'localguard-scanner' ),
			'memory_constrained'      => __( 'PHP memory is constrained; use smaller batches', 'localguard-scanner' ),
			'cron_available'          => __( 'WP-Cron is available', 'localguard-scanner' ),
			'cron_disabled'           => __( 'WP-Cron is disabled; configure a system cron', 'localguard-scanner' ),
			'cron_event_missing'      => __( 'The daily scan event is not scheduled', 'localguard-scanner' ),
			'cron_recent_failure'     => __( 'A recent automatic scan slice failed safely', 'localguard-scanner' ),
			'files_readable'          => __( 'Local rules and the WordPress root are readable', 'localguard-scanner' ),
			'files_unreadable'        => __( 'Local rules or the WordPress root cannot be read', 'localguard-scanner' ),
			'tables_available'        => __( 'LocalGuard Scanner tables are available', 'localguard-scanner' ),
			'tables_missing'          => __( 'One or more LocalGuard Scanner tables are missing', 'localguard-scanner' ),
		);
		return isset( $labels[ $key ] ) ? $labels[ $key ] : __( 'Diagnostic status unavailable', 'localguard-scanner' );
	}
}
