<?php

declare(strict_types=1);

namespace SecurWP\Modules\Hardening;

if ( ! defined( 'ABSPATH' ) ) {
     exit;
}

use SecurWP\Includes\Interfaces\Module;
use SecurWP\Includes\Module_Loader;
use SecurWP\Modules\AuditLog\AuditLog;
use SecurWP\Modules\Hardening\Tweaks\SystemTweaks;
use SecurWP\Modules\Hardening\Tweaks\WordPressTweaks;
use SecurWP\Includes\IpDetector;
use SecurWP\Modules\Hardening\Tweaks\BreachedPasswordChecker;
use SecurWP\Modules\Hardening\Tweaks\PasswordChangeInterstitial;
use SecurWP\Modules\Tools\ConfigFilePolicy;

/**
 * The Hardening module class.
 * Acts as a loader/controller for specific hardening components.
 */
class Hardening implements Module {
    public const PASSWORD_CHANGE_REQUIRED_META = 'securwp_password_change_required';
    private const CAPTCHA_SECRET_NAME = 'hardening_captcha_secret_key';
    private const CAPTCHA_SECRET_NAME_PREFIX = 'hardening_captcha_secret_key_';
    private const LOGIN_ATTEMPT_CLEANUP_HOOK = 'securwp_login_attempt_cleanup';
    private const SERVER_RULE_RECONCILE_HOOK = 'securwp_server_rule_reconcile';
    private const SERVER_RULE_MAINTENANCE_HOOK = 'securwp_server_rule_maintenance';
    private const SERVER_RULE_VERIFY_HOOK = 'securwp_server_rule_verify';
    private const USER_NICENAME_REMEDIATION_HOOK = 'securwp_user_nicename_remediation';
    private const SERVER_RULE_RECONCILE_LOCK = 'securwp_server_rule_reconcile_lock';
    private const USER_NICENAME_REMEDIATION_CURSOR_OPTION = 'securwp_user_nicename_remediation_cursor';
    private const USER_NICENAME_REMEDIATION_COMPLETED_OPTION = 'securwp_user_nicename_remediation_completed';
    private const USER_NICENAME_REMEDIATION_BATCH_SIZE = 50;
    private const WP_CONFIG_RULES_HASH_TRANSIENT = 'securwp_wp_config_rules_hash';
    private const WP_CONFIG_LAST_SYNC_ERROR_TRANSIENT = 'securwp_wp_config_rules_last_error';
    private const WP_CONFIG_SYNC_LOCK = 'hardening_wp_config_sync';
    private const WP_CONFIG_BEGIN_MARKER = '// BEGIN SecurWP - Do not modify or remove this line';
    private const WP_CONFIG_END_MARKER = '// END SecurWP - Do not modify or remove this line';
    private const PASSWORD_POLICY_STATE_META = 'securwp_password_policy_state';
    private const BREACHED_PASSWORD_LOGIN_CACHE_TTL = 86400;

    private $plugin_name;
    private $version;
    private $settings;

    private $system_tweaks;
    private $wp_tweaks;
    private $breached_password_checker;

    public function __construct( string $plugin_name, string $version ) {
        $this->plugin_name = $plugin_name;
        $this->version = $version;
        $this->load_settings();
    }

    /**
     * Get current settings.
     * @return array
     */
    public function get_settings() {
        return $this->settings;
    }

    /**
     * Update settings and handle side-effects (e.g. rewrite rules).
     * 
     * @param array $new_settings Sanitize settings array.
     * @return bool True if updated, false if no change.
     */
    public function update_settings( array $new_settings ) {
        $old_settings = self::filter_known_settings_keys( $this->settings );
        $defaults = $this->get_default_settings();
        $provider = self::normalize_captcha_provider_name( (string) ( $new_settings['captcha_provider'] ?? $old_settings['captcha_provider'] ?? $defaults['captcha_provider'] ) );
        $provider_profiles = self::normalize_captcha_provider_profiles( $old_settings['captcha_provider_profiles'] ?? [] );

        $secret_updated = false;
        if ( array_key_exists( 'captcha_secret_key', $new_settings ) ) {
            $incoming_secret = trim( (string) $new_settings['captcha_secret_key'] );
            if ( $incoming_secret !== '' ) {
                \SecurWP\Includes\SecretManager::set( self::get_captcha_secret_name( $provider ), $incoming_secret );
                $secret_updated = true;
            }
            unset( $new_settings['captcha_secret_key'] );
        }

        if ( array_key_exists( 'captcha_site_key', $new_settings ) ) {
            $provider_profiles[ $provider ]['site_key'] = trim( (string) $new_settings['captcha_site_key'] );
        }
        $new_settings['captcha_provider_profiles'] = $provider_profiles;

        // CAPTCHA secret is stored in SecretManager only.
        $old_settings['captcha_secret_key'] = '';
        $old_settings['captcha_site_key'] = self::get_captcha_site_key_for_provider( $old_settings, $old_settings['captcha_provider'] ?? $defaults['captcha_provider'] );
        
        // Merge with current settings so omitted keys retain their current values
        $final_settings = array_merge( $defaults, $old_settings, $new_settings );
        $final_settings['captcha_provider'] = $provider;
        $final_settings['captcha_provider_profiles'] = self::normalize_captcha_provider_profiles( $final_settings['captcha_provider_profiles'] ?? [] );
        $final_settings = self::filter_known_settings_keys( $final_settings );
        $final_settings['captcha_site_key'] = self::get_captcha_site_key_for_provider( $final_settings, $provider );
        $final_settings['captcha_secret_key'] = '';
        
        $updated = \SecurWP\Includes\SettingsStore::save( \SecurWP\Includes\SettingsStore::COL_HARDENING, $final_settings );

        if ( $updated || $final_settings !== $old_settings || $secret_updated ) {
            $this->settings = $final_settings;
            $this->handle_settings_change_effects( $old_settings, $final_settings );
            
            return true;
        }
        
        return false;
    }

    /**
     * Handle side effects of settings changes.
     */
    private function handle_settings_change_effects( $old, $new ) {
        $slug_changed     = ( ( $old['rename_login_url_slug'] ?? '' ) !== ( $new['rename_login_url_slug'] ?? '' ) );
        $enabled_changed  = ( ( $old['rename_login_url_enabled'] ?? false ) !== ( $new['rename_login_url_enabled'] ?? false ) );
        $register_changed = ( ( $old['rename_login_url_register_slug'] ?? '' ) !== ( $new['rename_login_url_register_slug'] ?? '' ) );

        if ( $slug_changed || $enabled_changed || $register_changed ) {
            flush_rewrite_rules();

            // Rotate the access token when slug changes to invalidate old cookies/bookmarks.
            if ( $slug_changed ) {
                \SecurWP\Modules\Login\Login::regenerate_access_token();
            }

            // Notify admin when the login URL changes while feature is enabled.
            if ( $slug_changed && ! empty( $new['rename_login_url_enabled'] ) && ! empty( $new['rename_login_url_slug'] ) ) {
                $this->send_login_url_change_notification( $new['rename_login_url_slug'] );
            }
        }

        // Write server-level rules whenever relevant settings change.
        $server_keys = [
            'disable_directory_browsing',
            'block_php_uploads',
            'block_php_plugins',
            'block_php_themes',
            'disable_xmlrpc',
            'block_sensitive_files',
        ];
        $server_changed = false;
        foreach ( $server_keys as $key ) {
            if ( ( $old[ $key ] ?? null ) !== ( $new[ $key ] ?? null ) ) {
                $server_changed = true;
                break;
            }
        }
        if ( $server_changed ) {
            // Rebuild SystemTweaks with new settings so the rule generators
            // use the updated values instead of the stale copy from initialize().
            $this->system_tweaks = new SystemTweaks( $new );
            $this->sync_server_rules_for_settings( true );
            $this->schedule_server_rule_reconcile( MINUTE_IN_SECONDS );
        }

        $wp_config_keys = [
            'disable_file_editor',
        ];
        $wp_config_changed = false;
        foreach ( $wp_config_keys as $key ) {
            if ( ( $old[ $key ] ?? null ) !== ( $new[ $key ] ?? null ) ) {
                $wp_config_changed = true;
                break;
            }
        }
        if ( $wp_config_changed ) {
            $this->sync_wp_config_rules_for_settings( $new, true );
        }

        $force_unique_nicename_was_enabled = ! empty( $old['force_unique_nicename'] );
        $force_unique_nicename_is_enabled  = ! empty( $new['force_unique_nicename'] );

        if ( ! $force_unique_nicename_was_enabled && $force_unique_nicename_is_enabled ) {
            $this->schedule_user_nicename_remediation( true );
        } elseif ( ! $force_unique_nicename_is_enabled ) {
            $this->clear_user_nicename_remediation();
        }
    }

    /**
     * Register Hardening module's server rule filters.
     *
     * These filters contribute rules to the unified ServerConfigWriter.
     * Other modules (Headers, Firewall, etc.) can add their own filters
     * at the same hooks.
     */
    public function register_server_rule_filters(): void {
        add_filter( 'securwp_server_rules_apache', [ $this, 'contribute_apache_rules' ] );
        add_filter( 'securwp_server_rules_nginx', [ $this, 'contribute_nginx_rules' ] );
    }

    /**
     * Contribute Hardening module's Apache rules to the unified writer.
     */
    public function contribute_apache_rules( string $rules ): string {
        if ( $this->system_tweaks ) {
            $rules .= $this->system_tweaks->get_htaccess_rules();
        }
        return $rules;
    }

    /**
     * Contribute Hardening module's Nginx rules to the unified writer.
     */
    public function contribute_nginx_rules( string $rules ): string {
        if ( $this->system_tweaks ) {
            $rules .= $this->system_tweaks->get_nginx_rules();
        }
        return $rules;
    }

    /**
     * Sync server rules when the desired managed block has drifted from disk state.
     *
     * Uses a live-state hash transient to avoid unnecessary file I/O on every
     * admin load while still re-syncing external drift or stale managed blocks.
     * Hooked to admin_init at priority 99 so all modules have registered their
     * server rule filters before the sync runs.
     */
    public function maybe_sync_server_rules(): void {
        // admin_init fires for AJAX too; skip those to avoid unnecessary I/O.
        if ( wp_doing_ajax() ) {
            return;
        }

        $current_hash = \SecurWP\Includes\ServerConfigWriter::build_sync_hash();
        $hash_key     = \SecurWP\Includes\ServerConfigWriter::get_hash_transient_key();

        if ( get_transient( $hash_key ) === $current_hash ) {
            return;
        }

        $this->sync_server_rules_for_settings( false );
    }

    /**
     * Ensure unattended server-rule maintenance is scheduled.
     */
    public function ensure_server_rule_background_jobs(): void {
        if ( ! wp_next_scheduled( self::SERVER_RULE_MAINTENANCE_HOOK ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::SERVER_RULE_MAINTENANCE_HOOK );
        }
    }

    /**
     * React to global settings changes that alter server target resolution.
     *
     * @param array<string,mixed> $settings
     */
    public function handle_global_settings_saved( array $settings = [] ): void {
        delete_transient( \SecurWP\Includes\ServerConfigWriter::get_hash_transient_key() );
        \SecurWP\Includes\ServerConfigWriter::mark_verification_pending();
        $this->schedule_server_rule_reconcile( MINUTE_IN_SECONDS );
    }

    /**
     * Schedule a single unattended reconciliation pass.
     */
    private function schedule_server_rule_reconcile( int $delay_seconds = MINUTE_IN_SECONDS ): void {
        if ( ! wp_next_scheduled( self::SERVER_RULE_RECONCILE_HOOK ) ) {
            wp_schedule_single_event( time() + max( 1, $delay_seconds ), self::SERVER_RULE_RECONCILE_HOOK );
        }
    }

    /**
     * Schedule a single unattended live verification pass.
     */
    private function schedule_server_rule_verification( int $delay_seconds = MINUTE_IN_SECONDS ): void {
        $run_at   = time() + max( 1, $delay_seconds );
        $existing = wp_next_scheduled( self::SERVER_RULE_VERIFY_HOOK );

        if ( $existing && $existing <= $run_at ) {
            return;
        }

        if ( $existing ) {
            wp_unschedule_event( $existing, self::SERVER_RULE_VERIFY_HOOK );
        }

        wp_schedule_single_event( $run_at, self::SERVER_RULE_VERIFY_HOOK );
    }

    /**
     * Remove any queued unattended verification pass for server rules.
     */
    public function clear_scheduled_server_rule_verification(): void {
        while ( true ) {
            $timestamp = wp_next_scheduled( self::SERVER_RULE_VERIFY_HOOK );
            if ( ! $timestamp ) {
                break;
            }

            wp_unschedule_event( $timestamp, self::SERVER_RULE_VERIFY_HOOK );
        }
    }

