<?php

declare(strict_types=1);

namespace SiteFort\Modules\Hardening;

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

use SiteFort\Includes\Interfaces\Module;
use SiteFort\Includes\Module_Loader;
use SiteFort\Modules\AuditLog\AuditLog;
use SiteFort\Modules\Hardening\Tweaks\SystemTweaks;
use SiteFort\Modules\Hardening\Tweaks\WordPressTweaks;
use SiteFort\Includes\DataDirectoryProtection;
use SiteFort\Includes\FilesystemMutation;
use SiteFort\Includes\IpDetector;
use SiteFort\Modules\Hardening\Compatibility\ServerRulePluginConflictDetector;
use SiteFort\Modules\Hardening\Tweaks\BreachedPasswordChecker;
use SiteFort\Modules\Hardening\Tweaks\PasswordChangeInterstitial;
use SiteFort\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 = 'sitefort_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 = 'sitefort_login_attempt_cleanup';
    private const SERVER_RULE_RECONCILE_HOOK = 'sitefort_server_rule_reconcile';
    private const SERVER_RULE_MAINTENANCE_HOOK = 'sitefort_server_rule_maintenance';
    private const SERVER_RULE_VERIFY_HOOK = 'sitefort_server_rule_verify';
    private const USER_NICENAME_REMEDIATION_HOOK = 'sitefort_user_nicename_remediation';
    private const SERVER_RULE_RECONCILE_LOCK = 'sitefort_server_rule_reconcile_lock';
    private const USER_NICENAME_REMEDIATION_CURSOR_OPTION = 'sitefort_user_nicename_remediation_cursor';
    private const USER_NICENAME_REMEDIATION_COMPLETED_OPTION = 'sitefort_user_nicename_remediation_completed';
    private const USER_NICENAME_REMEDIATION_BATCH_SIZE = 50;
    private const LOGIN_IDENTIFIER_MAX_LENGTH = 255;
    private const WP_CONFIG_RULES_HASH_TRANSIENT = 'sitefort_wp_config_rules_hash';
    private const WP_CONFIG_LAST_SYNC_ERROR_TRANSIENT = 'sitefort_wp_config_rules_last_error';
    private const WP_CONFIG_SYNC_LOCK = 'hardening_wp_config_sync';
    private const WP_CONFIG_BEGIN_MARKER = '// BEGIN SiteFort - Do not modify or remove this line';
    private const WP_CONFIG_END_MARKER = '// END SiteFort - Do not modify or remove this line';
    private const PASSWORD_POLICY_STATE_META = 'sitefort_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 !== '' ) {
                \SiteFort\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 = \SiteFort\Includes\SettingsStore::save( \SiteFort\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 ) {
                \SiteFort\Modules\Login\Login::regenerate_access_token();
            }

            // Fire the login-URL-changed event when the slug changes while the feature is on.
            if ( $slug_changed && ! empty( $new['rename_login_url_enabled'] ) && ! empty( $new['rename_login_url_slug'] ) ) {
                do_action( 'sitefort_login_url_changed', home_url( '/' . $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( 'sitefort_server_rules_apache', [ $this, 'contribute_apache_rules' ] );
        add_filter( 'sitefort_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 = \SiteFort\Includes\ServerConfigWriter::build_sync_hash();
        $hash_key     = \SiteFort\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( \SiteFort\Includes\ServerConfigWriter::get_hash_transient_key() );
        \SiteFort\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 : \SiteFort\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 = \SiteFort\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      = \SiteFort\Includes\ServerConfigWriter::get_hash_transient_key();
            $current_hash  = \SiteFort\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 ( \SiteFort\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 ( ! \SiteFort\Includes\ServerConfigWriter::needs_verification_refresh() ) {
            $this->clear_scheduled_server_rule_verification();
            return;
        }

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

    private function load_settings() {
        $defaults = $this->get_default_settings();
        $this->settings = \SiteFort\Includes\SettingsStore::get( \SiteFort\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 ) ) {
                \SiteFort\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 = \SiteFort\Includes\RequestContext::is_privileged() ? self::get_captcha_secret() : '';
        if ( $legacy_shared_secret !== '' ) {
            if ( ! self::has_captcha_secret( $active_provider ) ) {
                \SiteFort\Includes\SecretManager::set( self::get_captcha_secret_name( $active_provider ), $legacy_shared_secret );
                $settings_changed = true;
            }

            \SiteFort\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 ) {
            // In-memory normalization above already serves this request; only the
            // stored row is rewritten, at most once per version.
            $this->persist_hardening_normalization_once();
        }

        // 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( 'sitefort_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';
                }
            }
        }
    }

    /** State-column marker recording the version the stored hardening row was normalized at. */
    private const NORMALIZED_VERSION_STATE_KEY = 'hardening_normalized_ver';

    /**
     * Persist the normalized hardening settings at most once per plugin version,
     * gated by a marker in the state column. The marker is set only on a
     * successful save, so a failed write is retried later.
     *
     * @return void
     */
    private function persist_hardening_normalization_once(): void {
        $version = defined( 'SITEFORT_VERSION' ) ? SITEFORT_VERSION : '';
        $marker  = (string) \SiteFort\Includes\SettingsStore::get_field(
            \SiteFort\Includes\SettingsStore::COL_STATE,
            self::NORMALIZED_VERSION_STATE_KEY,
            ''
        );
        if ( $version !== '' && $marker === $version ) {
            return;
        }

        $saved = \SiteFort\Includes\SettingsStore::save(
            \SiteFort\Includes\SettingsStore::COL_HARDENING,
            $this->settings
        );

        if ( $saved && $version !== '' ) {
            \SiteFort\Includes\SettingsStore::set_field(
                \SiteFort\Includes\SettingsStore::COL_STATE,
                self::NORMALIZED_VERSION_STATE_KEY,
                $version
            );
        }
    }

    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,
            'password_policy_roles' => [ 'administrator' ], // Roles the strong-password policy applies to. Empty = nobody.
            '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

            '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' => false,

            '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 = \SiteFort\Includes\SettingsStore::get( \SiteFort\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 = \SiteFort\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.', 'sitefort' );
            case 'register_slug_required':
                return __( 'Register slug must be "wp-signup.php" or a valid custom slug.', 'sitefort' );
            case 'register_slug_conflict':
                return __( 'Register slug must be different from the custom login slug.', 'sitefort' );
            case 'login_slug_reserved':
                return __( 'Custom login slug conflicts with a protected WordPress endpoint. Choose a different slug.', 'sitefort' );
            case 'register_slug_reserved':
                return __( 'Register slug conflicts with a protected WordPress endpoint. Choose a different slug.', 'sitefort' );
            case 'redirect_url_invalid':
                return __( 'Redirect URL must be a valid site-relative path (for example, / for homepage) or a URL on this site.', 'sitefort' );
            case 'redirect_url_conflict':
                return __( 'Redirect URL conflicts with your protected login/register routes. Choose a different destination.', 'sitefort' );
            case 'redirect_url_reserved':
                return __( 'Redirect URL conflicts with a protected WordPress endpoint. Choose a different destination.', 'sitefort' );
            default:
                return __( 'Invalid login URL configuration.', 'sitefort' );
        }
    }

    /**
     * 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      = \SiteFort\Includes\ServerConfigWriter::get_hash_transient_key();
        $state         = \SiteFort\Includes\ServerConfigWriter::get_state();
        $hash          = \SiteFort\Includes\ServerConfigWriter::build_sync_hash();
        $server        = is_string( $state['server'] ?? null ) ? (string) $state['server'] : \SiteFort\Includes\ServerConfigWriter::detect_server();
        $config_path   = is_string( $state['managed_target_path'] ?? null ) ? (string) $state['managed_target_path'] : '';
        $desired_block = \SiteFort\Includes\ServerConfigWriter::get_managed_rules_for_server( $server );
        $operation     = ! empty( $state['desired_rules_present'] ) ? 'apply' : 'remove';

        if ( empty( $state['drifted'] ) ) {
            \SiteFort\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'] ) ) {
            \SiteFort\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 = \SiteFort\Includes\ServerConfigWriter::write();
        if ( $written ) {
            \SiteFort\Includes\ServerConfigWriter::clear_sync_failure();
            $next_state = \SiteFort\Includes\ServerConfigWriter::get_state();
            set_transient( $hash_key, (string) ( $next_state['sync_hash'] ?? \SiteFort\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 = \SiteFort\Includes\ServerConfigWriter::get_last_write_error_reason();
        \SiteFort\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 );
    }

    /**
     * Remove the SiteFort-managed wp-config.php hardening block during plugin deactivation.
     */
    public static function remove_wp_config_rules_on_deactivation(): bool {
        $instance = new self( 'sitefort', defined( 'SITEFORT_VERSION' ) ? (string) SITEFORT_VERSION : '' );
        return $instance->remove_wp_config_rules( 'deactivation' );
    }

    /**
     * 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;
        }

        $write_available = ! empty( $status['write_enabled'] )
            && ( ! empty( $status['writable'] ) || FilesystemMutation::has_configured_write_transport() );

        if ( ! $write_available ) {
            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();
        }
    }

    /**
     * Remove the managed SiteFort wp-config.php block without changing plugin secrets.
     */
    private function remove_wp_config_rules( string $runner ): bool {
        $status = ConfigFilePolicy::get_wp_config_status();
        $path   = is_string( $status['path'] ?? null ) ? (string) $status['path'] : '';
        if ( '' === $path || '' === $this->get_current_wp_config_managed_block( $path ) ) {
            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
            $this->clear_wp_config_sync_failure();
            return true;
        }

        // Deactivation cleanup must remove our persisted block even if the
        // normal "Write to Files" setting was disabled after the block was written.
        $write_available = ! empty( $status['file_exists'] )
            && ( ! empty( $status['dir_writable'] ) || FilesystemMutation::has_configured_write_transport() );
        if ( ! $write_available || ! $this->acquire_wp_config_sync_lock() ) {
            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
            $this->record_wp_config_sync_failure( $path, '', 'remove', ! $write_available ? 'writes_unavailable' : 'lock_unavailable' );
            $this->log_wp_config_rule_cleanup_failure( $runner, $path, ! $write_available ? 'writes_unavailable' : 'lock_unavailable' );
            return false;
        }

        try {
            $result = $this->write_wp_config_rules( $path, '' );
            if ( true === $result ) {
                delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
                $this->clear_wp_config_sync_failure();
                return true;
            }

            $reason = is_string( $result ) ? $result : 'unknown';
            delete_transient( self::WP_CONFIG_RULES_HASH_TRANSIENT );
            $this->record_wp_config_sync_failure( $path, '', 'remove', $reason );
            $this->log_wp_config_rule_cleanup_failure( $runner, $path, $reason );
            return false;
        } 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 SiteFort rule. SiteFort will reconcile it on the next sync.';
                    }
                } elseif ( $write_enabled ) {
                    $mode    = 'repair_required';
                    $message = 'wp-config.php contains a stale managed SiteFort rule and is not writable. SiteFort 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 SiteFort rule. SiteFort 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 = 'SiteFort 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 SiteFort is enforcing this rule at runtime.';
            } elseif ( $write_enabled ) {
                $mode    = 'runtime';
                $message = 'wp-config.php is not writable, so SiteFort is enforcing this rule at runtime.';
            } else {
                $mode    = 'runtime';
                $message = 'Write to Files is disabled, so SiteFort 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 = 'SiteFort 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 SiteFort 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 SiteFort 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 );
    }

    private function log_wp_config_rule_cleanup_failure( string $runner, string $path, string $reason ): void {
        if ( function_exists( '\sitefort_log' ) ) {
            \sitefort_log( sprintf( '[SiteFort Hardening] Could not remove managed wp-config.php rules during %s for %s: %s', $runner, $path, $reason ) );
        }
    }

    /**
     * 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( 'SiteFort could not %s the managed wp-config.php rule because creating a backup failed.', $action );
            case 'temp_file_failed':
                return sprintf( 'SiteFort could not %s the managed wp-config.php rule because the temporary file could not be created.', $action );
            case 'write_failed':
                return sprintf( 'SiteFort could not %s the managed wp-config.php rule because writing the updated file failed.', $action );
            case 'rename_failed':
                return sprintf( 'SiteFort could not %s the managed wp-config.php rule because replacing the live file failed.', $action );
            case 'rename_failed_restore_failed':
                return sprintf( 'SiteFort 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( 'SiteFort 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( 'SiteFort 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( 'SiteFort could not %s the managed wp-config.php rule because the updated configuration could not be composed safely.', $action );
            case 'read_failed':
                return sprintf( 'SiteFort could not %s the managed wp-config.php rule because the current wp-config.php file could not be read.', $action );
            default:
                return sprintf( 'SiteFort could not %s the managed wp-config.php rule. Review wp-config.php access and try again.', $action );
        }
    }

    /**
     * Write the managed SiteFort 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 = FilesystemMutation::read_file_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 ( ! FilesystemMutation::copy_path( $wp_config_path, $backup ) ) {
            return 'backup_failed';
        }

        $original_perms = fileperms( $wp_config_path );
        $original_mode  = false !== $original_perms ? ( $original_perms & 0777 ) : null;

        $write_result = FilesystemMutation::rewrite_existing_file( $wp_config_path, $updated_content, $original_mode, true );
        if ( is_wp_error( $write_result ) ) {
            $reason = $write_result->get_error_code();
            if ( ! $this->restore_wp_config_backup( $backup, $wp_config_path, $original_mode ) ) {
                return in_array( $reason, [ 'write_failed', 'rename_failed' ], true )
                    ? $reason . '_restore_failed'
                    : 'write_failed_restore_failed';
            }

            FilesystemMutation::delete_path( $backup );
            return in_array( $reason, [ 'write_failed', 'rename_failed' ], true ) ? $reason : 'write_failed';
        }

        if ( null !== $original_mode ) {
            FilesystemMutation::apply_file_permissions( $wp_config_path, $original_mode );
        }

        $verification = FilesystemMutation::read_file_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';
            }
            FilesystemMutation::delete_path( $backup );
            return 'verify_failed';
        }

        FilesystemMutation::delete_path( $backup );
        return true;
    }

    /**
     * Replace the managed SiteFort 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 SiteFort 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 SiteFort-managed block.
     */
    private function get_wp_config_managed_block_pattern(): string {
        $marker_pairs = [ [ self::WP_CONFIG_BEGIN_MARKER, self::WP_CONFIG_END_MARKER ] ];
        $patterns = [];

        foreach ( $marker_pairs as $pair ) {
            $patterns[] = preg_quote( (string) $pair[0], '/' ) . '.*?' . preg_quote( (string) $pair[1], '/' );
        }

        return '/(?:' . implode( '|', $patterns ) . ')\s*/s';
    }

    /**
     * Find a stable insertion point for SiteFort 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 . '.sitefort-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 ( ! FilesystemMutation::copy_path( $backup, $destination, $permissions ) ) {
            return false;
        }

        return true;
    }

    /**
     * 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   = 'sitefort_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( 'sitefort_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( \SiteFort\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 );
            $loader->add_action( 'admin_notices', null, [ ServerRulePluginConflictDetector::class, 'render_admin_notice' ], 10, 0 );
            $loader->add_action( 'network_admin_notices', null, [ ServerRulePluginConflictDetector::class, 'render_admin_notice' ], 10, 0 );
        }

        if ( ! empty( $this->settings['force_unique_nicename'] ) && \SiteFort\Includes\RequestContext::is_privileged() ) {
            $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( 'sitefort_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 );

        $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 );
        }

        // Unlock links from lockout emails; must work logged-out, the token is
        // the authorization. Registered unconditionally so links stay usable
        // after the lockout feature is toggled off.
        $loader->add_action( 'admin_post_sitefort_unlock', null, [ LockoutUnlockLink::class, 'handle_request' ], 10, 0 );
        $loader->add_action( 'admin_post_nopriv_sitefort_unlock', null, [ LockoutUnlockLink::class, 'handle_request' ], 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 );
        }

        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 );

        $needs_interstitial = (
            ! empty( $this->settings['password_expiration_days'] ) ||
            ! 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_sitefort_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, 'sitefort_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, 'sitefort_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 );
    }

    /**
     * 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, 'sitefort_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
     * SiteFort'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;
        }

        // Only flag weak passwords for users whose role is covered by the policy scope.
        if ( ! WordPressTweaks::user_in_password_policy_scope( $this->settings, $user ) ) {
            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(
                'sitefort_password_change_required',
                __( 'This account must use a stronger password before API authentication can continue. Sign in through the website and update the password.', 'sitefort' )
            );
        }

        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(
            'sitefort_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.', 'sitefort' );
            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.', 'sitefort' );
            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.', 'sitefort' );
            case 'weak':
                return __( 'This account must use a stronger password before API authentication can continue. Sign in through the website and update the password.', 'sitefort' );
            default:
                return __( 'This account must update its password before API authentication can continue. Sign in through the website and update the password.', 'sitefort' );
        }
    }

    /**
     * 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.
     *
     * @param bool $include_alternates Include both Apache and Nginx rule bodies for legacy/full consumers.
     * @return array<string,mixed>
     */
    public function get_manual_config_rules( bool $include_alternates = true ): array {
        $settings = $this->settings;
        $wp_raw   = trim( $this->get_wp_config_manual_rules_raw( $settings ) );

        $apache_rules = null;
        $nginx_rules  = null;
        $get_apache_rules = static function () use ( &$apache_rules ): string {
            if ( null === $apache_rules ) {
                $apache_rules = \SiteFort\Includes\ServerConfigWriter::get_managed_rules_for_server( 'apache' );
            }

            return $apache_rules;
        };
        $get_nginx_rules = static function () use ( &$nginx_rules ): string {
            if ( null === $nginx_rules ) {
                $nginx_rules = \SiteFort\Includes\ServerConfigWriter::get_managed_rules_for_server( 'nginx' );
            }

            return $nginx_rules;
        };
        $wp_rules = $this->wrap_manual_rules( $wp_raw, 'wp-config' );

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

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

        $payload = [
            'detected_server'      => $detected_server,
            'server_target_path'   => $server_path,
            'wp_config_path'       => $this->resolve_wp_config_path_for_rules(),
            'server_rules'         => $server_rules,
            'wp_config_rules'      => $wp_rules,
            'has_server_rules'     => '' !== $server_rules,
            'has_wp_config_rules'  => '' !== $wp_rules,
        ];

        if ( $include_alternates ) {
            $payload['server_rules_apache'] = $get_apache_rules();
            $payload['server_rules_nginx']  = $get_nginx_rules();
        }

        return $payload;
    }

    /**
     * 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 (SiteFort > 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 SiteFort 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 SiteFort - Do not modify or remove this line',
                $delimiter . ' SiteFort Config Details: 1',
                $rules,
                $delimiter . ' END SiteFort - 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 = 'sitefort_lockout_' . md5( $ip );
            set_transient( $lockout_key, $locked_at, $lockout_duration );
            LockoutRegistry::add_ip_lockout( $ip, $locked_at );
            do_action( 'sitefort_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 = 'sitefort_lockout_' . md5( $ip );
                    set_transient( $lockout_key, $locked_at, $lockout_duration );
                    LockoutRegistry::add_ip_lockout( $ip, $locked_at );
                    do_action( 'sitefort_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 = 'sitefort_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( 'sitefort_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();
        LockoutRegistry::prune_stale_entries();
    }

    /**
     * Render the dedicated SiteFort 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.', 'sitefort' ),
                human_time_diff( time(), time() + $time_remaining )
            );
            return new \WP_Error( 'sitefort_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.', 'sitefort' ),
            human_time_diff( time(), time() + $time_remaining )
        );
        return new \WP_Error( 'sitefort_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        = 'sitefort_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( 'sitefort_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   = 'sitefort_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( 'sitefort_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 {
        // sitefort_2fa_token: the 2FA challenge form (LoginInterstitial) fires
        // wp_login_failed synthetically on a wrong code so those attempts count
        // toward the same lockout thresholds as a wrong password, but that POST
        // never carries the classic username/password field names below.
        $keys = [ 'log', 'username', 'user_login', 'pwd', 'password', 'sitefort_2fa_token' ];

        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( 'sitefort_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( 'sitefort_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 '';
        }

        $identifier = sanitize_text_field( trim( (string) wp_unslash( (string) $value ) ) );
        return self::limit_login_identifier_length( $identifier );
    }

    private static function limit_login_identifier_length( string $value ): string {
        if ( function_exists( 'mb_substr' ) ) {
            $limited = mb_substr( $value, 0, self::LOGIN_IDENTIFIER_MAX_LENGTH, 'UTF-8' );
            if ( is_string( $limited ) ) {
                return $limited;
            }
        }

        return substr( $value, 0, self::LOGIN_IDENTIFIER_MAX_LENGTH );
    }

    /**
     * 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 SiteFort 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.', 'sitefort' ),
            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';
        sitefort_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;
        }

        // Defense in depth: do not query HIBP unless the submitted password is
        // the user's current primary password. This prevents failed attempts or
        // stale passwords from driving breach checks if another plugin invokes
        // the filter outside the normal WordPress authentication path.
        if ( ! wp_check_password( (string) $password, $user->user_pass, $user->ID ) ) {
            return $user;
        }

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

        if ( $active_reason === 'breached' ) {
            $result = $this->breached_password_checker->check_password( $password, true );
            if ( ! empty( $result['checked'] ) ) {
                $this->cache_breached_password_evaluation( $user, $result );
                if ( empty( $result['breached'] ) ) {
                    delete_user_meta( $user->ID, self::PASSWORD_CHANGE_REQUIRED_META );
                    AuditLog::add_log(
                        'Breached Password Flag Cleared',
                        [ 'username' => $user->user_login ],
                        'info',
                        'user'
                    );
                }
            }

            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,
                    'notice'      => 2,
                    'unavailable' => 3,
                    'ok'          => 4,
                    'hardened'    => 5,
                ];

                $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,
            'notice'      => 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.',
                'SiteFort data directory permissions are checked for runtime access, not strictness alone.',
                '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 = \SiteFort\Includes\ServerConfigWriter::detect_server();
        $server_path = \SiteFort\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',
            ];
        }

        $sitefort_data_dir = WP_CONTENT_DIR . '/sitefort-data';
        $targets[]         = [
            'id'          => 'sitefort-data-directory',
            'label'       => 'sitefort-data',
            'path'        => $sitefort_data_dir,
            'type'        => 'dir',
            'category'    => 'content',
            'sensitivity' => 'runtime_root',
        ];

        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' => 'secret',
            ];
        }

        if ( in_array( $server_type, [ 'apache', 'litespeed' ], true ) ) {
            $targets[] = [
                'id'          => 'server-config',
                'label'       => '.htaccess',
                'path'        => $server_path !== '' ? $server_path : \SiteFort\Includes\ServerConfigWriter::get_config_file_path( 'apache' ),
                'type'        => 'file',
                'category'    => 'config',
                'sensitivity' => 'sensitive',
            ];
        } elseif ( $server_type === 'nginx' ) {
            $nginx_path = $server_path !== '' ? $server_path : \SiteFort\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'];
        $sensitivity = (string) ( $target['sensitivity'] ?? 'standard' );
        $baseline    = $type === 'dir' ? $directory_baseline : $file_baseline;
        if ( 'secret' === $sensitivity && $type === 'file' ) {
            $baseline = 0640;
        }
        $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,
                    $sensitivity,
                    $path
                );
            } 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 ( 'notice' === $status ) {
            return [ 'notice', 'Notice' ];
        }

        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,
        string $path
    ): 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     = in_array( $sensitivity, [ 'sensitive', 'secret' ], true );
        $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 ( 'secret' === $sensitivity && $type === 'file' && ( $current_mode & 0004 ) !== 0 ) {
            if ( defined( 'SITEFORT_MASTER_KEY' ) ) {
                return [
                    'warning',
                    'secret_world_readable_master_key',
                    'This file defines SITEFORT_MASTER_KEY and is readable by every local account. Tighten it to 640 or 600 if the site still operates normally.',
                ];
            }

            return [
                'notice',
                'secret_world_readable',
                'Readable by all local accounts. Fine on hosts with account isolation, but 640 or 600 is recommended when the site still operates normally.',
            ];
        }

        if ( $type === 'dir' && $sensitivity === 'runtime_root' && ! DataDirectoryProtection::runtime_mode_is_usable( $current_mode, $path ) ) {
            return [
                'warning',
                'runtime_root_not_traversable',
                'SiteFort runtime storage must be writable and traversable by PHP. Review ownership, group alignment, or use a compatible mode such as 755.',
            ];
        }

        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;
    }
}
