<?php

declare(strict_types=1);

namespace SecurWP\Modules\Tools;

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

use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use SecurWP\Includes\IpDetector;
use SecurWP\Includes\MasterKey;
use SecurWP\Includes\ServerConfigWriter;
use SecurWP\Includes\SettingsStore;
use SecurWP\Includes\Capabilities;
use SecurWP\Modules\Hardening\Hardening;
use SecurWP\Modules\Cloud\Service\CloudSettings;
use SecurWP\Modules\Firewall\ExtendedProtection\Manager as ExtendedProtectionManager;
use SecurWP\Modules\Firewall\FirewallConfigService;

/**
 * REST API endpoints for the Security Tools module.
 *
 * Registers and handles all tool-related REST routes including salts
 * regeneration, database prefix change, admin ID change, and status retrieval.
 *
 */
class ToolsApi {

	/**
	 * Register all REST API routes for the Tools module.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		// POST /securwp/v1/tools/regenerate-salts -- regenerate salts.
		register_rest_route( 'securwp/v1', '/tools/regenerate-salts', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'handle_regenerate_salts' ],
			'permission_callback' => [ $this, 'check_config_write_permission' ],
		] );

		// POST /securwp/v1/tools/change-prefix -- change DB table prefix.
		register_rest_route( 'securwp/v1', '/tools/change-prefix', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'handle_change_prefix' ],
			'permission_callback' => [ $this, 'check_database_prefix_permission' ],
			'args'                => [
				'new_prefix' => [
					'required'          => true,
					'type'              => 'string',
					'sanitize_callback' => 'sanitize_text_field',
				],
			],
		] );

		// POST /securwp/v1/tools/change-admin-id -- change user ID 1.
		register_rest_route( 'securwp/v1', '/tools/change-admin-id', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'handle_change_admin_id' ],
			'permission_callback' => [ $this, 'check_admin_id_permission' ],
			'args'                => [
				'new_id' => [
					'required'          => true,
					'type'              => 'integer',
					'sanitize_callback' => 'absint',
				],
				'redirect_to' => [
					'required'          => false,
					'type'              => 'string',
					'sanitize_callback' => 'esc_url_raw',
				],
			],
		] );

		// GET /securwp/v1/tools/generate-admin-id -- generate a safe, unused user ID.
		register_rest_route( 'securwp/v1', '/tools/generate-admin-id', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'handle_generate_admin_id' ],
			'permission_callback' => [ $this, 'check_user_management_permission' ],
		] );

		// POST /securwp/v1/tools/rotate-encryption-key -- rotate encryption key.
		register_rest_route( 'securwp/v1', '/tools/rotate-encryption-key', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'handle_rotate_encryption_key' ],
			'permission_callback' => [ $this, 'check_key_rotation_permission' ],
		] );

		// GET /securwp/v1/tools/status -- get current status of tools.
		register_rest_route( 'securwp/v1', '/tools/status', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'handle_status' ],
			'permission_callback' => [ $this, 'check_permission' ],
		] );

		// GET /securwp/v1/tools/diagnostics -- operational diagnostics (scanner pipeline excluded).
		register_rest_route( 'securwp/v1', '/tools/diagnostics', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'handle_diagnostics' ],
			'permission_callback' => [ $this, 'check_permission' ],
		] );
	}

	/**
	 * Permission check for all tools endpoints.
	 *
	 * @return bool True if the current user can manage SecurWP.
	 */
	public function check_permission(): bool {
		return Capabilities::can_manage_plugin();
	}

	/**
	 * Require access to mutate wp-config.php or plugin-owned secrets.
	 */
	public function check_config_write_permission(): bool {
		return $this->check_permission()
			&& function_exists( 'current_user_can' )
			&& current_user_can( 'update_core' );
	}

	/**
	 * Require wp-config rights only when the active key source is wp-config.php.
	 */
	public function check_key_rotation_permission(): bool {
		if ( MasterKey::get_source() === 'constant' ) {
			return $this->check_config_write_permission();
		}

		return $this->check_permission();
	}

	/**
	 * Require network-level access before mutating shared database structure.
	 */
	public function check_database_prefix_permission(): bool {
		if ( is_multisite() && ! current_user_can( 'manage_network_options' ) ) {
			return false;
		}

		return $this->check_config_write_permission();
	}

	/**
	 * Require user-management privileges.
	 */
	public function check_user_management_permission(): bool {
		if ( ! $this->check_permission() || ! function_exists( 'current_user_can' ) ) {
			return false;
		}

		if ( is_multisite() && ! current_user_can( 'manage_network_users' ) ) {
			return false;
		}

		return current_user_can( 'edit_users' );
	}

	/**
	 * Require permission to edit the account being migrated.
	 */
	public function check_admin_id_permission(): bool {
		return $this->check_user_management_permission()
			&& Capabilities::can_edit_user_account( 1 );
	}