    /**
     * Schedule batched remediation for existing users whose nicename leaks the login.
     */
    private function schedule_user_nicename_remediation( bool $reset_cursor = false, int $delay_seconds = MINUTE_IN_SECONDS ): void {
        if ( $reset_cursor ) {
            update_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION, 0, false );
            update_option( self::USER_NICENAME_REMEDIATION_COMPLETED_OPTION, '0', false );
        } elseif ( get_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION, null ) === null ) {
            update_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION, 0, false );
            update_option( self::USER_NICENAME_REMEDIATION_COMPLETED_OPTION, '0', false );
        }

        if ( ! wp_next_scheduled( self::USER_NICENAME_REMEDIATION_HOOK ) ) {
            wp_schedule_single_event( time() + max( 1, $delay_seconds ), self::USER_NICENAME_REMEDIATION_HOOK );
        }
    }

    /**
     * Clear any queued nicename remediation batches and reset progress.
     */
    private function clear_user_nicename_remediation(): void {
        delete_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION );

        while ( true ) {
            $timestamp = wp_next_scheduled( self::USER_NICENAME_REMEDIATION_HOOK );
            if ( ! $timestamp ) {
                break;
            }

            wp_unschedule_event( $timestamp, self::USER_NICENAME_REMEDIATION_HOOK );
        }
    }

    /**
     * Repair a batch of existing users whose author slug still exposes the login.
     */
    public function run_user_nicename_remediation_batch(): void {
        if ( empty( $this->settings['force_unique_nicename'] ) || ! ( $this->wp_tweaks instanceof WordPressTweaks ) ) {
            $this->clear_user_nicename_remediation();
            return;
        }

        $cursor      = (int) get_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION, 0 );
        $next_cursor = $this->wp_tweaks->remediate_user_nicenames_batch( $cursor, self::USER_NICENAME_REMEDIATION_BATCH_SIZE );

        if ( $next_cursor === null || $next_cursor <= $cursor ) {
            update_option( self::USER_NICENAME_REMEDIATION_COMPLETED_OPTION, '1', false );
            $this->clear_user_nicename_remediation();
            return;
        }

        update_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION, $next_cursor, false );
        $this->schedule_user_nicename_remediation( false, 15 );
    }

    /**
     * Schedule an unattended verification pass only when the current server
     * environment is expected to enforce the updated hardening rules.
     *
     * @param array<string,mixed>|null $state
     */
    private function maybe_schedule_server_rule_verification( ?array $state = null ): void {
        $state = is_array( $state ) ? $state : \SecurWP\Includes\ServerConfigWriter::get_state();
        $verification = is_array( $state['verification'] ?? null ) ? (array) $state['verification'] : [];
        $status       = is_string( $verification['status'] ?? null ) ? (string) $verification['status'] : 'pending';
        if ( $status !== 'pending' ) {
            return;
        }

        $policy = \SecurWP\Includes\ServerConfigWriter::get_verification_schedule_policy( $state );

        if ( empty( $policy['single_event'] ) ) {
            return;
        }

        $this->schedule_server_rule_verification( (int) ( $policy['single_event_delay'] ?? MINUTE_IN_SECONDS ) );
    }

    /**
     * Reconcile server rules in unattended contexts.
     */
    public function run_scheduled_server_rule_reconcile(): void {
        if ( get_transient( self::SERVER_RULE_RECONCILE_LOCK ) ) {
            return;
        }

        set_transient( self::SERVER_RULE_RECONCILE_LOCK, 1, 5 * MINUTE_IN_SECONDS );

        try {
            $hash_key      = \SecurWP\Includes\ServerConfigWriter::get_hash_transient_key();
            $current_hash  = \SecurWP\Includes\ServerConfigWriter::build_sync_hash();

            if ( get_transient( $hash_key ) !== $current_hash ) {
                $this->sync_server_rules_for_settings( false );
            }

            $this->maybe_schedule_server_rule_verification();
        } finally {
            delete_transient( self::SERVER_RULE_RECONCILE_LOCK );
        }
    }

    /**
     * Hourly maintenance pass for drift repair and stale verification refresh.
     */
    public function run_server_rule_maintenance(): void {
        $this->run_scheduled_server_rule_reconcile();

        if ( \SecurWP\Includes\ServerConfigWriter::needs_verification_refresh() ) {
            $this->run_scheduled_server_rule_verification();
        }
    }

    /**
     * Run a fresh server-rule enforcement verification pass.
     */
    public function run_scheduled_server_rule_verification(): void {
        if ( ! \SecurWP\Includes\ServerConfigWriter::needs_verification_refresh() ) {
            $this->clear_scheduled_server_rule_verification();
            return;
        }

        \SecurWP\Includes\ServerConfigWriter::run_verification();
        $this->clear_scheduled_server_rule_verification();
    }

    /**
     * Send an email to the site admin when the login URL changes.
     */
    private function send_login_url_change_notification( string $new_slug ): void {
        $admin_email = get_option( 'admin_email' );
        $site_name   = get_bloginfo( 'name' );
        $new_url     = home_url( '/' . $new_slug );

        $subject = sprintf(
            /* translators: %s: site name */
            __( '[%s] Login URL Changed', 'securwp' ),
            $site_name
        );

        $message = sprintf(
            /* translators: 1: site name, 2: new login URL */
            __(
                "Hello,\n\nThe login URL for %1\$s has been changed.\n\nNew login URL: %2\$s\n\nPlease save this URL. You will need it to access your WordPress admin.\n\nIf you did not make this change, please contact your site administrator immediately.\n\n- SecurWP Security",
                'securwp'
            ),
            $site_name,
            $new_url
        );

        try {
            wp_mail( $admin_email, $subject, $message );
        } catch ( \Throwable $e ) {
            // Don't let email failures interrupt settings update.
        }
    }

    private function load_settings() {
        $defaults = $this->get_default_settings();
        $this->settings = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_HARDENING, $defaults );
        if ( ! is_array( $this->settings ) ) {
            $this->settings = $defaults;
        }

        $settings_changed = false;
        $stripped_settings = self::filter_known_settings_keys( $this->settings );
        if ( $stripped_settings !== $this->settings ) {
            $this->settings = $stripped_settings;
            $settings_changed = true;
        }

        $this->settings['captcha_provider'] = self::normalize_captcha_provider_name( (string) ( $this->settings['captcha_provider'] ?? $defaults['captcha_provider'] ) );
        $normalized_profiles = self::normalize_captcha_provider_profiles( $this->settings['captcha_provider_profiles'] ?? [] );
        if ( $normalized_profiles !== ( $this->settings['captcha_provider_profiles'] ?? [] ) ) {
            $this->settings['captcha_provider_profiles'] = $normalized_profiles;
            $settings_changed = true;
        }

        $normalized_login_redirect = self::normalize_login_redirect_settings( $this->settings );
        if ( $normalized_login_redirect !== $this->settings ) {
            $this->settings      = $normalized_login_redirect;
            $settings_changed = true;
        }

        $active_provider = self::normalize_captcha_provider_name( (string) ( $this->settings['captcha_provider'] ?? $defaults['captcha_provider'] ) );

        $legacy_site_key = trim( (string) ( $this->settings['captcha_site_key'] ?? '' ) );
        if ( $legacy_site_key !== '' && empty( $this->settings['captcha_provider_profiles'][ $active_provider ]['site_key'] ) ) {
            $this->settings['captcha_provider_profiles'][ $active_provider ]['site_key'] = $legacy_site_key;
            $settings_changed = true;
        }

        // Migrate any previously persisted plaintext CAPTCHA secret to encrypted storage.
        $legacy_secret = trim( (string) ( $this->settings['captcha_secret_key'] ?? '' ) );
        if ( $legacy_secret !== '' ) {
            if ( ! self::has_captcha_secret( $active_provider ) ) {
                \SecurWP\Includes\SecretManager::set( self::get_captcha_secret_name( $active_provider ), $legacy_secret );
            }
            $this->settings['captcha_secret_key'] = '';
            $settings_changed = true;
        } else {
            $this->settings['captcha_secret_key'] = '';
        }

        $legacy_shared_secret = self::get_captcha_secret();
        if ( $legacy_shared_secret !== '' ) {
            if ( ! self::has_captcha_secret( $active_provider ) ) {
                \SecurWP\Includes\SecretManager::set( self::get_captcha_secret_name( $active_provider ), $legacy_shared_secret );
                $settings_changed = true;
            }

            \SecurWP\Includes\SecretManager::delete( self::CAPTCHA_SECRET_NAME );
            $settings_changed = true;
        }

        $active_site_key = self::get_captcha_site_key_for_provider( $this->settings, $active_provider );
        if ( (string) ( $this->settings['captcha_site_key'] ?? '' ) !== $active_site_key ) {
            $this->settings['captcha_site_key'] = $active_site_key;
            $settings_changed = true;
        }

        if ( $settings_changed ) {
            \SecurWP\Includes\SettingsStore::save( \SecurWP\Includes\SettingsStore::COL_HARDENING, $this->settings );
        }

        // Multisite: network admin can enforce a minimum hardening baseline on all subsites.
        // Individual per-site settings are preserved for everything above this baseline.
        if ( is_multisite() ) {
            $network = get_site_option( 'securwp_network_settings', [] );
            if ( ! empty( $network['enforce_hardening'] ) ) {
                $this->settings['hide_wp_version']              = true;
                $this->settings['hide_login_errors']            = true;
                $this->settings['block_user_enumeration']       = true;
                $this->settings['limit_login_attempts_enabled'] = true;
                // Escalate XML-RPC to at least disable pingbacks; never downgrade 'disable'.
                if ( ( $this->settings['disable_xmlrpc'] ?? self::get_default_settings()['disable_xmlrpc'] ) === 'enable' ) {
                    $this->settings['disable_xmlrpc'] = 'disable_pingbacks';
                }
            }
        }
    }

    public static function get_default_settings() {
         return [
                'hide_wp_version' => true,
            'disable_xmlrpc' => 'disable_pingbacks',
            'restrict_rest_api' => true,
            'restrict_rest_api_allowed_routes' => [],
            'hide_login_errors' => true,
            'block_user_enumeration' => true,
            'disable_application_passwords' => true,
            'force_unique_nicename' => true,
            'login_type_restriction' => 'default', // default, email, username
            
            'disable_directory_browsing' => true,
            'block_php_uploads' => true,
            'block_php_plugins' => true,
            'block_php_themes' => true,
            'block_sensitive_files' => true,
            'disable_file_editor' => true,
            'check_file_perms' => false,
            
            'enable_strong_passwords' => true,
            'strong_password_min_score' => 3,    // Zxcvbn score 0-4 (3 = "Strong")
            'strong_password_admin_score' => 4,  // Higher requirement for admins/editors
            'check_breached_passwords' => true,
            'password_expiration_days' => 0,     // 0 = disabled
            'prevent_password_reuse' => false,
            'force_password_change_on_role_upgrade' => false,

            'disable_extra_headers' => true,
            // Login security controls (shared with Login module settings surface).
            'rename_login_url_enabled' => false,
            'rename_login_url_slug' => '',
            'rename_login_url_register_slug' => 'wp-signup.php',
            'rename_login_url_block_behavior' => '404', // 403, 404, redirect
            'rename_login_url_redirect_url' => '/',
            'limit_login_attempts_enabled' => true,
            'limit_login_attempts_max' => 5,
            'limit_login_attempts_max_user' => 10,
            'limit_login_attempts_window' => 300,
            'limit_login_attempts_lockout' => 1800,
            'auto_ban_admin' => true,

            'captcha_enabled'   => false,
            'captcha_provider'  => 'recaptcha_v3',
            'captcha_site_key'  => '',
            'captcha_secret_key' => '',
            'captcha_provider_profiles' => [],
                'captcha_threshold' => 0.5,
        ];
    }

    /**
     * Whether login-time forced password reset is active for breached passwords.
     *
     * Breached password detection is the primary policy switch. When it is on,
     * existing users with compromised credentials must also be remediated at
     * login time.
     *
     * @param array<string,mixed> $settings
     */
    public static function is_breached_password_login_reset_enabled( array $settings ): bool {
        return ! empty( $settings['check_breached_passwords'] );
    }

    /**
     * Return the effective hardening settings merged with the canonical defaults.
     *
     * This is the read path other modules should use when they need the actual
     * active hardening posture instead of the raw persisted delta.
     *
     * @return array<string,mixed>
     */
    public static function get_saved_settings(): array {
        $defaults = self::get_default_settings();
        $settings = \SecurWP\Includes\SettingsStore::get( \SecurWP\Includes\SettingsStore::COL_HARDENING, $defaults );
        $settings = is_array( $settings ) ? wp_parse_args( $settings, $defaults ) : $defaults;

        $settings['captcha_provider'] = self::normalize_captcha_provider_name( (string) ( $settings['captcha_provider'] ?? $defaults['captcha_provider'] ) );
        $settings['captcha_provider_profiles'] = self::normalize_captcha_provider_profiles( $settings['captcha_provider_profiles'] ?? [] );
        $settings = self::normalize_login_redirect_settings( $settings );
        $settings = self::filter_known_settings_keys( $settings );
        $settings['captcha_site_key'] = self::get_captcha_site_key_for_provider(
            $settings,
            (string) ( $settings['captcha_provider'] ?? $defaults['captcha_provider'] )
        );
        $settings['captcha_secret_key'] = '';

        return $settings;
    }

    /**
     * Return the canonical recommended hardening baseline used across admin,
     * reporting, and posture scoring.
     *
     * @return array<int,string>
     */
    public static function get_recommended_hardening_keys(): array {
        return [
            'hide_wp_version',
            'disable_xmlrpc',
            'disable_file_editor',
            'block_user_enumeration',
            'disable_directory_browsing',
            'block_php_uploads',
            'block_php_plugins',
            'block_php_themes',
        ];
    }

    /**
     * Return the minimum number of recommended hardening controls that should
     * be enabled before the setup widget treats the baseline as configured.
     */
    public static function get_recommended_hardening_threshold(): int {
        return max( 1, (int) ceil( count( self::get_recommended_hardening_keys() ) / 2 ) );
    }

    /**
     * Determine whether a hardening control is effectively active.
     *
     * This is stricter than raw truthiness so enum-backed controls and runtime
     * constants are evaluated by their actual protective state.
     *
     * @param array<string,mixed> $settings
     */
    public static function is_hardening_control_effective( array $settings, string $key ): bool {
        switch ( $key ) {
            case 'disable_xmlrpc':
                $mode = (string) ( $settings['disable_xmlrpc'] ?? self::get_default_settings()['disable_xmlrpc'] );
                return in_array( $mode, [ 'disable_pingbacks', 'disable' ], true );

            case 'disable_file_editor':
                return ( defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT ) || ! empty( $settings['disable_file_editor'] );

            default:
                return ! empty( $settings[ $key ] );
        }
    }

    /**
     * Count how many hardening controls are effectively active.
     *
     * @param array<string,mixed> $settings
     * @param array<int,string>|null $keys
     */
    public static function count_effective_hardening_controls( array $settings, ?array $keys = null ): int {
        $controls = is_array( $keys ) && ! empty( $keys )
            ? array_values( $keys )
            : self::get_recommended_hardening_keys();

        $count = 0;
        foreach ( $controls as $key ) {
            if ( self::is_hardening_control_effective( $settings, $key ) ) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * Return decrypted CAPTCHA secret from secure storage.
     */
    public static function get_captcha_secret( ?string $provider = null ): string {
        $secret_name = self::CAPTCHA_SECRET_NAME;
        if ( $provider !== null ) {
            $secret_name = self::get_captcha_secret_name( $provider );
        }

        $secret = \SecurWP\Includes\SecretManager::get( $secret_name );
        return is_string( $secret ) ? trim( $secret ) : '';
    }

    /**
     * Check whether CAPTCHA secret is configured in secure storage.
     */
    public static function has_captcha_secret( ?string $provider = null ): bool {
        return self::get_captcha_secret( $provider ) !== '';
    }

    /**
     * Return provider-scoped site key profiles.
     *
     * @param mixed $profiles
     * @return array<string,array{site_key:string}>
     */
    public static function normalize_captcha_provider_profiles( $profiles ): array {
        $normalized = [];

        foreach ( self::get_supported_captcha_providers() as $provider ) {
            $normalized[ $provider ] = [ 'site_key' => '' ];
        }

        if ( ! is_array( $profiles ) ) {
            return $normalized;
        }

        foreach ( $profiles as $provider => $data ) {
            $provider_key = self::normalize_captcha_provider_name( (string) $provider );
            if ( ! isset( $normalized[ $provider_key ] ) ) {
                continue;
            }

            if ( is_array( $data ) ) {
                $normalized[ $provider_key ]['site_key'] = trim( sanitize_text_field( (string) ( $data['site_key'] ?? '' ) ) );
            } else {
                $normalized[ $provider_key ]['site_key'] = trim( sanitize_text_field( (string) $data ) );
            }
        }

        return $normalized;
    }

    /**
     * Build provider profile payload for REST consumers.
     *
     * @param array<string,mixed> $settings
     * @return array<string,array{site_key:string,secret_configured:bool}>
     */
    public static function get_captcha_provider_profiles_response( array $settings ): array {
        $profiles = self::normalize_captcha_provider_profiles( $settings['captcha_provider_profiles'] ?? [] );

        foreach ( $profiles as $provider => &$profile ) {
            $profile['secret_configured'] = self::has_captcha_secret( $provider );
        }
        unset( $profile );

        return $profiles;
    }

    /**
     * Resolve active provider site key from provider profiles.
     *
     * @param array<string,mixed> $settings
     */
    public static function get_captcha_site_key_for_provider( array $settings, ?string $provider = null ): string {
        $resolved_provider = self::normalize_captcha_provider_name( (string) ( $provider ?? $settings['captcha_provider'] ?? 'recaptcha_v3' ) );
        $profiles = self::normalize_captcha_provider_profiles( $settings['captcha_provider_profiles'] ?? [] );

        if ( ! empty( $profiles[ $resolved_provider ]['site_key'] ) ) {
            return (string) $profiles[ $resolved_provider ]['site_key'];
        }

        return trim( sanitize_text_field( (string) ( $settings['captcha_site_key'] ?? '' ) ) );
    }

    /**
     * Supported CAPTCHA providers with dedicated credential storage.
     *
     * @return array<int,string>
     */
    public static function get_supported_captcha_providers(): array {
        return [ 'recaptcha_v2_checkbox', 'recaptcha_v2_invisible', 'recaptcha_v3', 'turnstile' ];
    }

    /**
     * Normalize legacy and supported provider names.
     */
    public static function normalize_captcha_provider_name( string $provider ): string {
        $provider = sanitize_key( $provider );
        if ( $provider === 'recaptcha_v2' ) {
            return 'recaptcha_v2_checkbox';
        }
        if ( ! in_array( $provider, self::get_supported_captcha_providers(), true ) ) {
            return 'recaptcha_v3';
        }

        return $provider;
    }

    /**
     * Resolve the secret storage name for a provider.
     */
    private static function get_captcha_secret_name( string $provider ): string {
        return self::CAPTCHA_SECRET_NAME_PREFIX . self::normalize_captcha_provider_name( $provider );
    }

    /**
     * Keys managed by Login configure commands (subset of hardening settings).
     *
     * @return array<int,string>
     */
    public static function get_login_setting_keys(): array {
        return [
            'rename_login_url_enabled',
            'rename_login_url_slug',
            'rename_login_url_register_slug',
            'rename_login_url_block_behavior',
            'rename_login_url_redirect_url',
            'limit_login_attempts_enabled',
            'limit_login_attempts_max',
            'limit_login_attempts_max_user',
            'limit_login_attempts_window',
            'limit_login_attempts_lockout',
            'login_type_restriction',
            'disable_xmlrpc',
            'block_user_enumeration',
            'hide_login_errors',
            'captcha_enabled',
            'captcha_provider',
            'captcha_site_key',
            'captcha_secret_key',
            'captcha_threshold',
        ];
    }

    /**
     * Validate merged login/rename settings for safe, non-conflicting behavior.
     *
     * @param array<string,mixed> $settings
     * @return string|null Validation error code or null when valid.
     */
    public static function validate_login_slug_configuration( array $settings ): ?string {
        $rename_enabled = ! empty( $settings['rename_login_url_enabled'] );
        if ( ! $rename_enabled ) {
            return null;
        }

        $login_slug = sanitize_title( (string) ( $settings['rename_login_url_slug'] ?? '' ) );
        if ( $login_slug === '' ) {
            return 'login_slug_required';
        }
        if ( self::is_reserved_login_slug( $login_slug ) ) {
            return 'login_slug_reserved';
        }

        $register_slug_raw = self::normalize_register_slug_setting( (string) ( $settings['rename_login_url_register_slug'] ?? '' ) );
        $register_is_default = ( $register_slug_raw === 'wp-signup.php' );
        $register_slug = $register_is_default ? '' : sanitize_title( $register_slug_raw );
        if ( ! $register_is_default && $register_slug === '' ) {
            return 'register_slug_required';
        }
        if ( ! $register_is_default && $register_slug === $login_slug ) {
            return 'register_slug_conflict';
        }
        if ( ! $register_is_default && self::is_reserved_login_slug( $register_slug ) ) {
            return 'register_slug_reserved';
        }

        $block_behavior = self::normalize_block_behavior_setting( (string) ( $settings['rename_login_url_block_behavior'] ?? '' ) );
        if ( $block_behavior !== 'redirect' ) {
            return null;
        }

        $redirect_url_raw = (string) ( $settings['rename_login_url_redirect_url'] ?? '' );
        $redirect_url     = self::normalize_redirect_url_setting( $redirect_url_raw );
        if ( $redirect_url === '' && trim( $redirect_url_raw ) === '' ) {
            $redirect_url = '/';
        }
        if ( $redirect_url === '' ) {
            return 'redirect_url_invalid';
        }

        $redirect_slug = self::extract_slug_from_redirect_url( $redirect_url );
        if ( $redirect_slug === '' ) {
            return null;
        }

        if ( $redirect_slug === $login_slug || ( $register_slug !== '' && $redirect_slug === $register_slug ) ) {
            return 'redirect_url_conflict';
        }
        if ( self::is_reserved_login_slug( $redirect_slug ) ) {
            return 'redirect_url_reserved';
        }

        return null;
    }

    /**
     * Reserved slugs that must never be used as custom login/register/redirect slugs.
     *
     * @return array<int,string>
     */
    private static function get_reserved_login_slugs(): array {
        $reserved = [
            'wp-login.php',
            'wp-signup.php',
            'wp-register.php',
            'wp-activate.php',
            'wp-admin',
            'xmlrpc.php',
            'wp-cron.php',
        ];

        $rest_prefix = function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json';
        if ( is_string( $rest_prefix ) && $rest_prefix !== '' ) {
            $reserved[] = $rest_prefix;
        } else {
            $reserved[] = 'wp-json';
        }

        $slugs = [];
        foreach ( $reserved as $value ) {
            $slug = sanitize_title( (string) $value );
            if ( $slug !== '' ) {
                $slugs[] = $slug;
            }
        }

        return array_values( array_unique( $slugs ) );
    }

    /**
     * Check whether slug conflicts with a protected WordPress/system endpoint.
     */
    private static function is_reserved_login_slug( string $slug ): bool {
        if ( $slug === '' ) {
            return false;
        }

        return in_array( $slug, self::get_reserved_login_slugs(), true );
    }

    /**
     * Translate login setting validation codes to user-facing messages.
     */
    public static function get_login_slug_validation_message( string $code ): string {
        switch ( $code ) {
            case 'login_slug_required':
                return __( 'Custom login slug is required when login URL renaming is enabled.', 'securwp' );
            case 'register_slug_required':
                return __( 'Register slug must be "wp-signup.php" or a valid custom slug.', 'securwp' );
            case 'register_slug_conflict':
                return __( 'Register slug must be different from the custom login slug.', 'securwp' );
            case 'login_slug_reserved':
                return __( 'Custom login slug conflicts with a protected WordPress endpoint. Choose a different slug.', 'securwp' );
            case 'register_slug_reserved':
                return __( 'Register slug conflicts with a protected WordPress endpoint. Choose a different slug.', 'securwp' );
            case 'redirect_url_invalid':
                return __( 'Redirect URL must be a valid site-relative path (for example, / for homepage) or a URL on this site.', 'securwp' );
            case 'redirect_url_conflict':
                return __( 'Redirect URL conflicts with your protected login/register routes. Choose a different destination.', 'securwp' );
            case 'redirect_url_reserved':
                return __( 'Redirect URL conflicts with a protected WordPress endpoint. Choose a different destination.', 'securwp' );
            default:
                return __( 'Invalid login URL configuration.', 'securwp' );
        }
    }

    /**
     * Sanitize a partial hardening settings patch.
     *
     * @param array<string,mixed>      $input
     * @param array<int,string>|null   $allowed_keys Optional key allowlist.
     * @return array<string,mixed>
     */
    public static function sanitize_settings_patch( array $input, ?array $allowed_keys = null ): array {
        $defaults = self::get_default_settings();
        $allowed  = null;
        if ( is_array( $allowed_keys ) && ! empty( $allowed_keys ) ) {
            $allowed = array_fill_keys(
                array_values(
                    array_intersect( array_keys( $defaults ), $allowed_keys )
                ),
                true
            );
        }

        $minimums = [
            'limit_login_attempts_max'     => 1,
            'limit_login_attempts_max_user'=> 1,
            'limit_login_attempts_window'  => 60,
            'limit_login_attempts_lockout' => 60,
            'strong_password_min_score'    => 0,
            'strong_password_admin_score'  => 0,
            'password_expiration_days'     => 0,
        ];
        $maximums = [
            'strong_password_min_score'    => 4,
            'strong_password_admin_score'  => 4,
            'password_expiration_days'     => 365,
        ];
        $float_ranges = [
            'captcha_threshold'            => [ 0.0, 1.0 ],
        ];
        $enum_values = [
            'disable_xmlrpc'               => [ 'enable', 'disable_pingbacks', 'disable' ],
            'login_type_restriction'       => [ 'default', 'email', 'username' ],
            'captcha_provider'             => [ 'recaptcha_v2', 'recaptcha_v2_checkbox', 'recaptcha_v2_invisible', 'recaptcha_v3', 'turnstile' ],
            'rename_login_url_block_behavior' => [ '403', '404', 'redirect' ],
        ];
        $slug_blacklist = array_values( array_unique( array_merge(
            [
                'login',
                'admin',
                'dashboard',
                'register',
                'wp-content',
                'wp-includes',
                'feed',
                'sitemap',
                'robots-txt',
            ],
            self::get_reserved_login_slugs()
        ) ) );

        $sanitized = [];
        foreach ( $input as $raw_key => $value ) {
            $key = sanitize_key( (string) $raw_key );
            if ( ! array_key_exists( $key, $defaults ) ) {
                continue;
            }
            if ( is_array( $allowed ) && ! isset( $allowed[ $key ] ) ) {
                continue;
            }

            if ( 'captcha_secret_key' === $key ) {
                // Empty values are treated as no-op so redacted API payloads cannot wipe secrets.
                $secret = trim( sanitize_text_field( (string) $value ) );
                if ( $secret !== '' ) {
                    $sanitized[ $key ] = $secret;
                }
                continue;
            }

            if ( 'rename_login_url_register_slug' === $key ) {
                $raw = trim( sanitize_text_field( (string) $value ) );
                $raw = self::normalize_register_slug_setting( $raw );
                if ( $raw === 'wp-signup.php' ) {
                    $sanitized[ $key ] = 'wp-signup.php';
                    continue;
                }

                $slug = sanitize_title( $raw );
                if ( strlen( $slug ) > 64 ) {
                    $slug = substr( $slug, 0, 64 );
                }
                if ( $slug === '' || in_array( $slug, $slug_blacklist, true ) ) {
                    continue;
                }
                $sanitized[ $key ] = $slug;
                continue;
            }

            if ( 'rename_login_url_redirect_url' === $key ) {
                $raw = trim( (string) $value );
                if ( $raw === '' ) {
                    $sanitized[ $key ] = '/';
                    continue;
                }

                $normalized = self::normalize_redirect_url_setting( $raw );
                if ( $normalized === '' ) {
                    $sanitized[ $key ] = '';
                    continue;
                }
                $sanitized[ $key ] = $normalized;
                continue;
            }

            if ( 'rename_login_url_slug' === $key ) {
                $slug = sanitize_title( (string) $value );
                if ( strlen( $slug ) > 64 ) {
                    $slug = substr( $slug, 0, 64 );
                }
                if ( in_array( $slug, $slug_blacklist, true ) ) {
                    continue;
                }
                $sanitized[ $key ] = $slug;
                continue;
            }

            if ( 'captcha_provider_profiles' === $key ) {
                $sanitized[ $key ] = self::normalize_captcha_provider_profiles( $value );
                continue;
            }

            if ( isset( $enum_values[ $key ] ) ) {
                $enum = sanitize_key( (string) $value );
                if ( ! in_array( $enum, $enum_values[ $key ], true ) ) {
                    continue;
                }
                $sanitized[ $key ] = $enum;
                continue;
            }

            if ( is_bool( $defaults[ $key ] ) ) {
                if ( function_exists( 'rest_sanitize_boolean' ) ) {
                    $sanitized[ $key ] = rest_sanitize_boolean( $value );
                } else {
                    $sanitized[ $key ] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
                }
                continue;
            }

            if ( is_float( $defaults[ $key ] ) ) {
                $float = (float) $value;
                if ( isset( $float_ranges[ $key ] ) ) {
                    $float = max( $float_ranges[ $key ][0], min( $float_ranges[ $key ][1], $float ) );
                }
                $sanitized[ $key ] = $float;
                continue;
            }

            if ( is_int( $defaults[ $key ] ) ) {
                $int = absint( $value );
                if ( isset( $minimums[ $key ] ) && $int < $minimums[ $key ] ) {
                    $int = $minimums[ $key ];
                }
                if ( isset( $maximums[ $key ] ) && $int > $maximums[ $key ] ) {
                    $int = $maximums[ $key ];
                }
                $sanitized[ $key ] = $int;
                continue;
            }

            if ( is_array( $defaults[ $key ] ) ) {
                $list = [];
                if ( is_array( $value ) ) {
                    foreach ( $value as $item ) {
                        if ( is_array( $item ) || is_object( $item ) ) {
                            continue;
                        }
                        $item = sanitize_key( (string) $item );
                        if ( $item === '' ) {
                            continue;
                        }
                        $list[] = $item;
                    }
                }
                $sanitized[ $key ] = array_values( array_unique( $list ) );
                continue;
            }

            $sanitized[ $key ] = sanitize_text_field( (string) $value );
        }

        return $sanitized;
    }

    /**
     * Normalize login redirect settings to canonical values.
     *
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function normalize_login_redirect_settings( array $settings ): array {
        $normalized = $settings;

        $normalized['rename_login_url_block_behavior'] = self::normalize_block_behavior_setting(
            (string) ( $settings['rename_login_url_block_behavior'] ?? '' )
        );
        $raw_redirect_url = (string) ( $settings['rename_login_url_redirect_url'] ?? '' );
        $normalized_redirect_url = self::normalize_redirect_url_setting( $raw_redirect_url );
        if ( $normalized_redirect_url === '' && trim( $raw_redirect_url ) === '' ) {
            $normalized_redirect_url = '/';
        }
        $normalized['rename_login_url_redirect_url'] = $normalized_redirect_url;

        return $normalized;
    }

    /**
     * Keep only canonical hardening settings keys from persisted and remote payloads.
     *
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private static function filter_known_settings_keys( array $settings ): array {
        return array_intersect_key( $settings, array_fill_keys( array_keys( self::get_default_settings() ), true ) );
    }

    private static function normalize_block_behavior_setting( string $value ): string {
        $mode = strtolower( trim( $value ) );
        if ( in_array( $mode, [ '403', '404', 'redirect' ], true ) ) {
            return $mode;
        }

        return '404';
    }

    private static function normalize_redirect_url_setting( string $value ): string {
        $raw = trim( $value );
        if ( $raw === '' ) {
            return '';
        }

        // Accept full URLs only when they stay on the current site.
        if ( preg_match( '/^[a-z][a-z0-9+\-.]*:\/\//i', $raw ) ) {
            $safe_url = esc_url_raw( $raw );
            if ( $safe_url === '' ) {
                return '';
            }

            $target_host = wp_parse_url( $safe_url, PHP_URL_HOST );
            $home_host   = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
            if ( ! is_string( $target_host ) || ! is_string( $home_host ) || strtolower( $target_host ) !== strtolower( $home_host ) ) {
                return '';
            }

            $path  = (string) wp_parse_url( $safe_url, PHP_URL_PATH );
            $query = (string) wp_parse_url( $safe_url, PHP_URL_QUERY );
            $path  = '/' . ltrim( $path, '/' );
            if ( $path === '' ) {
                $path = '/';
            }

            $result = $path;
            if ( $query !== '' ) {
                $result .= '?' . $query;
            }

            return $result;
        }

        // Accept site-relative paths.
        if ( 0 === strpos( $raw, '/' ) ) {
            $path  = (string) wp_parse_url( $raw, PHP_URL_PATH );
            $query = (string) wp_parse_url( $raw, PHP_URL_QUERY );
            if ( $path === '' ) {
                $path = '/';
            } else {
                $path = '/' . ltrim( $path, '/' );
            }

            $result = $path;
            if ( $query !== '' ) {
                $result .= '?' . $query;
            }

            return $result;
        }

        // Accept bare slug for convenience.
        $slug = sanitize_title( $raw );
        if ( $slug === '' ) {
            return '';
        }

        return '/' . $slug;
    }

    private static function extract_slug_from_redirect_url( string $url ): string {
        $path = (string) wp_parse_url( $url, PHP_URL_PATH );
        if ( $path === '' ) {
            return '';
        }

        $trimmed = trim( $path, '/' );
        if ( $trimmed === '' ) {
            return '';
        }

        $parts = explode( '/', $trimmed );
        return sanitize_title( (string) ( $parts[0] ?? '' ) );
    }

    private static function normalize_register_slug_setting( string $value ): string {
        $slug = trim( $value );
        if ( $slug === '' || strtolower( $slug ) === 'wp-signup.php' ) {
            return 'wp-signup.php';
        }

        return $slug;
    }

    /**
     * Write server rules and only mark sync success when write is successful.
     */
    private function sync_server_rules_for_settings( bool $log_failure ): void {
        $hash_key      = \SecurWP\Includes\ServerConfigWriter::get_hash_transient_key();
        $state         = \SecurWP\Includes\ServerConfigWriter::get_state();
        $hash          = \SecurWP\Includes\ServerConfigWriter::build_sync_hash();
        $server        = is_string( $state['server'] ?? null ) ? (string) $state['server'] : \SecurWP\Includes\ServerConfigWriter::detect_server();
        $config_path   = is_string( $state['managed_target_path'] ?? null ) ? (string) $state['managed_target_path'] : '';
        $desired_block = \SecurWP\Includes\ServerConfigWriter::get_managed_rules_for_server( $server );
        $operation     = ! empty( $state['desired_rules_present'] ) ? 'apply' : 'remove';

        if ( empty( $state['drifted'] ) ) {
            \SecurWP\Includes\ServerConfigWriter::clear_sync_failure();
            set_transient( $hash_key, $hash, DAY_IN_SECONDS );
            $this->maybe_schedule_server_rule_verification( $state );
            return;
        }

        if ( ! empty( $state['conflict_detected'] ) ) {
            \SecurWP\Includes\ServerConfigWriter::clear_sync_failure();
            set_transient( $hash_key, $hash, DAY_IN_SECONDS );

            if ( $log_failure ) {
                AuditLog::add_log(
                    'Server Rule Sync Blocked',
                    [
                        'server'      => $server,
                        'config_path' => $config_path,
                        'reason'      => 'nginx_rule_conflict',
                    ],
                    'warning',
                    'hardening'
                );
            }

            return;
        }

        if ( empty( $state['write_enabled'] ) || empty( $state['writable'] ) ) {
            delete_transient( $hash_key );
            return;
        }

        $written = \SecurWP\Includes\ServerConfigWriter::write();
        if ( $written ) {
            \SecurWP\Includes\ServerConfigWriter::clear_sync_failure();
            $next_state = \SecurWP\Includes\ServerConfigWriter::get_state();
            set_transient( $hash_key, (string) ( $next_state['sync_hash'] ?? \SecurWP\Includes\ServerConfigWriter::build_sync_hash() ), DAY_IN_SECONDS );
            $this->maybe_schedule_server_rule_verification( $next_state );
            return;
        }

        // Write failed: keep hash unset so admin_init retries on next load.
        delete_transient( $hash_key );

        $reason = \SecurWP\Includes\ServerConfigWriter::get_last_write_error_reason();
        \SecurWP\Includes\ServerConfigWriter::record_sync_failure( $config_path, $desired_block, $operation, $reason !== '' ? $reason : 'unknown' );

        if ( $log_failure ) {
            AuditLog::add_log(
                'Server Rule Sync Failed',
                [
                    'server'      => $server,
                    'config_path' => $config_path,
                    'reason'      => $reason !== '' ? $reason : 'unknown',
                ],
                'warning',
                'hardening'
            );
        }

        $this->schedule_server_rule_reconcile( 5 * MINUTE_IN_SECONDS );
    }

    /**
     * Build the hash used to detect whether wp-config rules are in sync.
     *
     * @param array<string,mixed> $settings
     */
    private function build_wp_config_rules_hash( array $settings ): string {
        $status = ConfigFilePolicy::get_wp_config_status();
        $path   = is_string( $status['path'] ?? null ) ? (string) $status['path'] : '';
        $raw    = trim( $this->get_wp_config_manual_rules_raw( $settings ) );
        $block  = $this->get_current_wp_config_managed_block( $path );

        return md5(
            'path=' . $path
            . ';write_enabled=' . ( ! empty( $status['write_enabled'] ) ? '1' : '0' )
            . ';writable=' . ( ! empty( $status['writable'] ) ? '1' : '0' )
            . ';rules=' . $raw
            . ';block=' . trim( $block )
        );
    }

    /**
     * Sync wp-config rules if they have drifted from current hardening settings.
     */
    public function maybe_sync_wp_config_rules(): void {
        if ( wp_doing_ajax() ) {
            return;
        }

        $current_hash = $this->build_wp_config_rules_hash( $this->settings );
        if ( get_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT ) === $current_hash ) {
            return;
        }

        $this->sync_wp_config_rules_for_settings( $this->settings, false );
    }

    /**
     * Persist generated wp-config rules when writes are permitted.
     *
     * @param array<string,mixed> $settings
     */
    private function sync_wp_config_rules_for_settings( array $settings, bool $log_failure ): void {
        $hash   = $this->build_wp_config_rules_hash( $settings );
        $status = ConfigFilePolicy::get_wp_config_status();
        $path   = is_string( $status['path'] ?? null ) ? (string) $status['path'] : '';
        $wrapped_rules = $this->wrap_manual_rules(
            trim( $this->get_wp_config_manual_rules_raw( $settings ) ),
            'wp-config'
        );
        $operation     = '' === trim( $wrapped_rules ) ? 'remove' : 'apply';
        $current_block = $this->get_current_wp_config_managed_block( $path );
        $is_drifted    = trim( $current_block ) !== trim( $wrapped_rules );

        if ( '' === $path ) {
            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
            $this->clear_wp_config_sync_failure();
            return;
        }

        if ( ! $is_drifted ) {
            set_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT, $hash, DAY_IN_SECONDS );
            $this->clear_wp_config_sync_failure();
            return;
        }

        if ( empty( $status['writable'] ) ) {
            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );

            if ( $log_failure ) {
                AuditLog::add_log(
                    'wp-config Rule Sync Pending',
                    [
                        'config_path' => $path,
                        'reason'      => 'writes_unavailable',
                    ],
                    'warning',
                    'hardening'
                );
            }
            return;
        }

        if ( ! $this->acquire_wp_config_sync_lock() ) {
            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
            return;
        }

        try {
            $result = $this->write_wp_config_rules( $path, $wrapped_rules );
            if ( true === $result ) {
                set_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT, $hash, DAY_IN_SECONDS );
                $this->clear_wp_config_sync_failure();
                return;
            }

            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
            $reason = is_string( $result ) ? $result : 'unknown';
            $this->record_wp_config_sync_failure( $path, $wrapped_rules, $operation, $reason );

            if ( $log_failure ) {
                AuditLog::add_log(
                    'wp-config Rule Sync Failed',
                    [
                        'config_path' => $path,
                        'reason'      => $reason,
                    ],
                    'warning',
                    'hardening'
                );
            }
        } finally {
            $this->release_wp_config_sync_lock();
        }
    }

    /**
     * Build the current wp-config rule sync state from settings and live disk state.
     *
     * @param array<string,mixed>|null $settings
     * @return array<string,mixed>
     */
    public function get_wp_config_rule_state( ?array $settings = null ): array {
        $resolved_settings = is_array( $settings ) ? $settings : $this->settings;
        $status            = ConfigFilePolicy::get_wp_config_status();
        $path              = is_string( $status['path'] ?? null ) ? (string) $status['path'] : '';
        $desired_block     = $this->wrap_manual_rules(
            trim( $this->get_wp_config_manual_rules_raw( $resolved_settings ) ),
            'wp-config'
        );
        $current_block     = $this->get_current_wp_config_managed_block( $path );
        $desired_present   = '' !== trim( $desired_block );
        $managed_present   = '' !== trim( $current_block );
        $persisted         = $desired_present && trim( $current_block ) === trim( $desired_block );
        $drifted           = trim( $current_block ) !== trim( $desired_block );
        $write_enabled     = ! empty( $status['write_enabled'] );
        $writable          = ! empty( $status['writable'] );
        $sync_failure      = $this->get_wp_config_sync_failure( $path, $desired_block );
        $mode              = 'none';
        $message           = '';

        if ( $desired_present ) {
            if ( $persisted ) {
                $mode    = 'persisted';
                $message = '';
            } elseif ( $managed_present ) {
                if ( $writable ) {
                    if ( null !== $sync_failure ) {
                        $mode    = 'sync_failed';
                        $message = $this->describe_wp_config_sync_failure(
                            (string) $sync_failure['operation'],
                            (string) $sync_failure['reason']
                        );
                    } else {
                        $mode    = 'repair_pending';
                        $message = 'wp-config.php contains an out-of-date managed SecurWP rule. SecurWP will reconcile it on the next sync.';
                    }
                } elseif ( $write_enabled ) {
                    $mode    = 'repair_required';
                    $message = 'wp-config.php contains a stale managed SecurWP rule and is not writable. SecurWP is enforcing this rule at runtime, but the persisted block cannot be repaired until write access returns.';
                } else {
                    $mode    = 'repair_required';
                    $message = 'Write to Files is disabled and wp-config.php contains a stale managed SecurWP rule. SecurWP is enforcing this rule at runtime, but the persisted block must be repaired manually or after writes are re-enabled.';
                }
            } elseif ( $writable ) {
                if ( null !== $sync_failure ) {
                    $mode    = 'sync_failed';
                    $message = $this->describe_wp_config_sync_failure(
                        (string) $sync_failure['operation'],
                        (string) $sync_failure['reason']
                    );
                } else {
                    $mode    = 'sync_pending';
                    $message = 'SecurWP will write this rule to wp-config.php on the next sync.';
                }
            } elseif ( '' === $path ) {
                $mode    = 'runtime';
                $message = 'wp-config.php could not be located, so SecurWP is enforcing this rule at runtime.';
            } elseif ( $write_enabled ) {
                $mode    = 'runtime';
                $message = 'wp-config.php is not writable, so SecurWP is enforcing this rule at runtime.';
            } else {
                $mode    = 'runtime';
                $message = 'Write to Files is disabled, so SecurWP is enforcing this rule at runtime.';
            }
        } elseif ( $managed_present ) {
            if ( $writable ) {
                if ( null !== $sync_failure ) {
                    $mode    = 'removal_failed';
                    $message = $this->describe_wp_config_sync_failure(
                        (string) $sync_failure['operation'],
                        (string) $sync_failure['reason']
                    );
                } else {
                    $mode    = 'removal_pending';
                    $message = 'SecurWP will remove its managed wp-config.php rule on the next sync.';
                }
            } elseif ( $write_enabled ) {
                $mode    = 'removal_required';
                $message = 'wp-config.php still contains the managed SecurWP rule. Restore write access or remove the block manually to restore the editor.';
            } else {
                $mode    = 'removal_required';
                $message = 'Write to Files is disabled, but wp-config.php still contains the managed SecurWP rule. Re-enable writes or remove the block manually to restore the editor.';
            }
        }

        return [
            'mode'                  => $mode,
            'message'               => $message,
            'path'                  => $path,
            'write_enabled'         => $write_enabled,
            'writable'              => $writable,
            'desired_rules_present' => $desired_present,
            'managed_block_present' => $managed_present,
            'persisted'             => $persisted,
            'drifted'               => $drifted,
            'last_error_reason'     => is_array( $sync_failure ) ? (string) ( $sync_failure['reason'] ?? '' ) : '',
            'last_error_operation'  => is_array( $sync_failure ) ? (string) ( $sync_failure['operation'] ?? '' ) : '',
        ];
    }

    /**
     * Persist the last concrete wp-config sync failure for UI/reporting.
     */
    private function record_wp_config_sync_failure( string $path, string $wrapped_rules, string $operation, string $reason ): void {
        set_transient(
            self::WP_CONFIG_LAST_SYNC_ERROR_TRANSIENT,
            [
                'path'         => $path,
                'operation'    => $operation,
                'reason'       => $reason,
                'desired_hash' => md5( trim( $wrapped_rules ) ),
                'recorded_at'  => time(),
            ],
            DAY_IN_SECONDS
        );
    }

    /**
     * Clear the cached wp-config sync failure once the file is reconciled.
     */
    private function clear_wp_config_sync_failure(): void {
        delete_transient( self::WP_CONFIG_LAST_SYNC_ERROR_TRANSIENT );
    }

    /**
     * Return the last sync failure if it still matches the current desired file state.
     *
     * @return array<string,mixed>|null
     */
    private function get_wp_config_sync_failure( string $path, string $desired_block ): ?array {
        $failure = get_transient( self::WP_CONFIG_LAST_SYNC_ERROR_TRANSIENT );
        if ( ! is_array( $failure ) ) {
            return null;
        }

        $failure_path         = is_string( $failure['path'] ?? null ) ? (string) $failure['path'] : '';
        $failure_reason       = is_string( $failure['reason'] ?? null ) ? (string) $failure['reason'] : '';
        $failure_operation    = is_string( $failure['operation'] ?? null ) ? (string) $failure['operation'] : '';
        $failure_desired_hash = is_string( $failure['desired_hash'] ?? null ) ? (string) $failure['desired_hash'] : '';
        $current_desired_hash = md5( trim( $desired_block ) );

        if (
            '' === $failure_reason
            || '' === $failure_operation
            || $failure_path !== $path
            || $failure_desired_hash !== $current_desired_hash
        ) {
            return null;
        }

        return $failure;
    }

    /**
     * Translate low-level write failures into operator-facing state messages.
     */
    private function describe_wp_config_sync_failure( string $operation, string $reason ): string {
        $action = 'remove' === $operation ? 'remove' : 'write';

        switch ( $reason ) {
            case 'backup_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because creating a backup failed.', $action );
            case 'temp_file_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because the temporary file could not be created.', $action );
            case 'write_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because writing the updated file failed.', $action );
            case 'rename_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because replacing the live file failed.', $action );
            case 'rename_failed_restore_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule, and restoring wp-config.php from backup also failed. Review the file immediately.', $action );
            case 'verify_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because post-write verification failed. The original file was restored.', $action );
            case 'verify_failed_restore_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule, verification failed, and restoring wp-config.php from backup also failed. Review the file immediately.', $action );
            case 'compose_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because the updated configuration could not be composed safely.', $action );
            case 'read_failed':
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule because the current wp-config.php file could not be read.', $action );
            default:
                return sprintf( 'SecurWP could not %s the managed wp-config.php rule. Review wp-config.php access and try again.', $action );
        }
    }

    /**
     * Write the managed SecurWP block into wp-config.php via temp file + replace.
     *
     * @return true|string True on success, otherwise a failure reason.
     */
    private function write_wp_config_rules( string $wp_config_path, string $wrapped_rules ) {
        $content = file_get_contents( $wp_config_path );
        if ( false === $content ) {
            return 'read_failed';
        }

        $updated_content = $this->replace_wp_config_managed_block( $content, $wrapped_rules );
        if ( null === $updated_content ) {
            return 'compose_failed';
        }

        if ( $updated_content === $content ) {
            return true;
        }

        $config_dir = dirname( $wp_config_path );
        $backup     = $this->build_wp_config_backup_path( $wp_config_path );
        if ( ! copy( $wp_config_path, $backup ) ) {
            return 'backup_failed';
        }

        $temp_file  = tempnam( $config_dir, '.wp-config-securwp-' );
        if ( false === $temp_file ) {
            wp_delete_file( $backup );
            return 'temp_file_failed';
        }

        $original_perms = fileperms( $wp_config_path );
        $original_mode  = false !== $original_perms ? ( $original_perms & 0777 ) : null;
        if ( null !== $original_mode ) {
            $this->apply_file_permissions( $temp_file, $original_mode );
        }

        $bytes_written  = file_put_contents( $temp_file, $updated_content, LOCK_EX );
        $expected_bytes = strlen( $updated_content );
        if ( false === $bytes_written || $bytes_written !== $expected_bytes ) {
            wp_delete_file( $temp_file );
            wp_delete_file( $backup );
            return 'write_failed';
        }

        if ( ! $this->move_file( $temp_file, $wp_config_path ) ) {
            wp_delete_file( $temp_file );
            if ( ! $this->restore_wp_config_backup( $backup, $wp_config_path, $original_mode ) ) {
                return 'rename_failed_restore_failed';
            }
            wp_delete_file( $backup );
            return 'rename_failed';
        }

        if ( null !== $original_mode ) {
            $this->apply_file_permissions( $wp_config_path, $original_mode );
        }

        $verification = file_get_contents( $wp_config_path );
        if ( ! is_string( $verification ) || $verification !== $updated_content ) {
            if ( ! $this->restore_wp_config_backup( $backup, $wp_config_path, $original_mode ) ) {
                return 'verify_failed_restore_failed';
            }
            wp_delete_file( $backup );
            return 'verify_failed';
        }

        wp_delete_file( $backup );
        return true;
    }

    /**
     * Replace the managed SecurWP block in wp-config.php or inject it safely.
     */
    private function replace_wp_config_managed_block( string $content, string $wrapped_rules ): ?string {
        $pattern = $this->get_wp_config_managed_block_pattern();
        $stripped = preg_replace( $pattern, '', $content, 1 );
        if ( null === $stripped ) {
            return null;
        }

        $stripped = preg_replace( "/\n{3,}/", "\n\n", $stripped );
        if ( null === $stripped ) {
            return null;
        }

        $wrapped_rules = trim( $wrapped_rules );
        if ( '' === $wrapped_rules ) {
            return rtrim( $stripped ) . "\n";
        }

        $insertion_point = $this->find_wp_config_rules_insertion_point( $stripped );
        if ( false === $insertion_point ) {
            return null;
        }

        $prefix    = substr( $stripped, 0, $insertion_point );
        $suffix    = substr( $stripped, $insertion_point );
        $injection = rtrim( $wrapped_rules ) . "\n\n";

        if ( '' !== $prefix && ! preg_match( "/\n\s*\n$/", $prefix ) ) {
            $injection = "\n" . $injection;
        }

        return $prefix . $injection . $suffix;
    }

    /**
     * Read the currently managed SecurWP block from wp-config.php.
     */
    private function get_current_wp_config_managed_block( string $path ): string {
        if ( '' === $path || ! file_exists( $path ) ) {
            return '';
        }

        $content = file_get_contents( $path );
        if ( ! is_string( $content ) || '' === $content ) {
            return '';
        }

        if ( preg_match( $this->get_wp_config_managed_block_pattern(), $content, $match ) ) {
            return trim( (string) $match[0] );
        }

        return '';
    }

    /**
     * Match the SecurWP-managed block.
     */
    private function get_wp_config_managed_block_pattern(): string {
        return '/'
            . preg_quote( self::WP_CONFIG_BEGIN_MARKER, '/' )
            . '.*?'
            . preg_quote( self::WP_CONFIG_END_MARKER, '/' )
            . '\s*/s';
    }

    /**
     * Find a stable insertion point for SecurWP managed constants in wp-config.php.
     *
     * @return int|false
     */
    private function find_wp_config_rules_insertion_point( string $content ) {
        if ( preg_match( "/^[\/\*#\s]*That's all, stop editing!/m", $content, $match, PREG_OFFSET_CAPTURE ) ) {
            return (int) $match[0][1];
        }

        if ( preg_match( '/if\s*\(\s*!\s*defined\s*\(\s*[\'"]ABSPATH[\'"]\s*\)/', $content, $match, PREG_OFFSET_CAPTURE ) ) {
            return (int) $match[0][1];
        }

        if ( preg_match( '/require(?:_once)?\s*\(?\s*ABSPATH\s*\.\s*[\'"]wp-settings\.php[\'"]/', $content, $match, PREG_OFFSET_CAPTURE ) ) {
            return (int) $match[0][1];
        }

        if ( preg_match( '/\?>\s*$/', $content, $match, PREG_OFFSET_CAPTURE ) ) {
            return (int) $match[0][1];
        }

        return strlen( $content );
    }

    /**
     * Build a backup path for wp-config sync with a safe random suffix.
     */
    private function build_wp_config_backup_path( string $wp_config_path ): string {
        try {
            $suffix = bin2hex( random_bytes( 8 ) );
        } catch ( \Throwable $exception ) {
            $suffix = wp_generate_password( 16, false, false );
        }

        return $wp_config_path . '.securwp-hardening-backup-' . $suffix;
    }

    /**
     * Restore wp-config.php from a backup copy.
     */
    private function restore_wp_config_backup( string $backup, string $destination, ?int $permissions = null ): bool {
        if ( ! @copy( $backup, $destination ) ) {
            return false;
        }

        if ( null !== $permissions ) {
            $this->apply_file_permissions( $destination, $permissions );
        }

        return true;
    }

    /**
     * Move a file into place, preferring atomic rename and falling back to copy + delete.
     */
    private function move_file( string $source, string $destination ): bool {
        $filesystem = $this->get_filesystem();
        if ( is_object( $filesystem ) && method_exists( $filesystem, 'move' ) && $filesystem->move( $source, $destination, true ) ) {
            return file_exists( $destination );
        }

        if ( ! @copy( $source, $destination ) ) {
            return false;
        }

        wp_delete_file( $source );
        return ! file_exists( $source ) && file_exists( $destination );
    }

    /**
     * Resolve the WordPress filesystem adapter when available.
     */
    private function get_filesystem(): ?\WP_Filesystem_Base {
        if ( ! function_exists( 'WP_Filesystem' ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        WP_Filesystem();
        global $wp_filesystem;

        return $wp_filesystem instanceof \WP_Filesystem_Base ? $wp_filesystem : null;
    }

    /**
     * Apply filesystem permissions without calling chmod() directly.
     */
    private function apply_file_permissions( string $path, int $permissions ): void {
        $filesystem = $this->get_filesystem();
        if ( is_object( $filesystem ) && method_exists( $filesystem, 'chmod' ) ) {
            $filesystem->chmod( $path, $permissions );
        }
    }

    /**
     * Acquire a short-lived mutex around wp-config sync writes.
     */
    private function acquire_wp_config_sync_lock( int $expires = 180 ): bool {
        global $wpdb;
        $options_table = $this->table_identifier( $wpdb->options );
        $lock_option   = 'securwp_lock_' . self::WP_CONFIG_SYNC_LOCK;
        $now           = time();
        $release_at    = $now + $expires;

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $insert_sql = $wpdb->prepare(
            "INSERT IGNORE INTO {$options_table} (option_name, option_value, autoload) VALUES (%s, %s, 'no')",
            $lock_option,
            (string) $release_at
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Mutex acquisition uses an atomic INSERT IGNORE on the options table.
        $inserted = $wpdb->query( $insert_sql );
        if ( $inserted ) {
            return true;
        }

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $lock_sql = $wpdb->prepare(
            "SELECT option_value FROM {$options_table} WHERE option_name = %s LIMIT 1",
            $lock_option
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Mutex query.
        $locked_until = (int) $wpdb->get_var( $lock_sql );
        if ( $locked_until < $now ) {
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
            $reclaim_sql = $wpdb->prepare(
                "UPDATE {$options_table} SET option_value = %s WHERE option_name = %s AND option_value = %s",
                (string) $release_at,
                $lock_option,
                (string) $locked_until
            );
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Mutex reclamation updates a single options row atomically.
            $reclaimed = $wpdb->query( $reclaim_sql );
            return $reclaimed > 0;
        }

        return false;
    }

    /**
     * Release the wp-config sync mutex.
     */
    private function release_wp_config_sync_lock(): void {
        delete_option( 'securwp_lock_' . self::WP_CONFIG_SYNC_LOCK );
    }

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

        return '`' . $table_name . '`';
    }

    public function initialize( \SecurWP\Includes\Module_Loader $loader ) {
        $this->system_tweaks = new SystemTweaks( $this->settings );
        $this->wp_tweaks = new WordPressTweaks( $this->settings, $loader );

        // Register filters so this module contributes rules to the unified
        // ServerConfigWriter. Must be done before sync check.
        $this->register_server_rule_filters();
        add_action( self::SERVER_RULE_RECONCILE_HOOK, [ $this, 'run_scheduled_server_rule_reconcile' ] );
        add_action( self::SERVER_RULE_MAINTENANCE_HOOK, [ $this, 'run_server_rule_maintenance' ] );
        add_action( self::SERVER_RULE_VERIFY_HOOK, [ $this, 'run_scheduled_server_rule_verification' ] );
        add_action( self::USER_NICENAME_REMEDIATION_HOOK, [ $this, 'run_user_nicename_remediation_batch' ] );

        // Deferred to admin_init (priority 99) so all modules have registered
        // their rule filters by the time the sync runs.
        $loader->add_action( 'init', $this, 'ensure_server_rule_background_jobs', 20, 0 );
        if ( is_admin() ) {
            $loader->add_action( 'admin_init', $this, 'maybe_sync_server_rules', 99, 0 );
            $loader->add_action( 'admin_init', $this, 'maybe_sync_wp_config_rules', 100, 0 );
        }

        if ( ! empty( $this->settings['force_unique_nicename'] ) ) {
            $cursor_exists           = get_option( self::USER_NICENAME_REMEDIATION_CURSOR_OPTION, null ) !== null;
            $remediation_has_run     = get_option( self::USER_NICENAME_REMEDIATION_COMPLETED_OPTION, null ) === '1';
            if ( $cursor_exists || ! $remediation_has_run ) {
                $this->schedule_user_nicename_remediation( ! $cursor_exists && ! $remediation_has_run, 30 );
            }
        }

        // When global settings change (server_type, nginx_file, write_to_files),
        // invalidate the rule hashes so the next reconciliation pass uses the
        // correct target path and verification state.
        $loader->add_action( 'securwp_settings_saved_global_settings', $this, 'handle_global_settings_saved', 10, 1 );

        $api = new HardeningApi( $this );
        $loader->add_action( 'rest_api_init', $api, 'register_routes', 10, 0 );

        // auto_ban_admin works independently of limit_login_attempts_enabled.
        if ( ! empty( $this->settings['limit_login_attempts_enabled'] ) || ! empty( $this->settings['auto_ban_admin'] ) ) {
            // Enforce browser lockouts before renamed-login routing can fall
            // through to the normal login form and downstream theme filters.
            $loader->add_action( 'setup_theme', $this, 'enforce_browser_login_lockout', 1, 0 );
            $loader->add_action( 'login_init', $this, 'enforce_browser_login_lockout', 20, 0 );
            $loader->add_action( 'wp_login_failed', $this, 'handle_failed_login', 10, 2 );
            $loader->add_filter( 'authenticate', $this, 'check_login_lockout', 25, 3 );
            $loader->add_action( self::LOGIN_ATTEMPT_CLEANUP_HOOK, $this, 'cleanup_expired_login_attempts', 10, 0 );

            if ( ! wp_next_scheduled( self::LOGIN_ATTEMPT_CLEANUP_HOOK ) ) {
                wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::LOGIN_ATTEMPT_CLEANUP_HOOK );
            }
        }

        if ( ! empty( $this->settings['captcha_enabled'] ) ) {
            $captcha_settings = $this->settings;
            $active_provider = self::normalize_captcha_provider_name( (string) ( $captcha_settings['captcha_provider'] ?? 'recaptcha_v3' ) );
            $captcha_settings['captcha_provider'] = $active_provider;
            $captcha_settings['captcha_site_key'] = self::get_captcha_site_key_for_provider( $this->settings, $active_provider );
            $captcha_settings['captcha_secret_key'] = self::get_captcha_secret( $active_provider );
            $captcha = new Tweaks\CaptchaProvider( $captcha_settings );
            if ( $captcha->is_enabled() ) {
                $loader->add_filter( 'authenticate', $captcha, 'check_login', 20, 3 );
                $loader->add_action( 'login_enqueue_scripts', $captcha, 'enqueue_login_scripts' );
                $loader->add_action( 'login_form', $captcha, 'render_captcha_field' );
            }
        }

        $bp_block    = ! empty( $this->settings['check_breached_passwords'] );
        $bp_on_login = self::is_breached_password_login_reset_enabled( $this->settings );
        if ( $bp_block || $bp_on_login ) {
            $this->breached_password_checker = new BreachedPasswordChecker( $this->settings );
        }
        if ( $bp_block ) {
            $loader->add_action( 'user_profile_update_errors', $this->breached_password_checker, 'validate_profile_update', 10, 3 );
            $loader->add_action( 'validate_password_reset', $this->breached_password_checker, 'validate_password_reset', 10, 2 );
            $loader->add_filter( 'registration_errors', $this->breached_password_checker, 'validate_registration', 10, 3 );
        }
        if ( $bp_on_login ) {
            // Primary-password auth only. Application passwords authenticate on
            // the generic 'authenticate' chain too, and must not be treated as
            // the user's normal website password for breach-policy purposes.
            $loader->add_filter( 'wp_authenticate_user', $this, 'check_breached_password_on_login', 998, 2 );
        }
        if ( ! empty( $this->settings['enable_strong_passwords'] ) ) {
            $loader->add_filter( 'wp_authenticate_user', $this, 'check_strong_password_on_login', 999, 2 );
        }

        $loader->add_action( 'profile_update', $this, 'track_password_change', 10, 2 );
        $loader->add_action( 'password_reset', $this, 'track_password_reset', 10, 1 );

        if ( ! empty( $this->settings['force_password_change_on_role_upgrade'] ) ) {
            $loader->add_action( 'set_user_role', $this, 'handle_role_change', 10, 3 );
        }

        $needs_interstitial = (
            ! empty( $this->settings['password_expiration_days'] ) ||
            ! empty( $this->settings['force_password_change_on_role_upgrade'] ) ||
            ! empty( $this->settings['enable_strong_passwords'] ) ||
            $bp_on_login
        );
        if ( $needs_interstitial ) {
            $loader->add_filter( 'authenticate', $this, 'enforce_password_change_on_api_authentication', 29, 3 );
            $interstitial = new PasswordChangeInterstitial( $this->settings, $this->wp_tweaks );
            $loader->add_filter( 'authenticate', $interstitial, 'intercept_authentication', 29, 3 );
            $loader->add_action( 'login_form_securwp_password_change', $interstitial, 'render_form', 10, 0 );
        }
    }

    /**
     * Track password change on profile update.
     *
     * @param int   $user_id       User ID.
     * @param array $old_user_data Previous user data (WP 5.8+ passes WP_User pre-6.0).
     */
    public function track_password_change( int $user_id, $old_user_data = null ): void {
        // WordPress fires profile_update after wp_update_user. We detect password
        // change by comparing the hash. $old_user_data may be array or WP_User.
        $old_pass = '';
        if ( $old_user_data instanceof \WP_User ) {
            $old_pass = $old_user_data->user_pass;
        } elseif ( is_array( $old_user_data ) && isset( $old_user_data['user_pass'] ) ) {
            $old_pass = $old_user_data['user_pass'];
        }

        $new_user = get_userdata( $user_id );
        if ( ! $new_user ) {
            return;
        }

        // Only update timestamp if the password hash actually changed.
        if ( $old_pass && $old_pass === $new_user->user_pass ) {
            return;
        }

        update_user_meta( $user_id, 'securwp_last_password_change', time() );
        delete_user_meta( $user_id, self::PASSWORD_CHANGE_REQUIRED_META );
        self::clear_password_policy_state( $user_id );
    }

    /**
     * Track password change on password reset.
     *
     * @param \WP_User $user The user who reset their password.
     */
    public function track_password_reset( \WP_User $user ): void {
        update_user_meta( $user->ID, 'securwp_last_password_change', time() );
        delete_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META );
        self::clear_password_policy_state( $user->ID );
    }

    /**
     * Clear cached password-policy evaluations for the current password hash.
     */
    public static function clear_password_policy_state( int $user_id ): void {
        if ( $user_id <= 0 ) {
            return;
        }

        delete_user_meta( $user_id, self::PASSWORD_POLICY_STATE_META );
    }

    /**
     * Flag user for forced password change when promoted to a privileged role.
     *
     * @param int    $user_id   User ID.
     * @param string $new_role  New role slug.
     * @param array  $old_roles Previous roles.
     */
    public function handle_role_change( int $user_id, string $new_role, array $old_roles ): void {
        $privileged_roles = [ 'administrator', 'editor' ];
        $was_privileged   = ! empty( array_intersect( $old_roles, $privileged_roles ) );
        $is_privileged    = in_array( $new_role, $privileged_roles, true );

        if ( $is_privileged && ! $was_privileged ) {
            update_user_meta( $user_id, self::PASSWORD_CHANGE_REQUIRED_META, 'role_upgrade' );
        }
    }

    /**
     * Check whether a user's password has expired.
     *
     * @param int $user_id      User ID.
     * @param int $max_age_days Maximum password age in days (0 = disabled).
     * @return bool True if password is expired.
     */
    public static function is_password_expired( int $user_id, int $max_age_days ): bool {
        if ( $max_age_days <= 0 ) {
            return false;
        }

        $last_changed = (int) get_user_meta( $user_id, 'securwp_last_password_change', true );

        if ( ! $last_changed ) {
            $user = get_userdata( $user_id );
            $last_changed = $user ? strtotime( $user->user_registered ) : 0;
        }

        if ( ! $last_changed ) {
            return true;
        }

        return ( time() - $last_changed ) > ( $max_age_days * DAY_IN_SECONDS );
    }

    /**
     * Check whether the authenticated user's current password still meets
     * SecurWP's strong-password policy.
     *
     * When the password is weak in a browser login flow, flag the account for
     * the existing password-change interstitial. For XML-RPC and REST password
     * authentication, fail closed because an interactive interstitial cannot run.
     *
     * @param \WP_User|\WP_Error $user     Authenticated user object.
     * @param string             $password Plaintext password submitted for login.
     * @return \WP_User|\WP_Error
     */
    public function check_strong_password_on_login( $user, string $password ) {
        if ( ! ( $user instanceof \WP_User ) || $password === '' ) {
            return $user;
        }

        if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
            return $user;
        }

        if ( ! ( $this->wp_tweaks instanceof WordPressTweaks ) ) {
            return $user;
        }

        if ( $this->get_effective_password_change_reason( $user ) !== null ) {
            return $user;
        }

        $cached_strength = $this->get_cached_strong_password_evaluation( $user );
        if ( $cached_strength === null ) {
            $cached_strength = $this->wp_tweaks->is_password_strong_enough( $password, $user );
            $this->cache_strong_password_evaluation( $user, $cached_strength );
        }

        if ( $cached_strength ) {
            return $user;
        }

        if ( ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) {
            return new \WP_Error(
                'securwp_password_change_required',
                __( 'This account must use a stronger password before API authentication can continue. Sign in through the website and update the password.', 'securwp' )
            );
        }

        update_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META, 'weak' );
        AuditLog::add_log(
            'Weak Password Reset Required',
            [ 'username' => $user->user_login ],
            'warning',
            'user'
        );

        return $user;
    }

    /**
     * Fail closed for API password authentication when the account already
     * requires an interactive password change.
     *
     * Browser logins are redirected to the existing password-change
     * interstitial. REST and XML-RPC requests cannot complete that flow, so
     * they must stop until the password is updated through the website.
     *
     * @param \WP_User|\WP_Error|null $user     The authenticated user, error, or null.
     * @param string                  $username Submitted username.
     * @param string                  $password Submitted plaintext password.
     * @return \WP_User|\WP_Error|null
     */
    public function enforce_password_change_on_api_authentication( $user, $username, $password ) {
        if ( ! ( $user instanceof \WP_User ) || empty( $password ) || ! $this->is_api_password_auth_request() ) {
            return $user;
        }

        if ( ! wp_check_password( (string) $password, $user->user_pass, $user->ID ) ) {
            return $user;
        }

        $reason = $this->get_effective_password_change_reason( $user );
        if ( $reason === null || $reason === '' ) {
            return $user;
        }

        return new \WP_Error(
            'securwp_password_change_required',
            $this->get_api_password_change_required_message( $reason )
        );
    }

    /**
     * Determine whether the current authentication attempt is a non-browser API request.
     */
    private function is_api_password_auth_request(): bool {
        return ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST );
    }

    /**
     * Build a user-facing API authentication error for password-change-required states.
     */
    private function get_api_password_change_required_message( string $reason ): string {
        switch ( $reason ) {
            case 'breached':
                return __( 'This account must use a new password before API authentication can continue because the current password was found in a data breach. Sign in through the website and update the password.', 'securwp' );
            case 'role_upgrade':
                return __( 'This account must use a new password before API authentication can continue because the role was upgraded. Sign in through the website and update the password.', 'securwp' );
            case 'admin_forced':
                return __( 'This account must use a new password before API authentication can continue. Sign in through the website and update the password.', 'securwp' );
            case 'expired':
                return __( 'This account must use a new password before API authentication can continue because the current password has expired. Sign in through the website and update the password.', 'securwp' );
            case 'weak':
                return __( 'This account must use a stronger password before API authentication can continue. Sign in through the website and update the password.', 'securwp' );
            default:
                return __( 'This account must update its password before API authentication can continue. Sign in through the website and update the password.', 'securwp' );
        }
    }

    /**
     * Resolve the currently-active password-change reason for a user and clean
     * up stale stored flags that no longer map to an enabled policy.
     */
    private function get_effective_password_change_reason( \WP_User $user ): ?string {
        $reason = PasswordChangeInterstitial::resolve_required_change_reason( $user, $this->settings );
        if ( $reason !== null && $reason !== '' ) {
            return $reason;
        }

        $stored_flag = sanitize_key( (string) get_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META, true ) );
        if ( $stored_flag !== '' ) {
            delete_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META );
        }

        return null;
    }

    /**
     * Resolve cached strong-password evaluation for the user's current password.
     */
    private function get_cached_strong_password_evaluation( \WP_User $user ): ?bool {
        if ( ! ( $this->wp_tweaks instanceof WordPressTweaks ) ) {
            return null;
        }

        $state = $this->get_password_policy_state( $user );
        $strong = $state['strong'] ?? null;
        if ( ! is_array( $strong ) ) {
            return null;
        }

        $signature = is_string( $strong['signature'] ?? null ) ? (string) $strong['signature'] : '';
        if ( $signature === '' || ! hash_equals( $this->wp_tweaks->get_password_strength_signature( $user ), $signature ) ) {
            return null;
        }

        return array_key_exists( 'passes', $strong ) ? (bool) $strong['passes'] : null;
    }

    /**
     * Persist the strong-password evaluation for the user's current password.
     */
    private function cache_strong_password_evaluation( \WP_User $user, bool $passes ): void {
        if ( ! ( $this->wp_tweaks instanceof WordPressTweaks ) ) {
            return;
        }

        $state = $this->get_password_policy_state( $user );
        $state['strong'] = [
            'signature'    => $this->wp_tweaks->get_password_strength_signature( $user ),
            'passes'       => $passes,
            'evaluated_at' => time(),
        ];

        $this->save_password_policy_state( $user, $state );
    }

    /**
     * Resolve cached breached-password state for the user's current password.
     *
     * @return array{breached:bool,count:int,checked_at:int,stale:bool}|null
     */
    private function get_cached_breached_password_evaluation( \WP_User $user ): ?array {
        $state  = $this->get_password_policy_state( $user );
        $breach = $state['breach'] ?? null;
        if ( ! is_array( $breach ) ) {
            return null;
        }

        $checked_at = absint( $breach['checked_at'] ?? 0 );
        if ( $checked_at <= 0 ) {
            return null;
        }

        $breached = ! empty( $breach['breached'] );
        $count    = absint( $breach['count'] ?? 0 );
        $stale    = ! $breached && ( time() - $checked_at ) > self::BREACHED_PASSWORD_LOGIN_CACHE_TTL;

        return [
            'breached'   => $breached,
            'count'      => $count,
            'checked_at' => $checked_at,
            'stale'      => $stale,
        ];
    }

    /**
     * Persist a successful breached-password lookup for the current password.
     *
     * @param array{breached:bool,count:int,checked:bool} $result
     */
    private function cache_breached_password_evaluation( \WP_User $user, array $result ): void {
        if ( empty( $result['checked'] ) ) {
            return;
        }

        $state = $this->get_password_policy_state( $user );
        $state['breach'] = [
            'breached'   => ! empty( $result['breached'] ),
            'count'      => absint( $result['count'] ?? 0 ),
            'checked_at' => time(),
        ];

        $this->save_password_policy_state( $user, $state );
    }

    /**
     * Load cached password-policy state for the user's current password hash.
     *
     * @return array<string,mixed>
     */
    private function get_password_policy_state( \WP_User $user ): array {
        $raw_state = get_user_meta( $user->ID, self::PASSWORD_POLICY_STATE_META, true );
        $state     = is_array( $raw_state ) ? $raw_state : [];
        $fingerprint = $this->get_password_policy_fingerprint( $user );

        if ( $fingerprint === '' ) {
            return [];
        }

        $stored_fingerprint = is_string( $state['fingerprint'] ?? null ) ? (string) $state['fingerprint'] : '';
        if ( $stored_fingerprint !== '' && ! hash_equals( $stored_fingerprint, $fingerprint ) ) {
            $state = [];
        }

        $state['fingerprint'] = $fingerprint;

        return $state;
    }

    /**
     * Persist password-policy state for the user's current password hash.
     *
     * @param array<string,mixed> $state
     */
    private function save_password_policy_state( \WP_User $user, array $state ): void {
        $fingerprint = $this->get_password_policy_fingerprint( $user );
        if ( $fingerprint === '' ) {
            return;
        }

        $state['fingerprint'] = $fingerprint;
        update_user_meta( $user->ID, self::PASSWORD_POLICY_STATE_META, $state );
    }

    /**
     * Derive a stable fingerprint for the currently stored password hash.
     */
    private function get_password_policy_fingerprint( \WP_User $user ): string {
        if ( empty( $user->user_pass ) || ! is_string( $user->user_pass ) ) {
            return '';
        }

        return hash_hmac( 'sha256', $user->user_pass, wp_salt( 'secure_auth' ) );
    }
    
    public function get_block_php_snippets() {
        $snippets = [
            'htaccess' => [],
            'nginx' => []
        ];

        if ( $this->system_tweaks ) {
             $snippets['htaccess']['system'] = $this->system_tweaks->get_htaccess_rules();
             $snippets['nginx']['system'] = $this->system_tweaks->get_nginx_rules();
        }
        
        return $snippets;
    }

    public function check_file_permissions() {
        return $this->run_file_perms_check();
    }

    /**
     * Build manual copy/paste configuration rules from current hardening state.
     *
     * The output is stable and generated directly from persisted settings.
     * It does not depend on transient runtime toggles.
     *
     * @return array<string,mixed>
     */
    public function get_manual_config_rules(): array {
        $settings = $this->settings;
        $wp_raw   = trim( $this->get_wp_config_manual_rules_raw( $settings ) );

        $apache_rules = \SecurWP\Includes\ServerConfigWriter::get_managed_rules_for_server( 'apache' );
        $nginx_rules  = \SecurWP\Includes\ServerConfigWriter::get_managed_rules_for_server( 'nginx' );
        $wp_rules     = $this->wrap_manual_rules( $wp_raw, 'wp-config' );

        $detected_server = \SecurWP\Includes\ServerConfigWriter::detect_server();
        $server_path     = \SecurWP\Includes\ServerConfigWriter::get_config_file_path( $detected_server );
        $server_rules    = '';

        if ( in_array( $detected_server, [ 'apache', 'litespeed' ], true ) ) {
            $server_rules = $apache_rules;
            if ( '' === $server_path ) {
                $server_path = ABSPATH . '.htaccess';
            }
        } elseif ( 'nginx' === $detected_server ) {
            $server_rules = $nginx_rules;
            if ( '' === $server_path ) {
                $server_path = \SecurWP\Includes\ServerConfigWriter::get_nginx_path();
            }
        } else {
            // Unknown server: prefer Apache format first because many shared hosts
            // expose Apache-compatible stacks via proxies.
            $server_rules = '' !== $apache_rules ? $apache_rules : $nginx_rules;
            if ( '' === $server_path ) {
                $server_path = '' !== $apache_rules
                    ? ABSPATH . '.htaccess'
                    : \SecurWP\Includes\ServerConfigWriter::get_nginx_path();
            }
        }

        return [
            'detected_server'      => $detected_server,
            'server_target_path'   => $server_path,
            'wp_config_path'       => $this->resolve_wp_config_path_for_rules(),
            'server_rules'         => $server_rules,
            'server_rules_apache'  => $apache_rules,
            'server_rules_nginx'   => $nginx_rules,
            'wp_config_rules'      => $wp_rules,
            'has_server_rules'     => '' !== $server_rules,
            'has_wp_config_rules'  => '' !== $wp_rules,
        ];
    }

    /**
     * Generate raw wp-config.php rule lines from hardening settings.
     *
     * @param array<string,mixed> $settings
     */
    private function get_wp_config_manual_rules_raw( array $settings ): string {
        $lines = [];

        if ( ! empty( $settings['disable_file_editor'] ) ) {
            $lines[] = "// Disable Theme & Plugin Editor (SecurWP > Hardening > WordPress Obscurity)";
            $lines[] = "if ( ! defined( 'DISALLOW_FILE_EDIT' ) ) {";
            $lines[] = "    define( 'DISALLOW_FILE_EDIT', true );";
            $lines[] = "}";
        }

        return implode( "\n", $lines );
    }

    /**
     * Wrap generated rule snippets with SecurWP begin/end markers.
     */
    private function wrap_manual_rules( string $rules, string $type ): string {
        $rules = trim( $rules );
        if ( '' === $rules ) {
            return '';
        }

        $delimiter = 'wp-config' === $type ? '//' : '#';

        return implode(
            "\n",
            [
                $delimiter . ' BEGIN SecurWP - Do not modify or remove this line',
                $delimiter . ' SecurWP Config Details: 1',
                $rules,
                $delimiter . ' END SecurWP - Do not modify or remove this line',
            ]
        );
    }

    /**
     * Resolve wp-config.php path for manual instructions.
     */
    private function resolve_wp_config_path_for_rules(): string {
        $path = ConfigFilePolicy::get_wp_config_path();

        return is_string( $path ) && '' !== $path
            ? $path
            : ABSPATH . 'wp-config.php';
    }

    public function handle_failed_login( $username, $error = null ) {
        if ( ! self::should_process_failed_login_signal( is_scalar( $username ) ? (string) $username : '' ) ) {
            return;
        }

        $ip                 = $this->get_ip_address();
        $time_window        = absint( $this->settings['limit_login_attempts_window'] );
        $lockout_duration   = absint( $this->settings['limit_login_attempts_lockout'] );
        $lockout_subject    = self::resolve_login_lockout_subject( $username );
        $display_identifier = $lockout_subject['display'] ?? self::sanitize_login_identifier_input( $username );
        $lockout_activated = false;

        // Auto-ban "admin" username: instantly lock out the IP on the first
        // attempt. Only fires when no real user is named 'admin', so sites
        // that have not yet renamed their admin account are not broken.
        if ( ! empty( $this->settings['auto_ban_admin'] )
            && 'admin' === strtolower( (string) $username )
            && $ip
            && ! username_exists( 'admin' )
        ) {
            $locked_at = time();
            $lockout_key = 'securwp_lockout_' . md5( $ip );
            set_transient( $lockout_key, $locked_at, $lockout_duration );
            LockoutRegistry::add_ip_lockout( $ip, $locked_at );
            do_action( 'securwp_lockout_activated', $ip, 'ip' );
            AuditLog::add_log(
                'IP Lockout',
                [
                    'ip'              => $ip,
                    'type'            => 'ip',
                    'reason'          => 'admin_username_attempt',
                    'triggered_after' => 1,
                    'window_seconds'  => $time_window,
                    'lockout_seconds' => $lockout_duration,
                ],
                'warning',
                'authentication'
            );
            if ( $this->should_render_browser_login_lockout_response() ) {
                $context = $this->get_active_login_lockout_context( (string) $username );
                if ( null !== $context ) {
                    $this->render_login_lockout_page( (int) $context['remaining'] );
                }
            }
            return;
        }

        if ( $ip && ! empty( $this->settings['limit_login_attempts_enabled'] ) ) {
            $attempts_data = $this->record_login_attempt( 'ip', $ip, $time_window );

            $max_attempts_ip = absint( $this->settings['limit_login_attempts_max'] );

            if ( $attempts_data['count'] >= $max_attempts_ip && $max_attempts_ip > 0 ) {
                if ( ( time() - $attempts_data['first_attempt'] ) <= $time_window ) {
                    $locked_at = time();
                    $lockout_key = 'securwp_lockout_' . md5( $ip );
                    set_transient( $lockout_key, $locked_at, $lockout_duration );
                    LockoutRegistry::add_ip_lockout( $ip, $locked_at );
                    do_action( 'securwp_lockout_activated', $ip, 'ip' );
                    AuditLog::add_log(
                        'IP Lockout',
                        [
                            'ip'              => $ip,
                            'type'            => 'ip',
                            'triggered_after' => (int) $attempts_data['count'],
                            'window_seconds'  => $time_window,
                            'lockout_seconds' => $lockout_duration,
                        ],
                        'warning',
                        'authentication'
                    );
                    $this->clear_login_attempts( 'ip', $ip );
                    $lockout_activated = true;
                }
            }
        }

        if ( ! empty( $lockout_subject['key'] ) && ! empty( $this->settings['limit_login_attempts_enabled'] ) ) {
            $user_attempts = $this->record_login_attempt( 'user', (string) $lockout_subject['key'], $time_window );

            $max_attempts_user = absint( $this->settings['limit_login_attempts_max_user'] ?? 10 );

            if ( $user_attempts['count'] >= $max_attempts_user && $max_attempts_user > 0 ) {
                 if ( ( time() - $user_attempts['first_attempt'] ) <= $time_window ) {
                    $locked_at = time();
                    $user_lockout_key = 'securwp_lockout_user_' . md5( (string) $lockout_subject['key'] );
                    set_transient( $user_lockout_key, $locked_at, $lockout_duration );
                    LockoutRegistry::add_user_lockout( (string) $lockout_subject['key'], $display_identifier, $locked_at );
                    do_action( 'securwp_lockout_activated', $display_identifier, 'user' );
                    AuditLog::add_log(
                        'User Lockout',
                        [
                            'username'        => $display_identifier,
                            'type'            => 'user',
                            'lockout_key'     => (string) $lockout_subject['key'],
                            'triggered_after' => (int) $user_attempts['count'],
                            'window_seconds'  => $time_window,
                            'lockout_seconds' => $lockout_duration,
                        ],
                        'warning',
                        'authentication'
                    );
                    $this->clear_login_attempts( 'user', (string) $lockout_subject['key'] );
                    $lockout_activated = true;
                 }
            }
        }

        if ( $lockout_activated && $this->should_render_browser_login_lockout_response() ) {
            $context = $this->get_active_login_lockout_context( (string) $username );
            if ( null !== $context ) {
                $this->render_login_lockout_page( (int) $context['remaining'] );
            }
        }
    }

    /**
     * Record a failed login attempt in the plugin-owned attempt store.
     *
     * @return array{count:int,first_attempt:int}
     */
    private function record_login_attempt( string $subject_type, string $subject_key, int $time_window ): array {
        $store = new LoginAttemptStore();
        return $store->record_attempt( $subject_type, $subject_key, $time_window );
    }

    /**
     * Clear failed-login attempts for a subject after a lockout is applied.
     */
    private function clear_login_attempts( string $subject_type, string $subject_key ): void {
        $store = new LoginAttemptStore();
        $store->clear_attempts( $subject_type, $subject_key );
    }

    /**
     * Remove expired login attempt counters to keep the store compact.
     */
    public function cleanup_expired_login_attempts(): void {
        $store = new LoginAttemptStore();
        $store->purge_expired();
    }

    /**
     * Render the dedicated SecurWP lockout page for browser login requests
     * before the login form or any downstream error masking can run.
     */
    public function enforce_browser_login_lockout(): void {
        if ( ! $this->should_render_browser_login_lockout_response() ) {
            return;
        }

        $context = $this->get_active_login_lockout_context( $this->get_requested_login_identifier() );
        if ( null === $context ) {
            return;
        }

        $this->render_login_lockout_page( (int) $context['remaining'] );
    }

    public function check_login_lockout( $user, $username, $password ) {
        // XML-RPC authentication is hardened separately and must not inherit
        // browser login-security lockouts.
        if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
            return $user;
        }

        $context = $this->get_active_login_lockout_context( is_string( $username ) ? $username : '' );
        if ( null === $context ) {
            return $user;
        }

        if ( $this->should_render_browser_login_lockout_response() ) {
            $this->render_login_lockout_page( (int) $context['remaining'] );
        }

        $time_remaining = (int) $context['remaining'];
        if ( $context['type'] === 'ip' ) {
            $error_msg = sprintf(
                /* translators: %s: human-readable time remaining until the IP lockout expires. */
                __( '<strong>ERROR</strong>: Too many failed login attempts from this IP. Please try again in %s.', 'securwp' ),
                human_time_diff( time(), time() + $time_remaining )
            );
            return new \WP_Error( 'securwp_login_lockout', $error_msg );
        }

        $error_msg = sprintf(
            /* translators: %s: human-readable time remaining until the account lockout expires. */
            __( '<strong>ERROR</strong>: This account has failed too many logins. Please try again in %s.', 'securwp' ),
            human_time_diff( time(), time() + $time_remaining )
        );
        return new \WP_Error( 'securwp_login_lockout_user', $error_msg );
    }

    /**
     * Resolve the currently active login lockout for this visitor or the
     * submitted username, clearing expired lockouts as they are encountered.
     *
     * @return array{type: string, identifier: string, remaining: int}|null
     */
    private function get_active_login_lockout_context( string $username = '' ): ?array {
        $ip = $this->get_ip_address();
        if ( $ip !== '' ) {
            $lockout_key        = 'securwp_lockout_' . md5( $ip );
            $lockout_start_time = get_transient( $lockout_key );
            if ( false !== $lockout_start_time ) {
                $lockout_duration = absint( $this->settings['limit_login_attempts_lockout'] );
                $time_remaining   = ( (int) $lockout_start_time + $lockout_duration ) - time();

                if ( $time_remaining > 0 ) {
                    return [
                        'type'       => 'ip',
                        'identifier' => $ip,
                        'remaining'  => $time_remaining,
                    ];
                }

                delete_transient( $lockout_key );
                LockoutRegistry::clear_ip_lockout( $ip );
                do_action( 'securwp_lockout_cleared', $ip, 'ip' );
            }
        }

        $candidates = self::get_user_lockout_key_candidates( $username );
        if ( empty( $candidates ) ) {
            return null;
        }

        $lockout_duration = absint( $this->settings['limit_login_attempts_lockout'] );
        foreach ( $candidates as $identifier ) {
            $user_lockout_key   = 'securwp_lockout_user_' . md5( $identifier );
            $lockout_start_time = get_transient( $user_lockout_key );
            if ( false === $lockout_start_time ) {
                continue;
            }

            $time_remaining = ( (int) $lockout_start_time + $lockout_duration ) - time();
            if ( $time_remaining > 0 ) {
                return [
                    'type'       => 'user',
                    'identifier' => $identifier,
                    'remaining'  => $time_remaining,
                ];
            }

            delete_transient( $user_lockout_key );
            LockoutRegistry::clear_user_lockouts( [ $identifier ] );
            do_action( 'securwp_lockout_cleared', $identifier, 'user' );
        }

        return null;
    }

    /**
     * Determine whether a wp_login_failed signal represents a real
     * credential submission that should affect lockout counters.
     *
     * Some login-page GET requests can trigger wp_login_failed indirectly via
     * auth hooks. Lockout counting must only consume trusted browser-login
     * submission paths. XML-RPC is hardened separately and must never mutate
     * login-security counters or audit events.
     */
    public static function should_process_failed_login_signal( string $identifier = '' ): bool {
        if ( wp_doing_cron() ) {
            return false;
        }

        $remote_ip = sanitize_text_field( (string) wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
        $server_ip = sanitize_text_field( (string) wp_unslash( $_SERVER['SERVER_ADDR'] ?? '' ) );
        $loopback  = [ '127.0.0.1', '::1' ];

        if ( $server_ip !== '' ) {
            $loopback[] = $server_ip;
        }

        if ( in_array( $remote_ip, $loopback, true ) ) {
            return false;
        }

        if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
            return false;
        }

        $method = isset( $_SERVER['REQUEST_METHOD'] )
            ? strtoupper( sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_METHOD'] ) ) )
            : 'GET';

        if ( 'POST' !== $method ) {
            return false;
        }

        return self::request_contains_login_payload();
    }

    /**
     * Restrict the dedicated lockout page to interactive browser requests
     * against wp-login.php so API/system auth paths stay on WP_Error handling.
     */
    private function should_render_browser_login_lockout_response(): bool {
        if ( wp_doing_ajax() || wp_doing_cron() ) {
            return false;
        }

        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return false;
        }

        if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
            return false;
        }

        if ( defined( 'WP_CLI' ) && WP_CLI ) {
            return false;
        }

        if ( is_user_logged_in() ) {
            return false;
        }

        if ( $this->is_wp_login_request_script() ) {
            return true;
        }

        return $this->is_renamed_login_route_request();
    }

    /**
     * Detect direct wp-login.php execution.
     */
    private function is_wp_login_request_script(): bool {
        $script_name = self::read_server_scalar( 'SCRIPT_NAME' );
        $php_self    = self::read_server_scalar( 'PHP_SELF' );

        return basename( $script_name ?: $php_self ) === 'wp-login.php';
    }

    /**
     * Detect requests hitting the configured renamed login URL before the Login
     * module rewrites them to wp-login.php.
     */
    private function is_renamed_login_route_request(): bool {
        if ( empty( $this->settings['rename_login_url_enabled'] ) ) {
            return false;
        }

        $login_slug = sanitize_title( (string) ( $this->settings['rename_login_url_slug'] ?? '' ) );
        if ( $login_slug === '' ) {
            return false;
        }

        return $this->get_current_request_path() === $login_slug;
    }

    /**
     * Return true when the current request contains credential-submit fields.
     */
    private static function request_contains_login_payload(): bool {
        $keys = [ 'log', 'username', 'user_login', 'pwd', 'password' ];

        foreach ( $keys as $key ) {
            // phpcs:disable WordPress.Security.NonceVerification.Missing
            if ( isset( $_POST[ $key ] ) ) {
                return true;
            }
            // phpcs:enable WordPress.Security.NonceVerification.Missing
        }

        return false;
    }

    /**
     * Normalize the current request path relative to the WordPress site base.
     */
    private function get_current_request_path(): string {
        $request_uri = self::read_server_scalar( 'REQUEST_URI' );
        if ( $request_uri === '' ) {
            return '';
        }

        $path = wp_parse_url( $request_uri, PHP_URL_PATH );
        if ( ! is_string( $path ) || $path === '' ) {
            return '';
        }

        $normalized = trim( rawurldecode( $path ), '/' );
        if ( $normalized === '' ) {
            return '';
        }

        $site_base = trim( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ), '/' );
        if ( $site_base !== '' ) {
            if ( $normalized === $site_base ) {
                return '';
            }

            $prefix = $site_base . '/';
            if ( str_starts_with( $normalized, $prefix ) ) {
                $normalized = substr( $normalized, strlen( $prefix ) );
            }
        }

        return trim( $normalized, '/' );
    }

    /**
     * Resolve the submitted login identifier from the current request payload.
     */
    private function get_requested_login_identifier(): string {
        // phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended
        $candidates = [
            self::read_request_scalar( $_POST, 'log' ),
            self::read_request_scalar( $_POST, 'username' ),
            self::read_request_scalar( $_POST, 'user_login' ),
            self::read_request_scalar( $_REQUEST, 'log' ),
            self::read_request_scalar( $_REQUEST, 'username' ),
            self::read_request_scalar( $_REQUEST, 'user_login' ),
        ];
        // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended

        foreach ( $candidates as $candidate ) {
            $normalized = $this->normalize_login_identifier( $candidate );
            if ( $normalized !== '' ) {
                return $normalized;
            }
        }

        return '';
    }

    /**
     * Read a scalar request value, unslash it, and sanitize it for downstream checks.
     *
     * @param array<string,mixed> $source
     */
    private static function read_request_scalar( array $source, string $key ): string {
        if ( ! array_key_exists( $key, $source ) || ! is_scalar( $source[ $key ] ) ) {
            return '';
        }

        return sanitize_text_field( wp_unslash( (string) $source[ $key ] ) );
    }

    /**
     * Read a scalar server value and normalize it before routing checks.
     */
    private static function read_server_scalar( string $key ): string {
        if ( ! isset( $_SERVER[ $key ] ) || ! is_scalar( $_SERVER[ $key ] ) ) {
            return '';
        }

        return sanitize_text_field( wp_unslash( (string) $_SERVER[ $key ] ) );
    }

    /**
     * Normalize a username/email value to the stored login-lockout key format.
     *
     * @param mixed $value
     */
    public static function normalize_login_identifier( $value ): string {
        $normalized = self::sanitize_login_identifier_input( $value );
        if ( $normalized === '' ) {
            return '';
        }

        if ( function_exists( 'mb_strtolower' ) ) {
            return mb_strtolower( $normalized, 'UTF-8' );
        }

        return strtolower( $normalized );
    }

    /**
     * Return the canonical identifier first, then the legacy raw form for
     * backward-compatible lockout reads during upgrade.
     *
     * @param mixed $value
     * @return string[]
     */
    public static function get_login_identifier_candidates( $value ): array {
        $raw = self::sanitize_login_identifier_input( $value );
        if ( $raw === '' ) {
            return [];
        }

        $candidates = [ self::normalize_login_identifier( $raw ) ];
        if ( $raw !== $candidates[0] ) {
            $candidates[] = $raw;
        }

        return array_values( array_unique( array_filter( $candidates, static function ( $candidate ): bool {
            return is_string( $candidate ) && $candidate !== '';
        } ) ) );
    }

    /**
     * Clear current and legacy user lockout keys for the provided identifier.
     *
     * @param mixed $value
     */
    public static function clear_user_lockout( $value ): void {
        $cleared_identifiers = [];
        foreach ( self::get_user_lockout_key_candidates( $value ) as $identifier ) {
            delete_transient( 'securwp_lockout_user_' . md5( $identifier ) );
            $cleared_identifiers[] = $identifier;
        }

        if ( ! empty( $cleared_identifiers ) ) {
            LockoutRegistry::clear_user_lockouts( $cleared_identifiers );
        }
    }

    /**
     * Remove a current IP lockout.
     */
    public static function clear_ip_lockout( string $ip ): void {
        $ip = sanitize_text_field( trim( $ip ) );
        if ( '' === $ip ) {
            return;
        }

        delete_transient( 'securwp_lockout_' . md5( $ip ) );
        LockoutRegistry::clear_ip_lockout( $ip );
    }

    /**
     * Clear failed-login attempt counters for an IP after manual recovery.
     */
    public static function clear_ip_login_attempts( string $ip ): void {
        $ip = sanitize_text_field( trim( $ip ) );
        if ( '' === $ip ) {
            return;
        }

        $store = new LoginAttemptStore();
        $store->clear_attempts( 'ip', $ip );
    }

    /**
     * Clear failed-login attempt counters for every known key form of a user.
     *
     * Manual unlock should reset the associated counter state so the next
     * failed login starts a fresh observation window.
     *
     * @param mixed $value Username, email, or canonical lockout key.
     */
    public static function clear_user_login_attempts( $value ): void {
        $store = new LoginAttemptStore();

        foreach ( self::get_user_lockout_key_candidates( $value ) as $identifier ) {
            $store->clear_attempts( 'user', $identifier );
        }
    }

    /**
     * Resolve the canonical lockout subject for a submitted login identifier.
     *
     * Existing accounts are keyed by user ID so username/email variants share
     * one counter. Unknown identifiers keep a normalized input-based key.
     *
     * @param mixed $value
     * @return array{key:string,display:string}|null
     */
    public static function resolve_login_lockout_subject( $value ): ?array {
        $raw = self::sanitize_login_identifier_input( $value );
        if ( $raw === '' ) {
            return null;
        }

        $user = self::find_login_user( $raw );
        if ( $user instanceof \WP_User ) {
            return [
                'key'     => 'user:' . $user->ID,
                'display' => (string) $user->user_login,
            ];
        }

        return [
            'key'     => 'identifier:' . self::normalize_login_identifier( $raw ),
            'display' => $raw,
        ];
    }

    /**
     * Return canonical and legacy candidate keys for user lockout lookup.
     *
     * @param mixed $value
     * @return string[]
     */
    public static function get_user_lockout_key_candidates( $value ): array {
        $raw = self::sanitize_login_identifier_input( $value );
        if ( $raw === '' ) {
            return [];
        }

        $candidates = [];
        $subject = self::resolve_login_lockout_subject( $raw );
        if ( is_array( $subject ) && ! empty( $subject['key'] ) ) {
            $candidates[] = (string) $subject['key'];
        }

        $normalized = self::normalize_login_identifier( $raw );
        if ( $normalized !== '' ) {
            $candidates[] = $normalized;
        }

        $candidates[] = $raw;

        $user = self::find_login_user( $raw );
        if ( $user instanceof \WP_User ) {
            $candidates[] = (string) $user->user_login;
            if ( ! empty( $user->user_email ) ) {
                $candidates[] = self::normalize_login_identifier( (string) $user->user_email );
                $candidates[] = (string) $user->user_email;
            }
        }

        return array_values( array_unique( array_filter( $candidates, 'is_string' ) ) );
    }

    /**
     * Sanitize a login identifier without changing its semantic value.
     *
     * @param mixed $value
     */
    public static function sanitize_login_identifier_input( $value ): string {
        if ( ! is_scalar( $value ) ) {
            return '';
        }

        return sanitize_text_field( trim( (string) wp_unslash( (string) $value ) ) );
    }

    /**
     * Resolve the submitted login identifier to an existing user when possible.
     */
    private static function find_login_user( string $value ): ?\WP_User {
        if ( $value === '' ) {
            return null;
        }

        if ( is_email( $value ) ) {
            $email_user = get_user_by( 'email', sanitize_email( $value ) );
            if ( $email_user instanceof \WP_User ) {
                return $email_user;
            }
        }

        $login = sanitize_user( $value, false );
        if ( $login === '' ) {
            return null;
        }

        $login_user = get_user_by( 'login', $login );
        return $login_user instanceof \WP_User ? $login_user : null;
    }

    /**
     * Render the shared SecurWP block page for login lockouts.
     */
    private function render_login_lockout_page( int $time_remaining ): void {
        $time_remaining = max( 1, $time_remaining );
        $message        = sprintf(
            /* translators: %s: human-readable time remaining until login access is restored. */
            __( 'Authentication access is temporarily restricted after repeated failed sign-in attempts. Please wait %s before trying again.', 'securwp' ),
            human_time_diff( time(), time() + $time_remaining )
        );

        if ( ! headers_sent() ) {
            http_response_code( 403 );
            header( 'Content-Type: text/html; charset=UTF-8' );
            header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
        }

        require_once dirname( __DIR__ ) . '/Firewall/block-page.php';
        securwp_render_block_page( 403, $message, IpDetector::get_ip() );
        exit;
    }

    /**
     * Check the primary website password against HIBP at login time.
     *
     * If the password is found in data breaches, flag the user for a forced
     * password change. This runs on 'wp_authenticate_user', so it evaluates
     * only the primary password-auth path after WordPress has already verified
     * the submitted password against the main user_pass hash.
     *
     * Hooked to 'wp_authenticate_user' before the strong-password check so the
     * more severe breach remediation takes precedence when both apply.
     *
     * @param \WP_User|\WP_Error $user     The authenticated user or error.
     * @param string             $password The plaintext primary password.
     * @return \WP_User|\WP_Error Unchanged - flagging is done via user meta.
     */
    public function check_breached_password_on_login( $user, $password ) {
        // Only check if primary-password authentication succeeded and we have
        // the already-verified plaintext password from wp_authenticate_user.
        if ( ! ( $user instanceof \WP_User ) || empty( $password ) ) {
            return $user;
        }

        // Skip if an active password-change requirement already applies.
        if ( $this->get_effective_password_change_reason( $user ) !== null ) {
            return $user;
        }

        $cached_result = $this->get_cached_breached_password_evaluation( $user );
        if ( is_array( $cached_result ) && ! $cached_result['stale'] ) {
            if ( $cached_result['breached'] ) {
                update_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META, 'breached' );
            }

            return $user;
        }

        $result = $this->breached_password_checker->check_password( $password );
        if ( ! empty( $result['checked'] ) ) {
            $this->cache_breached_password_evaluation( $user, $result );
        } elseif ( is_array( $cached_result ) ) {
            $result = [
                'breached' => $cached_result['breached'],
                'count'    => $cached_result['count'],
                'checked'  => true,
            ];
        }

        if ( $result['breached'] ) {
            update_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META, 'breached' );
        }

        return $user;
    }

    private function get_ip_address() {
        return IpDetector::get_ip();
    }

    private function run_file_perms_check(): array {
        $abs_path            = trailingslashit( ABSPATH );
        $directory_baseline  = $this->get_filesystem_baseline_mode( true );
        $file_baseline       = $this->get_filesystem_baseline_mode( false );
        $results             = [];
        $seen                = [];

        foreach ( $this->get_file_permission_audit_targets() as $target ) {
            $path = isset( $target['path'] ) ? (string) $target['path'] : '';
            if ( $path === '' ) {
                continue;
            }

            $normalized_path = wp_normalize_path( untrailingslashit( $path ) );
            if ( isset( $seen[ $normalized_path ] ) ) {
                continue;
            }

            $seen[ $normalized_path ] = true;
            $results[]                = $this->build_file_permission_audit_result(
                $target,
                $abs_path,
                $directory_baseline,
                $file_baseline
            );
        }

        usort(
            $results,
            static function ( array $left, array $right ): int {
                $status_order = [
                    'critical'    => 0,
                    'warning'     => 1,
                    'unavailable' => 2,
                    'ok'          => 3,
                    'hardened'    => 4,
                ];

                $left_status  = $status_order[ $left['status'] ] ?? 99;
                $right_status = $status_order[ $right['status'] ] ?? 99;
                if ( $left_status !== $right_status ) {
                    return $left_status <=> $right_status;
                }

                return strcmp( (string) $left['path'], (string) $right['path'] );
            }
        );

        $summary = [
            'ok'          => 0,
            'hardened'    => 0,
            'warning'     => 0,
            'critical'    => 0,
            'unavailable' => 0,
            'total'       => count( $results ),
        ];

        foreach ( $results as $result ) {
            $status = (string) ( $result['status'] ?? 'unavailable' );
            if ( isset( $summary[ $status ] ) ) {
                $summary[ $status ]++;
            }
        }

        return [
            'generated_at' => time(),
            'baseline'     => [
                'directory' => sprintf( '%o', $directory_baseline ),
                'file'      => sprintf( '%o', $file_baseline ),
            ],
            'summary'      => $summary,
            'notes'        => [
                'Stricter-than-baseline permissions are acceptable when the site remains functional.',
                'Review paths marked warning or critical. They are more permissive than the compatible baseline for this host.',
            ],
            'items'        => $results,
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function get_file_permission_audit_targets(): array {
        $targets     = [];
        $upload_dir  = wp_upload_dir();
        $theme_root  = function_exists( 'get_theme_root' ) ? (string) get_theme_root() : '';
        $server_type = \SecurWP\Includes\ServerConfigWriter::detect_server();
        $server_path = \SecurWP\Includes\ServerConfigWriter::get_config_file_path( $server_type );

        $targets[] = [
            'id'          => 'wordpress-root',
            'label'       => 'WordPress Root',
            'path'        => ABSPATH,
            'type'        => 'dir',
            'category'    => 'core',
            'sensitivity' => 'standard',
        ];
        $targets[] = [
            'id'          => 'wp-admin',
            'label'       => 'wp-admin',
            'path'        => ABSPATH . 'wp-admin',
            'type'        => 'dir',
            'category'    => 'core',
            'sensitivity' => 'standard',
        ];
        $targets[] = [
            'id'          => 'wp-includes',
            'label'       => 'wp-includes',
            'path'        => ABSPATH . 'wp-includes',
            'type'        => 'dir',
            'category'    => 'core',
            'sensitivity' => 'standard',
        ];
        $targets[] = [
            'id'          => 'wp-content',
            'label'       => 'wp-content',
            'path'        => WP_CONTENT_DIR,
            'type'        => 'dir',
            'category'    => 'content',
            'sensitivity' => 'standard',
        ];
        $targets[] = [
            'id'          => 'plugins-directory',
            'label'       => 'Plugins Directory',
            'path'        => WP_PLUGIN_DIR,
            'type'        => 'dir',
            'category'    => 'content',
            'sensitivity' => 'standard',
        ];

        if ( $theme_root !== '' ) {
            $targets[] = [
                'id'          => 'themes-directory',
                'label'       => 'Themes Directory',
                'path'        => $theme_root,
                'type'        => 'dir',
                'category'    => 'content',
                'sensitivity' => 'standard',
            ];
        }

        if ( ! empty( $upload_dir['basedir'] ) ) {
            $targets[] = [
                'id'          => 'uploads-directory',
                'label'       => 'Uploads Directory',
                'path'        => (string) $upload_dir['basedir'],
                'type'        => 'dir',
                'category'    => 'content',
                'sensitivity' => 'standard',
            ];
        }

        $securwp_data_dir = WP_CONTENT_DIR . '/securwp-data';
        $targets[]         = [
            'id'          => 'securwp-data-directory',
            'label'       => 'securwp-data',
            'path'        => $securwp_data_dir,
            'type'        => 'dir',
            'category'    => 'content',
            'sensitivity' => 'standard',
        ];

        if ( defined( 'WPMU_PLUGIN_DIR' ) && is_string( WPMU_PLUGIN_DIR ) && WPMU_PLUGIN_DIR !== '' ) {
            $targets[] = [
                'id'          => 'mu-plugins-directory',
                'label'       => 'Must-Use Plugins Directory',
                'path'        => WPMU_PLUGIN_DIR,
                'type'        => 'dir',
                'category'    => 'content',
                'sensitivity' => 'standard',
            ];
        }

        $wp_config_path = ConfigFilePolicy::get_wp_config_path();
        if ( is_string( $wp_config_path ) && $wp_config_path !== '' ) {
            $targets[] = [
                'id'          => 'wp-config',
                'label'       => 'wp-config.php',
                'path'        => $wp_config_path,
                'type'        => 'file',
                'category'    => 'config',
                'sensitivity' => 'sensitive',
            ];
        }

        if ( in_array( $server_type, [ 'apache', 'litespeed' ], true ) ) {
            $targets[] = [
                'id'          => 'server-config',
                'label'       => '.htaccess',
                'path'        => $server_path !== '' ? $server_path : ABSPATH . '.htaccess',
                'type'        => 'file',
                'category'    => 'config',
                'sensitivity' => 'sensitive',
            ];
        } elseif ( $server_type === 'nginx' ) {
            $nginx_path = $server_path !== '' ? $server_path : \SecurWP\Includes\ServerConfigWriter::get_nginx_path();
            if ( $nginx_path !== '' ) {
                $targets[] = [
                    'id'          => 'server-config',
                    'label'       => 'Nginx Rules File',
                    'path'        => $nginx_path,
                    'type'        => 'file',
                    'category'    => 'config',
                    'sensitivity' => 'sensitive',
                ];
            }
        }

        return $targets;
    }

    /**
     * @param array<string,mixed> $target
     * @return array<string,mixed>
     */
    private function build_file_permission_audit_result(
        array $target,
        string $abs_path,
        int $directory_baseline,
        int $file_baseline
    ): array {
        $path        = (string) $target['path'];
        $type        = (string) $target['type'];
        $baseline    = $type === 'dir' ? $directory_baseline : $file_baseline;
        $exists      = $type === 'dir' ? is_dir( $path ) : is_file( $path );
        $current     = null;
        $status      = 'unavailable';
        $message     = 'This path is not available on the current filesystem.';
        $reason      = 'missing';
        $can_read    = false;

        if ( $exists ) {
            $perms = @fileperms( $path );
            if ( is_int( $perms ) ) {
                $can_read = true;
                $current  = $perms & 0777;
                [ $status, $reason, $message ] = $this->classify_permission_audit_status(
                    $current,
                    $baseline,
                    $type,
                    (string) ( $target['sensitivity'] ?? 'standard' )
                );
            } else {
                $status  = 'unavailable';
                $reason  = 'metadata_unavailable';
                $message = 'Permission metadata could not be read for this path. Review ownership and local filesystem access.';
            }
        }

        [ $display_status, $display_label ] = $this->get_file_permission_display_status_meta( $status );

        return [
            'id'                  => (string) $target['id'],
            'label'               => (string) $target['label'],
            'path'                => $this->get_audit_display_path( $path, $abs_path ),
            'type'                => $type,
            'category'            => (string) ( $target['category'] ?? 'core' ),
            'status'              => $status,
            'display_status'      => $display_status,
            'display_label'       => $display_label,
            'reason'              => $reason,
            'exists'              => $exists,
            'readable'            => $can_read,
            'current'             => is_int( $current ) ? sprintf( '%o', $current ) : null,
            'compatible_baseline' => sprintf( '%o', $baseline ),
            'message'             => $message,
        ];
    }

    /**
     * @return array{0:string,1:string}
     */
    private function get_file_permission_display_status_meta( string $status ): array {
        if ( in_array( $status, [ 'critical', 'warning' ], true ) ) {
            return [ 'warning', 'Warning' ];
        }

        if ( in_array( $status, [ 'ok', 'hardened' ], true ) ) {
            return [ 'ok', 'OK' ];
        }

        return [ 'unavailable', 'Unavailable' ];
    }

    /**
     * @return array{0:string,1:string,2:string}
     */
    private function classify_permission_audit_status(
        int $current_mode,
        int $baseline_mode,
        string $type,
        string $sensitivity
    ): array {
        $world_writable   = (bool) ( $current_mode & 0002 );
        $group_writable   = (bool) ( $current_mode & 0020 );
        $extra_bits       = $current_mode & ( ~$baseline_mode & 0777 );
        $file_executable  = $type === 'file' && (bool) ( $current_mode & 0111 );
        $is_sensitive     = $sensitivity === 'sensitive';
        $is_within_target = ( $current_mode | $baseline_mode ) === $baseline_mode;

        if ( $world_writable ) {
            return [
                'critical',
                'world_writable',
                'This path is writable by any local account. Restrict write access immediately.',
            ];
        }

        if ( $group_writable && $is_sensitive ) {
            return [
                'critical',
                'sensitive_group_writable',
                'This sensitive configuration path is writable outside the owner account. Tighten it if the site can still operate normally.',
            ];
        }

        if ( $group_writable ) {
            return [
                'warning',
                'group_writable',
                'This path is writable outside the owner account. Review whether group write access is intentional on this host.',
            ];
        }

        if ( $file_executable ) {
            return [
                'warning',
                'unexpected_executable',
                'This file has executable permission bits set. Review whether that is required on this host.',
            ];
        }

        if ( $extra_bits !== 0 ) {
            return [
                'warning',
                'more_permissive_than_baseline',
                'Permissions are more permissive than the compatible baseline for this path.',
            ];
        }

        if ( $current_mode === $baseline_mode ) {
            return [
                'ok',
                'baseline',
                'Permissions match the compatible baseline for this path.',
            ];
        }

        if ( $is_within_target ) {
            return [
                'hardened',
                'stricter_than_baseline',
                'Permissions are stricter than the compatible baseline and appear intentionally hardened.',
            ];
        }

        return [
            'warning',
            'unexpected_mode',
            'Permissions could not be classified cleanly. Review this path manually.',
        ];
    }

    private function get_filesystem_baseline_mode( bool $directory ): int {
        $constant = $directory ? 'FS_CHMOD_DIR' : 'FS_CHMOD_FILE';
        $default  = $directory ? 0755 : 0644;

        if ( defined( $constant ) ) {
            $value = constant( $constant );
            if ( is_int( $value ) ) {
                return $value & 0777;
            }
        }

        return $default;
    }

    private function get_audit_display_path( string $path, string $abs_path ): string {
        $normalized_root = wp_normalize_path( untrailingslashit( $abs_path ) );
        $normalized_path = wp_normalize_path( untrailingslashit( $path ) );

        if ( $normalized_path === $normalized_root ) {
            return '/';
        }

        $root_prefix = $normalized_root . '/';
        if ( str_starts_with( $normalized_path, $root_prefix ) ) {
            return substr( $normalized_path, strlen( $root_prefix ) );
        }

        return $normalized_path;
    }
}