	/**
	 * Handle the salts regeneration request.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response
	 */
	public function handle_regenerate_salts( WP_REST_Request $request ): WP_REST_Response {
		$salts  = new SaltsRegeneration();
		$result = $salts->regenerate();

		if ( is_wp_error( $result ) ) {
			$status = in_array(
				$result->get_error_code(),
				[ 'writes_disabled', 'config_not_found', 'not_writable', 'dir_not_writable' ],
				true
			) ? 400 : 500;

			return new WP_REST_Response(
				[
					'success' => false,
					'message' => $result->get_error_message(),
					'error'   => $result->get_error_message(),
					'code'    => $result->get_error_code(),
				],
				$status
			);
		}

		do_action( 'securwp_tools_action_completed', 'regenerate_salts' );

		return new WP_REST_Response( [
			'success' => true,
			'message' => 'Security salts regenerated successfully. All existing sessions have been invalidated.',
		] );
	}

	/**
	 * Handle the database prefix change request.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response
	 */
	public function handle_change_prefix( WP_REST_Request $request ): WP_REST_Response {
		if ( is_multisite() && ! current_user_can( 'manage_network_options' ) ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => 'Network Administrator permission is required to change the database prefix in multisite.',
					'error'   => 'Network Administrator permission is required to change the database prefix in multisite.',
					'code'    => 'insufficient_network_permissions',
				],
				403
			);
		}

		$new_prefix = $request->get_param( 'new_prefix' );
		if ( empty( $new_prefix ) ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => 'The new_prefix parameter is required.',
					'error'   => 'The new_prefix parameter is required.',
				],
				400
			);
		}

		$changer = new DatabasePrefixChanger();
		$result  = $changer->change( $new_prefix );

		if ( is_wp_error( $result ) ) {
			$status = in_array(
				$result->get_error_code(),
				[
					'invalid_prefix',
					'same_prefix',
					'writes_disabled',
					'no_tables',
					'table_collision',
					'locked',
					'config_not_found',
					'config_not_writable',
					'dir_not_writable',
					'config_pattern_not_found',
				],
				true
			) ? 400 : 500;

			return new WP_REST_Response(
				[
					'success' => false,
					'message' => $result->get_error_message(),
					'error'   => $result->get_error_message(),
					'code'    => $result->get_error_code(),
				],
				$status
			);
		}

		do_action( 'securwp_tools_action_completed', 'change_prefix' );
		$warnings = $changer->get_last_warnings();

		return new WP_REST_Response( [
			'success'    => true,
			'message'    => empty( $warnings )
				? 'Database prefix changed successfully to "' . $new_prefix . '".'
				: 'Database prefix changed to "' . $new_prefix . '" with review warnings.',
			'new_prefix' => $new_prefix,
			'warnings'   => $warnings,
		] );
	}

	/**
	 * Handle the admin user ID change request.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response
	 */
	public function handle_change_admin_id( WP_REST_Request $request ): WP_REST_Response {
		$new_id          = (int) $request->get_param( 'new_id' );
		$reauth_required = 1 === get_current_user_id();
		if ( $new_id < 1 ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'error'   => 'The new_id parameter is required and must be a positive integer.',
				],
				400
			);
		}

		$changer = new AdminIdChanger();
		$result  = $changer->change( $new_id );

		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'error'   => $result->get_error_message(),
					'code'    => $result->get_error_code(),
				],
				400
			);
		}

		do_action( 'securwp_tools_action_completed', 'change_admin_id' );

		return new WP_REST_Response( [
			'success'         => true,
			'message'         => $reauth_required
				? 'Admin user ID changed from 1 to ' . $new_id . '. Your current session has been signed out. Please log in again.'
				: 'Admin user ID changed from 1 to ' . $new_id . '.',
			'new_id'          => $new_id,
			'reauth_required' => $reauth_required,
			'login_url'       => $reauth_required
				? wp_login_url( $this->resolve_post_login_redirect( $request->get_param( 'redirect_to' ) ) )
				: null,
		] );
	}

	/**
	 * Resolve a safe post-login redirect target for reauthentication flows.
	 *
	 * @param mixed $redirect_to Requested redirect target.
	 * @return string
	 */
	private function resolve_post_login_redirect( $redirect_to ): string {
		$default_redirect = admin_url();

		if ( ! is_string( $redirect_to ) || '' === $redirect_to ) {
			return $default_redirect;
		}

		$validated_redirect = wp_validate_redirect( $redirect_to, $default_redirect );

		return is_string( $validated_redirect ) && '' !== $validated_redirect
			? $validated_redirect
			: $default_redirect;
	}

	/**
	 * Handle the generate admin ID request.
	 *
	 * Returns a safe, unused user ID starting from the next sequential ID.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response
	 */
	public function handle_generate_admin_id( WP_REST_Request $request ): WP_REST_Response {
		$changer = new AdminIdChanger();

		return new WP_REST_Response( [
			'success'      => true,
			'suggested_id' => $changer->generate_safe_id(),
		] );
	}

	/**
	 * Handle the encryption key rotation request.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response
	 */
	public function handle_rotate_encryption_key( WP_REST_Request $request ): WP_REST_Response {
		$rotation = new KeyRotation();
		$result   = $rotation->rotate();

		if ( is_wp_error( $result ) ) {
			$status = in_array(
				$result->get_error_code(),
				[ 'writes_disabled', 'config_not_found', 'config_not_writable', 'dir_not_writable' ],
				true
			) ? 400 : 500;

			return new WP_REST_Response(
				[
					'success' => false,
					'error'   => $result->get_error_message(),
					'code'    => $result->get_error_code(),
				],
				$status
			);
		}

		do_action( 'securwp_tools_action_completed', 'rotate_encryption_key' );

		return new WP_REST_Response( [
			'success' => true,
			'message' => 'Encryption key rotated successfully. All secrets have been re-encrypted.',
			'source'  => $result['source'],
		] );
	}

	/**
	 * Handle the tools status request.
	 *
	 * Returns current database prefix, whether user ID 1 exists,
	 * wp-config.php writability, and salt age info.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response
	 */
	public function handle_status( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		// Current database prefix.
		$current_prefix = $wpdb->prefix;

		// Whether user ID 1 exists.
		$user_id_1_exists = (bool) get_user_by( 'id', 1 );

		// wp-config.php status (resolved path + global write policy).
		$config_status      = ConfigFilePolicy::get_wp_config_status();
		$wp_config_path     = $config_status['path'];
		$wp_config_writable = (bool) $config_status['writable'];
		$wp_config_modified = ( is_string( $wp_config_path ) && file_exists( $wp_config_path ) ) ? filemtime( $wp_config_path ) : null;

		// Encryption key source.
		$key_source = \SecurWP\Includes\MasterKey::get_source();

		return new WP_REST_Response( [
			'success' => true,
			'data'    => [
				'db_prefix'           => $current_prefix,
				'user_id_1_exists'    => $user_id_1_exists,
				'wp_config_writable'  => $wp_config_writable,
				'wp_config_modified'  => $wp_config_modified,
				'write_to_files'      => (bool) $config_status['write_enabled'],
				'wp_config_age_human' => $wp_config_modified
					? human_time_diff( $wp_config_modified, time() ) . ' ago'
					: null,
				'key_source'          => $key_source,
			],
		] );
	}

	/**
	 * Handle diagnostics request.
	 *
	 * Builds a security-plugin-style diagnostics payload focused on operational and
	 * security readiness. Scanner pipeline internals are intentionally excluded.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function handle_diagnostics( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$now           = time();
		$hardening     = Hardening::get_saved_settings();
		$firewall      = FirewallConfigService::load_effective_settings();
		$cloud         = CloudSettings::get_all();
		$config_status = ConfigFilePolicy::get_wp_config_status();

		$sections = [];

		$php_version            = PHP_VERSION;
		$php_sapi               = (string) php_sapi_name();
		$php_ok                 = version_compare( $php_version, '8.1.0', '>=' );
		$php_memory_limit       = (string) ini_get( 'memory_limit' );
		$process_owner          = $this->detect_process_owner();
		$openssl_available      = function_exists( 'openssl_verify' );
		$openssl_version_text   = defined( 'OPENSSL_VERSION_TEXT' ) ? (string) OPENSSL_VERSION_TEXT : 'Unknown';
		$curl_available         = is_callable( 'curl_version' );
		$curl_info              = $curl_available ? (array) curl_version() : [];
		$curl_version           = (string) ( $curl_info['version'] ?? 'Unavailable' );
		$curl_version_number    = isset( $curl_info['version_number'] ) ? (int) $curl_info['version_number'] : 0;
		$curl_host              = (string) ( $curl_info['host'] ?? 'Unknown' );
		$curl_features_code     = isset( $curl_info['features'] ) ? (int) $curl_info['features'] : 0;
		$curl_ssl_version       = (string) ( $curl_info['ssl_version'] ?? 'Unknown' );
		$curl_protocols         = isset( $curl_info['protocols'] ) && is_array( $curl_info['protocols'] )
			? $this->format_curl_protocols( $curl_info['protocols'] )
			: 'Unavailable';
		$db_version             = (string) $wpdb->db_version();
		if ( $db_version === '' ) {
			$db_version = 'Unknown';
		}
		$display_errors_enabled = $this->ini_flag_enabled( (string) ini_get( 'display_errors' ) );

		$sections[] = [
			'id'          => 'runtime',
			'title'       => 'Runtime Environment',
			'description' => 'Core PHP, WordPress, and extension health checks.',
			'checks'      => [
				$this->build_check(
					'php_version',
					'PHP version',
					$php_ok ? 'ok' : 'warning',
					$php_version . ( $php_sapi !== '' ? ' (' . $php_sapi . ')' : '' ),
					$php_ok ? 'Meets recommended baseline (8.1+).' : 'Below recommended baseline (8.1+).'
				),
				$this->build_check(
					'php_memory_limit',
					'PHP memory limit',
					'info',
					$php_memory_limit !== '' ? $php_memory_limit : 'Unknown',
					'Configured PHP memory ceiling.'
				),
				$this->build_check(
					'process_owner',
					'Process owner',
					$process_owner === 'Unknown' ? 'warning' : 'info',
					$process_owner,
					'Effective OS user executing PHP.'
				),
				$this->build_check(
					'wordpress_version',
					'WordPress version',
					'info',
					get_bloginfo( 'version' ),
					'Current application version.'
				),
				$this->build_check(
					'database_version',
					'Database version',
					'info',
					$db_version,
					'Database engine version reported by WordPress.'
				),
				$this->build_check(
					'curl_support',
					'cURL support',
					$curl_available ? 'ok' : 'critical',
					$curl_available
						? $curl_version . ( $curl_version_number > 0 ? ' (0x' . dechex( $curl_version_number ) . ')' : '' )
						: 'Unavailable',
					$curl_available ? 'Outbound HTTP transport available.' : 'Required for stable external API connectivity.'
				),
				$this->build_check(
					'openssl_support',
					'OpenSSL support',
					$openssl_available ? 'ok' : 'critical',
					$openssl_available ? 'Available' : 'Unavailable',
					$openssl_available ? 'TLS cryptography is available.' : 'TLS features are not available in this PHP runtime.'
				),
				$this->build_check(
					'openssl_version',
					'OpenSSL version',
					$openssl_available ? 'info' : 'critical',
					$openssl_available ? $openssl_version_text : 'Unavailable',
					'OpenSSL runtime version used by PHP.'
				),
				$this->build_check(
					'curl_ssl_version',
					'cURL SSL backend',
					$curl_available ? 'info' : 'critical',
					$curl_available ? $curl_ssl_version : 'Unavailable',
					'SSL backend reported by cURL.'
				),
				$this->build_check(
					'curl_host',
					'cURL host target',
					$curl_available ? 'info' : 'critical',
					$curl_available ? $curl_host : 'Unavailable',
					'Host tuple reported by cURL build.'
				),
				$this->build_check(
					'curl_features',
					'cURL features code',
					$curl_available ? 'info' : 'critical',
					$curl_available ? '0x' . dechex( $curl_features_code ) : 'Unavailable',
					'Bitmask of cURL build capabilities.'
				),
				$this->build_check(
					'curl_protocols',
					'cURL supported protocols',
					$curl_available ? 'info' : 'critical',
					$curl_protocols,
					'Protocols available to cURL in this runtime.'
				),
				$this->build_check(
					'display_errors',
					'PHP display_errors',
					$display_errors_enabled ? 'warning' : 'ok',
					$display_errors_enabled ? 'Enabled' : 'Disabled',
					$display_errors_enabled ? 'Disable on production to avoid leaking runtime details.' : 'Production-safe.'
				),
			],
		];

		$wp_debug         = defined( 'WP_DEBUG' ) && WP_DEBUG;
		$wp_debug_log     = defined( 'WP_DEBUG_LOG' ) ? WP_DEBUG_LOG : false;
		$wp_debug_display = defined( 'WP_DEBUG_DISPLAY' ) ? WP_DEBUG_DISPLAY : true;

		$disallow_file_edit = ( defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT ) || ! empty( $hardening['disable_file_editor'] );
		$xmlrpc_mode         = (string) ( $hardening['disable_xmlrpc'] ?? Hardening::get_default_settings()['disable_xmlrpc'] );
		$login_limit_enabled = ! empty( $hardening['limit_login_attempts_enabled'] );

		$wp_debug_log_state = 'Disabled';
		if ( $wp_debug_log === true ) {
			$wp_debug_log_state = 'Enabled (' . WP_CONTENT_DIR . '/debug.log)';
		} elseif ( is_string( $wp_debug_log ) && $wp_debug_log !== '' ) {
			$wp_debug_log_state = 'Enabled (' . $wp_debug_log . ')';
		}

		$sections[] = [
			'id'          => 'wordpress_security',
			'title'       => 'WordPress Security Posture',
			'description' => 'Production safety flags and hardening controls.',
			'checks'      => [
				$this->build_check(
					'wp_debug',
					'WP_DEBUG',
					$wp_debug ? 'warning' : 'ok',
					$wp_debug ? 'Enabled' : 'Disabled',
					$wp_debug ? 'Debug mode should be disabled on production.' : 'Production-safe.'
				),
				$this->build_check(
					'wp_debug_log',
					'WP_DEBUG_LOG',
					$wp_debug ? 'warning' : 'info',
					$wp_debug_log_state,
					$wp_debug ? 'Debug logging is active while WP_DEBUG is enabled.' : 'Debug logging state.'
				),
				$this->build_check(
					'wp_debug_display',
					'WP_DEBUG_DISPLAY',
					$wp_debug && (bool) $wp_debug_display ? 'warning' : 'ok',
					$wp_debug_display ? 'Enabled' : 'Disabled',
					$wp_debug && (bool) $wp_debug_display ? 'Visible runtime errors can leak sensitive details.' : 'Runtime errors are not shown publicly.'
				),
				$this->build_check(
					'file_editor_lock',
					'Theme/Plugin editor lock',
					$disallow_file_edit ? 'ok' : 'warning',
					$disallow_file_edit ? 'Locked' : 'Unlocked',
					$disallow_file_edit ? 'In-dashboard editor is disabled.' : 'Enable DISALLOW_FILE_EDIT or SecurWP file editor hardening.'
				),
				$this->build_check(
					'xmlrpc_mode',
					'XML-RPC hardening',
					$xmlrpc_mode === 'enable' ? 'warning' : 'ok',
					$xmlrpc_mode,
					$xmlrpc_mode === 'enable' ? 'XML-RPC remains fully enabled.' : 'XML-RPC hardening is active.'
				),
				$this->build_check(
					'login_rate_limit',
					'Login attempt limiting',
					$login_limit_enabled ? 'ok' : 'warning',
					$login_limit_enabled
						? sprintf(
							'Enabled (%d attempts / %ds)',
							(int) ( $hardening['limit_login_attempts_max'] ?? 5 ),
							(int) ( $hardening['limit_login_attempts_window'] ?? 300 )
						)
						: 'Disabled',
					$login_limit_enabled ? 'Brute-force protection is active.' : 'Enable login attempt limits to reduce brute-force risk.'
				),
			],
		];

		$firewall_enabled = FirewallConfigService::is_origin_enforcement_active( $firewall );
		$firewall_mode    = FirewallConfigService::get_origin_enforcement_mode( $firewall );
		switch ( $firewall_mode ) {
			case FirewallConfigService::MODE_ENFORCE:
				$firewall_label  = 'Enabled';
				$firewall_detail = 'Runtime request filtering is active.';
				break;
			case FirewallConfigService::MODE_PENDING_VALIDATION:
				$firewall_label  = 'Pending validation';
				$firewall_detail = 'Firewall is enabled, but origin enforcement is waiting for IP validation in a real browser request.';
				break;
			case FirewallConfigService::MODE_MONITOR:
				$firewall_label  = 'Safe mode';
				$firewall_detail = 'Origin enforcement is paused because visitor IP detection is not reliable yet.';
				break;
			default:
				$firewall_label  = 'Disabled';
				$firewall_detail = 'Firewall protections are currently disabled.';
				break;
		}
		$proxy_mode       = (string) ( $firewall['proxy_detection'] ?? 'automatic' );
		$proxy_header     = (string) ( $firewall['proxy_header'] ?? '' );
		$proxy_value      = $proxy_mode;
		if ( $proxy_mode === 'manual' ) {
			$proxy_value .= $proxy_header !== '' ? ' (' . $proxy_header . ')' : ' (header missing)';
		}
		$proxy_warning = $proxy_mode === 'manual' && $proxy_header === '';
		$ip_readiness  = get_transient( IpDetector::READINESS_TRANSIENT );
		$proxy_detail  = $proxy_warning
			? 'Manual mode requires a trusted header selection.'
			: 'Proxy/IP detection configuration is valid.';
		if ( is_array( $ip_readiness ) && isset( $ip_readiness['ready'] ) && empty( $ip_readiness['ready'] ) ) {
			$proxy_warning = true;
			$proxy_detail  = sanitize_text_field( (string) ( $ip_readiness['reason'] ?? $proxy_detail ) );
		}

		$cloud_has_creds = ! empty( $cloud['site_token'] ) && ! empty( $cloud['site_secret'] );
		$license_status  = (string) ( $cloud['license_status'] ?? '' );
		$license_plan    = (string) ( $cloud['license_plan'] ?? 'free' );
		$console_enabled = ! empty( $cloud['console_enabled'] );
		$cloud_level     = ( $cloud_has_creds && in_array( $license_status, [ 'active', 'valid' ], true ) )
			? 'ok'
			: 'warning';
		$cloud_value = $cloud_has_creds
			? sprintf( 'Connected (%s / %s)', $license_plan, $license_status !== '' ? $license_status : 'unknown' )
			: 'Not connected';

		$waf_checks = [];
		if ( class_exists( '\SecurWP\Modules\Firewall\ExtendedProtection\Manager' ) ) {
			$extended = ExtendedProtectionManager::get_status();
			$active   = ! empty( $extended['active'] );
			$bootstrap_exists = ! empty( $extended['bootstrap_exists'] );
			$config_exists    = ! empty( $extended['config_exists'] );
			$waf_level        = $active ? 'ok' : 'warning';

			if ( $active && ( ! $bootstrap_exists || ! $config_exists ) ) {
				$waf_level = 'critical';
			}

			$waf_checks[] = $this->build_check(
				'extended_waf',
				'Extended WAF bootstrap',
				$waf_level,
				$active ? 'Active' : 'Inactive',
				$active
					? 'auto_prepend_file points to SecurWP bootstrap.'
					: 'Extended WAF is not currently active.'
			);
			$waf_checks[] = $this->build_check(
				'extended_waf_server',
				'Extended WAF server mode',
				'info',
				(string) ( $extended['server_type'] ?? 'unknown' ),
				'Detected server profile for bootstrap mode.'
			);
		} else {
			$waf_checks[] = $this->build_check(
				'extended_waf',
				'Extended WAF bootstrap',
				'info',
				'Unavailable',
				'Extended WAF manager class is not loaded in this runtime.'
			);
		}

		$sections[] = [
			'id'          => 'firewall_cloud',
			'title'       => 'Firewall & Console Connectivity',
			'description' => 'Firewall enforcement and SecurWP Console link status.',
			'checks'      => array_merge(
				[
					$this->build_check(
						'firewall_enabled',
						'Firewall module',
						$firewall_enabled ? 'ok' : 'warning',
						$firewall_label,
						$firewall_detail
					),
					$this->build_check(
						'proxy_detection',
						'Real IP detection mode',
						$proxy_warning ? 'warning' : 'ok',
						$proxy_value,
						$proxy_detail
					),
					$this->build_check(
						'console_link',
						'Console link status',
						$cloud_level,
						$cloud_value,
						$cloud_has_creds
							? ( $console_enabled ? 'Remote management commands are enabled.' : 'Credentials present, but remote command channel is disabled.' )
							: 'Connect this site to Console for centralized policy and license sync.'
					),
				],
				$waf_checks
			),
		];

		$wp_config_path     = is_string( $config_status['path'] ) ? $config_status['path'] : '';
		$wp_config_exists   = ! empty( $config_status['file_exists'] );
		$wp_config_writable = ! empty( $config_status['writable'] );
		$write_enabled      = ! empty( $config_status['write_enabled'] );
		$wp_config_level    = ! $wp_config_exists ? 'critical' : 'ok';
		$wp_config_message  = ! $wp_config_exists
			? 'wp-config.php could not be resolved through WordPress path rules.'
			: ( $wp_config_writable
				? 'Writable for managed updates.'
				: 'Read-only is a hardened state. SecurWP will use manual mode for wp-config changes.' );

		$filesystem_checks = [
			$this->build_check(
				'file_write_policy',
				'SecurWP file write policy',
				$write_enabled ? 'ok' : 'warning',
				$write_enabled ? 'Automatic writes enabled' : 'Manual-only mode',
				$write_enabled ? 'Automatic file operations are allowed.' : 'Automatic writes are disabled; manual rule mode is required.'
			),
			$this->build_check(
				'wp_config_status',
				'wp-config.php access',
				$wp_config_level,
				$wp_config_exists ? ( $wp_config_writable ? 'Writable' : 'Read-only' ) : 'Missing',
				$wp_config_exists
					? ( $wp_config_path !== '' ? $wp_config_path : $wp_config_message )
					: $wp_config_message
			),
			$this->build_check(
				'wp_content_writable',
				'wp-content writable',
				wp_is_writable( WP_CONTENT_DIR ) ? 'ok' : 'warning',
				( wp_is_writable( WP_CONTENT_DIR ) ? 'Writable' : 'Read-only' ) . ' (' . WP_CONTENT_DIR . ')',
				WP_CONTENT_DIR
			),
		];

		$server_rule_state        = ServerConfigWriter::get_state();
		$server_config_path       = is_string( $server_rule_state['managed_target_path'] ?? null ) ? (string) $server_rule_state['managed_target_path'] : '';
		$server_inspection_path   = is_string( $server_rule_state['inspection_path'] ?? null ) ? (string) $server_rule_state['inspection_path'] : '';
		$server_config_exists     = ! empty( $server_rule_state['file_exists'] );
		$server_config_writable   = ! empty( $server_rule_state['writable'] );
		$server_verification      = is_array( $server_rule_state['verification'] ?? null ) ? (array) $server_rule_state['verification'] : [];
		$verification_status      = is_string( $server_verification['status'] ?? null ) ? (string) $server_verification['status'] : 'pending';
		$verification_reason      = is_string( $server_verification['reason'] ?? null ) ? (string) $server_verification['reason'] : '';
		$verification_status_code = isset( $server_verification['status_code'] ) ? (int) $server_verification['status_code'] : 0;
		$verification_level       = 'info';
		$verification_value       = 'Pending';
		$verification_message     = is_string( $server_verification['message'] ?? null ) ? (string) $server_verification['message'] : 'Activation has not been verified yet.';

		switch ( $verification_status ) {
			case 'passed':
				$verification_level = 'ok';
				if ( $verification_reason === 'persisted_config' ) {
					$verification_value = 'Applied';
				} else {
					$verification_value = 'Verified';
				}
				break;
			case 'partial':
				$verification_level = 'ok';
				$verification_value = 'Verified (partial coverage)';
				break;
			case 'failed':
				$verification_level = 'critical';
				$verification_value = 'Activation failed';
				break;
			case 'unavailable':
				$verification_level = 'warning';
				$verification_value = 'Unavailable';
				break;
			case 'unsupported':
				$verification_value = 'Unsupported';
				break;
			case 'not_required':
				$verification_value = 'Not required';
				break;
			default:
				$verification_value = 'Pending';
				break;
		}

		$server_config_level = ! empty( $server_rule_state['desired_rules_present'] )
			? ( $server_config_writable ? 'ok' : 'warning' )
			: 'info';
		$server_config_value = $server_config_exists
			? ( $server_config_writable ? 'Writable' : 'Read-only' )
			: 'Not present';
		$server_config_note  = $server_config_path !== ''
			? $server_config_path
			: ( $server_inspection_path !== ''
				? 'SecurWP is inspecting ' . $server_inspection_path . ' for existing managed rules, but no automatic server config target is available for this environment.'
				: 'SecurWP could not resolve a server config target for this environment.' );

		$filesystem_checks[] = $this->build_check(
			'server_config_file',
			'Hardening server config target',
			$server_config_level,
			$server_config_value . ( $server_config_path !== '' ? ' (' . $server_config_path . ')' : '' ),
			$server_config_note
		);
		$server_rule_mode = (string) ( $server_rule_state['mode'] ?? 'none' );
		switch ( $server_rule_mode ) {
			case 'persisted':
				$server_rule_level = 'ok';
				break;
			case 'none':
				$server_rule_level = 'info';
				break;
			case 'sync_pending':
			case 'repair_pending':
			case 'manual_only':
			case 'unsupported_server':
				$server_rule_level = 'warning';
				break;
			default:
				$server_rule_level = 'critical';
				break;
		}
		$filesystem_checks[] = $this->build_check(
			'server_rule_state',
			'Hardening server rule state',
			$server_rule_level,
			$server_rule_mode,
			(string) ( $server_rule_state['message'] ?? 'Unknown server rule state.' )
		);
		$filesystem_checks[] = $this->build_check(
			'server_rule_verification',
			'Hardening activation verification',
			$verification_level,
			$verification_value,
			$verification_message
		);

		$sections[] = [
			'id'          => 'filesystem',
			'title'       => 'Filesystem Access',
			'description' => 'Config file access and write policy checks.',
			'checks'      => $filesystem_checks,
		];

		$settings_table  = $wpdb->prefix . 'securwp_settings';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLES verifies that the plugin's settings table exists.
		$settings_exists = (string) $wpdb->get_var(
			$wpdb->prepare( 'SHOW TABLES LIKE %s', $settings_table )
		) === $settings_table;
		$table_like         = $wpdb->esc_like( $wpdb->prefix . 'securwp_' ) . '%';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLES enumerates plugin-owned tables for the diagnostic report.
		$securwp_tables    = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_like ) );
		$securwp_table_cnt = is_array( $securwp_tables ) ? count( $securwp_tables ) : 0;

		$sections[] = [
			'id'          => 'database',
			'title'       => 'Database Integrity',
			'description' => 'Prefix, schema, and SecurWP table checks.',
			'checks'      => [
				$this->build_check(
					'db_prefix',
					'Database prefix',
					$wpdb->prefix === 'wp_' ? 'warning' : 'ok',
					$wpdb->prefix,
					$wpdb->prefix === 'wp_'
						? 'Default prefix is easier to target in automated attacks.'
						: 'Custom prefix is in use.'
				),
				$this->build_check(
					'settings_table',
					'SecurWP settings table',
					$settings_exists ? 'ok' : 'critical',
					$settings_exists ? 'Present' : 'Missing',
					$settings_exists ? $settings_table : 'Core plugin table is missing.'
				),
				$this->build_check(
					'securwp_tables_count',
					'SecurWP table count',
					$securwp_table_cnt > 0 ? 'ok' : 'warning',
					(string) $securwp_table_cnt,
					$securwp_table_cnt > 0 ? 'Detected active SecurWP tables.' : 'No SecurWP tables were detected.'
				),
			],
		];

		$disable_wp_cron = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
		$cron_array      = _get_cron_array();
		$overdue_jobs    = 0;
		if ( is_array( $cron_array ) ) {
			foreach ( $cron_array as $timestamp => $hooks ) {
				if ( ! is_numeric( $timestamp ) || $timestamp >= ( $now - 1800 ) ) {
					continue;
				}
				if ( is_array( $hooks ) ) {
					foreach ( $hooks as $instances ) {
						if ( is_array( $instances ) ) {
							$overdue_jobs += count( $instances );
						}
					}
				}
			}
		}

		$loopback_result = wp_remote_get(
			home_url( '/wp-login.php' ),
			[
				'timeout'     => 8,
				'redirection' => 2,
			]
		);

		$loopback_level   = 'ok';
		$loopback_value   = 'HTTP 200';
		$loopback_message = 'Local loopback HTTP request succeeded.';

		if ( is_wp_error( $loopback_result ) ) {
			$loopback_level   = 'warning';
			$loopback_value   = 'Request failed';
			$loopback_message = $loopback_result->get_error_message();
		} else {
			$code = (int) wp_remote_retrieve_response_code( $loopback_result );
			$loopback_value = 'HTTP ' . $code;
			if ( $code < 200 || $code >= 400 ) {
				$loopback_level   = 'warning';
				$loopback_message = 'Loopback request did not return a successful response.';
			}
		}

		$debug_log_path = WP_CONTENT_DIR . '/debug.log';
		$debug_log_size = file_exists( $debug_log_path ) ? (int) filesize( $debug_log_path ) : 0;
		$debug_log_mb   = $debug_log_size > 0 ? round( $debug_log_size / 1024 / 1024, 2 ) : 0.0;
		$debug_log_lvl  = ( $wp_debug && $debug_log_size > 5 * 1024 * 1024 ) ? 'warning' : 'info';
		$debug_log_val  = file_exists( $debug_log_path ) ? sprintf( '%.2f MB', $debug_log_mb ) : 'Not present';
		$debug_log_msg  = file_exists( $debug_log_path ) ? $debug_log_path : 'Debug log file is not present.';

		$sections[] = [
			'id'          => 'jobs_logs',
			'title'       => 'Jobs & Logging',
			'description' => 'Cron execution and log health checks.',
			'checks'      => [
				$this->build_check(
					'wp_cron_flag',
					'DISABLE_WP_CRON',
					$disable_wp_cron ? 'info' : 'ok',
					$disable_wp_cron ? 'Enabled' : 'Disabled',
					$disable_wp_cron
						? 'WordPress cron is disabled; server-level cron must trigger wp-cron.php.'
						: 'WordPress cron runner is enabled.'
				),
				$this->build_check(
					'overdue_cron_jobs',
					'Overdue cron jobs (>30m)',
					$overdue_jobs > 0 ? 'warning' : 'ok',
					(string) $overdue_jobs,
					$overdue_jobs > 0 ? 'Some scheduled tasks are overdue.' : 'No overdue cron tasks detected.'
				),
				$this->build_check(
					'loopback_http',
					'Loopback HTTP request',
					$loopback_level,
					$loopback_value,
					$loopback_message
				),
				$this->build_check(
					'debug_log_file',
					'wp-content/debug.log',
					$debug_log_lvl,
					$debug_log_val,
					$debug_log_msg
				),
			],
		];

		return new WP_REST_Response(
			[
				'success' => true,
				'data'    => [
					'generated_at' => $now,
					'summary'      => $this->build_summary( $sections ),
					'sections'     => $sections,
				],
			]
		);
	}

	/**
	 * Build diagnostics summary by severity.
	 *
	 * @param array<int,array<string,mixed>> $sections Section list.
	 * @return array<string,int>
	 */
	private function build_summary( array $sections ): array {
		$summary = [
			'ok'       => 0,
			'warning'  => 0,
			'critical' => 0,
			'info'     => 0,
			'total'    => 0,
		];

		foreach ( $sections as $section ) {
			$checks = is_array( $section['checks'] ?? null ) ? $section['checks'] : [];
			foreach ( $checks as $check ) {
				$level = (string) ( $check['level'] ?? 'info' );
				if ( ! isset( $summary[ $level ] ) ) {
					$level = 'info';
				}
				$summary[ $level ]++;
				$summary['total']++;
			}
		}

		return $summary;
	}

	/**
	 * Create a normalized diagnostics check row.
	 *
	 * @param string $id      Check identifier.
	 * @param string $label   Display label.
	 * @param string $level   ok|warning|critical|info.
	 * @param string $value   Current value.
	 * @param string $message Supporting message.
	 * @return array<string,string>
	 */
	private function build_check( string $id, string $label, string $level, string $value, string $message ): array {
		$normalized_level = in_array( $level, [ 'ok', 'warning', 'critical', 'info' ], true ) ? $level : 'info';

		return [
			'id'      => $id,
			'label'   => $label,
			'level'   => $normalized_level,
			'value'   => $value,
			'message' => $message,
		];
	}

	/**
	 * Resolve effective OS user for the current PHP process.
	 *
	 * @return string
	 */
	private function detect_process_owner(): string {
		if ( function_exists( 'posix_geteuid' ) && function_exists( 'posix_getpwuid' ) ) {
			$uid = posix_geteuid();
			if ( is_int( $uid ) ) {
				$pw = posix_getpwuid( $uid );
				if ( is_array( $pw ) && ! empty( $pw['name'] ) ) {
					return (string) $pw['name'];
				}
			}
		}

		$env_user = getenv( 'USERNAME' );
		if ( ! is_string( $env_user ) || $env_user === '' ) {
			$env_user = getenv( 'USER' );
		}
		if ( is_string( $env_user ) && $env_user !== '' ) {
			return $env_user;
		}

		if ( function_exists( 'get_current_user' ) ) {
			$current_user = get_current_user();
			if ( is_string( $current_user ) && $current_user !== '' ) {
				return $current_user;
			}
		}

		if ( ! empty( $_SERVER['LOGON_USER'] ) && is_string( $_SERVER['LOGON_USER'] ) ) {
			return sanitize_text_field( wp_unslash( $_SERVER['LOGON_USER'] ) );
		}

		return 'Unknown';
	}

	/**
	 * Compress cURL protocol list for concise diagnostics rendering.
	 *
	 * @param array<int,string> $protocols Protocol list from curl_version().
	 * @return string
	 */
	private function format_curl_protocols( array $protocols ): string {
		if ( empty( $protocols ) ) {
			return 'Unavailable';
		}

		$visible = array_slice( array_values( $protocols ), 0, 10 );
		$result  = implode( ', ', $visible );
		if ( count( $protocols ) > 10 ) {
			$result .= ', ...';
		}
		return $result;
	}

	/**
	 * Convert php.ini boolean-like values into a strict boolean.
	 *
	 * @param string $value Raw ini value.
	 * @return bool
	 */
	private function ini_flag_enabled( string $value ): bool {
		$normalized = strtolower( trim( $value ) );
		return in_array( $normalized, [ '1', 'on', 'true', 'yes' ], true );
	}
}
