<?php

declare(strict_types=1);

namespace WPUltimateSecurity\UltimateSecurity\Modules\Authentication;

use WPUltimateSecurity\UltimateSecurity\Modules\Base\AbstractModule;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\Utils;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\StringHelper;
use WPUltimateSecurity\UltimateSecurity\Modules\TestMode\TestModeService;
use WP_Error;

class LoginLimit extends AbstractModule
{
    public const MODULE_ID = 'auth.login_limit';

    protected const VERSION = '2.0.0';

    /**
     * Get unique module identifier.
     */
    public static function getId(): string
    {
        return self::MODULE_ID;
    }

    /**
     * Get human-readable module name.
     */
    public static function getName(): string
    {
        return __('Login Limit', 'ultimate-security');
    }

    /**
     * Get module domain.
     */
    public static function getDomain(): string
    {
        return 'authentication';
    }
	/* -------------------------------------------------------------------------
	 * CONFIG / META KEYS
	 * --------------------------------------------------------------------- */
	protected static $login_limit_options;
	protected static $reset_lockout_cron_hook     = 'ultimate_security_reset_expired_lockouts';
	protected static $reset_lockout_cron_schedule = 'minute';

	protected static $user_attempts_meta_key      = 'ultimate_security_failed_login_attempts';
	protected static $user_lockout_meta_key       = 'ultimate_security_lockout_expires';
	protected static $user_lockout_count_meta_key = 'ultimate_security_lockout_count';
	protected static $user_lockout_reset_meta_key = 'ultimate_security_lockout_reset_time';
	protected static $user_locked_time_meta_key   = 'ultimate_security_locked_time';
	protected static $user_lockout_phase_meta_key = 'ultimate_security_lockout_phase';     // 1 | 2
	protected static $user_lockout_attempts_meta  = 'ultimate_security_lockout_attempts';  // attempts *during* lockout

	protected static $bypass_cookie_name    = 'ultimate_security_bypass';
	protected static $bypass_transient_pref = 'ultimate_security_bypass_';

    /**
     * Boot the module after registration.
     */
	public static function boot(): void
    {
		$options                    = Utils::ultimate_security_get_option();
		self::$login_limit_options  = $options['security']['login_limit'] ?? [
			'enable'                   => 0,
			'login_attempts'           => 4,
			'lockout_duration'         => 10,
			'increase_login_attempts'  => 4,
			'increase_lockout_duration'=> 12,
			'retries_reset_duration'   => 12,
			'block_users'              => '',
			'recovery_url'             => '',
		];
		add_filter( 'cron_schedules', [ self::class, 'add_cron_schedule' ] );
		self::ensure_reset_lockout_cron_state();


		if ( ! empty( self::$login_limit_options['enable'] ) ) {
			self::init();
		}
	}

	/* -------------------------------------------------------------------------
	 * INITIALISATION & CRON
	 * --------------------------------------------------------------------- */
	public static function add_cron_schedule( $schedules ) {
		$schedules['minute'] = [
			'interval' => 60,
			'display'  => __( 'Every Minute', 'ultimate-security' ),
		];
		return $schedules;
	}

	protected static function ensure_reset_lockout_cron_state() {
		$hook = self::$reset_lockout_cron_hook;

		if ( empty( self::$login_limit_options['enable'] ) ) {
			self::unschedule_reset_lockout_cron();
			return;
		}

		if ( ! wp_next_scheduled( $hook ) ) {
			wp_schedule_event( time(), self::$reset_lockout_cron_schedule, $hook );
		}
	}

	protected static function unschedule_reset_lockout_cron() {
		$hook     = self::$reset_lockout_cron_hook;
		$attempts = 0;

		while ( $event = wp_get_scheduled_event( $hook ) ) {
			$result = wp_unschedule_event( $event->timestamp, $hook, $event->args );

			if ( ! $result ) {
				$attempts ++;

				if ( $attempts >= 3 ) {
					break;
				}

				wp_cache_delete( 'cron', 'options' );
				continue;
			}

			$attempts = 0;
		}
	}

	public static function init() {

        // login_form

		add_filter( 'authenticate',  [ self::class, 'check_login_limits' ], 100, 3 );
		add_action( 'wp_login_failed', [ self::class, 'track_failed_login' ] );
		add_action( 'wp_login',        [ self::class, 'clear_login_attempts' ], 10, 2 );

		/* --- recovery URL/bypass --- */
		if ( ! empty( self::$login_limit_options['recovery_url'] ) ) {
			// Call directly since we're already inside init action
			self::check_recovery_url();
			add_action( 'login_init', [ self::class, 'check_bypass_cookie' ], 5 );
		}

		/* --- UX helpers --- */
		add_filter( 'login_errors',  [ self::class, 'customize_login_errors' ] );
		add_action( 'login_form',    [ self::class, 'inject_lockout_refresh_script' ] );
		add_action( 'login_init',    [ self::class, 'check_expired_lockouts' ], 5 );
        
		/* --- cron for expiry / reset --- */
		add_action( self::$reset_lockout_cron_hook, [ self::class, 'reset_expired_lockouts' ] );
        
		/* --- AJAX checker --- */
		add_action( 'wp_ajax_nopriv_check_lockout_status', [ self::class, 'ajax_check_lockout_status' ] );
		// add_action( 'wp_ajax_check_lockout_status',        [ self::class, 'ajax_check_lockout_status' ] );

	}

    /**
     * Add JavaScript to refresh the lockout message without reload
     */
    public static function inject_lockout_refresh_script()
    {
        // $nonce    = wp_create_nonce('ultimate_security_lockout_check');
        // if (!check_ajax_referer('ultimate_security_lockout_check', 'security', false)) {
        //     wp_send_json_error(array('message' => __('Invalid security token.', 'ultimate-security')), 403);
        // }
        if (isset( $_POST['ultimate_security_login_form_field'] ) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ultimate_security_login_form_field'])), 'ultimate_security_login_form_action' ) ) {
            if (isset($_POST['log'])) {
                $username = sanitize_user(wp_unslash($_POST['log']));
                $nonce    = wp_create_nonce('ultimate_security_lockout_check');
                ?>
                <script type="text/javascript" id="inject_lockout">
                    (function () {
                        var nonce = '<?php echo esc_js($nonce); ?>';

                        function getUsername() {
                            return document.getElementById('user_login') ?
                                document.getElementById('user_login').value : '<?php echo esc_js($username); ?>';
                        }

                        function updateLockoutMessage() {
                            var username = getUsername();
                            if (!username) {
                                return;
                            }

                            var xhr = new XMLHttpRequest();
                            xhr.open('POST', '<?php echo esc_url(admin_url('admin-ajax.php')); ?>');
                            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                            xhr.onload = function () {
                                if (xhr.status === 200) {
                                    var response = JSON.parse(xhr.responseText);
                                    var errorDiv = document.getElementById('login_error');

                                    if (response.success) {
                                        if (response.data.locked) {
                                            if (errorDiv) {
                                                errorDiv.innerHTML = response.data.message;
                                            } else {
                                                var form = document.getElementById('loginform');
                                                if (form) {
                                                    errorDiv = document.createElement('div');
                                                    errorDiv.id = 'login_error';
                                                    errorDiv.innerHTML = response.data.message;
                                                    form.parentNode.insertBefore(errorDiv, form);
                                                }
                                            }
                                        } else if (errorDiv) {
                                            errorDiv.parentNode.removeChild(errorDiv);
                                        }
                                    }
                                }
                            };

                            var params = 'action=check_lockout_status&security=' + encodeURIComponent(nonce) + '&username=' + encodeURIComponent(username);
                            xhr.send(params);
                        }

                        updateLockoutMessage();
                        setInterval(updateLockoutMessage, 10000);
                    })();
                </script>
                <?php
            }
        }
    }

    /**
     * AJAX handler for checking lockout status
     */
    public static function ajax_check_lockout_status()
    {
        if (!check_ajax_referer('ultimate_security_lockout_check', 'security', false)) {
            wp_send_json_error(array('message' => __('Invalid security token.', 'ultimate-security')), 403);
        }

        if (!isset($_POST['username'])) {
            wp_send_json_error(array('message' => __('Invalid request', 'ultimate-security')));
        }

        $username = sanitize_user(wp_unslash($_POST['username']));
        $user = get_user_by('login', $username);

        if (!$user) {
            wp_send_json_error(array('message' => __('User not found', 'ultimate-security')));
        }

        if (self::is_user_blocked($username)) {
            wp_send_json_success(array(
                'locked' => true,
                'message' => __('This user has been blocked by the site administrator.', 'ultimate-security')
            ));
            return;
        }

        self::maybe_clear_expired_lockout($user->ID);

        if (self::is_user_locked_out($user->ID)) {

            if (TestModeService::shouldSimulate($user->ID)) {
                $lockout_expiry = self::get_user_lockout_expiry($user->ID);
                $remaining = max(1, ceil(($lockout_expiry - time()) / 60));
                TestModeService::logSimulation(
                    'login_limit',
                    'user_locked_out',
                    sprintf('User would be locked out for %d minutes', $remaining),
                    [
                        'username' => $username,
                        'lockout_expires' => $lockout_expiry,
                        'remaining_minutes' => $remaining,
                    ]
                );
            } else {

                $lockout_expiry = self::get_user_lockout_expiry($user->ID);
                $remaining = max(1, ceil(($lockout_expiry - time()) / 60));
                // translators: %d is the number of minutes until the account is unlocked.
                $message = sprintf(__('Too many failed login attempts. Your account is locked for %d minute.', 'Too many failed login attempts. Your account is locked for %d minutes.', $remaining, 'ultimate-security'), $remaining);


                wp_send_json_success(array(
                    'locked' => true,
                    'message' => $message,
                    'remaining' => $remaining
                ));
            }
        } else {
            wp_send_json_success(array(
                'locked' => false,
                'message' => ''
            ));
        }
    }

    /**
     * Check for expired lockouts when login page loads
     */
    public static function check_expired_lockouts()
    {

        if (isset( $_POST['ultimate_security_login_form_field'] ) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ultimate_security_login_form_field'])), 'ultimate_security_login_form_action' ) ) {
            if (isset($_POST['log'])) {
                $username = sanitize_user(wp_unslash($_POST['log']));
                if (!empty($username)) {
                    $user = get_user_by('login', $username);
                    if ($user) {
                        self::maybe_clear_expired_lockout($user->ID);
                    }
                }
            }
        }
    }

    /**
     * Check bypass cookie and take appropriate action
     */
    public static function check_bypass_cookie()
    {
        if (isset($_COOKIE[self::$bypass_cookie_name])) {
            $cookie_value = sanitize_user(wp_unslash($_COOKIE[self::$bypass_cookie_name]));

            if (get_transient(self::$bypass_transient_pref . $cookie_value)) {
                // Important: Add this filter at a high priority (99) 
                // to ensure it runs before check_login_limits (100)
                add_filter('authenticate', array(self::class, 'bypass_login_limits'), 99);

                add_filter('login_message', function ($message) {
                    return $message . '<p class="message recovery-key-used">' .
                        __('Recovery key accepted. Login limits bypassed.', 'ultimate-security') .
                        '</p>';
                });

                // Do not delete the transient immediately to ensure it persists during the login process
                // We'll delete it after successful login
            }
        }
    }

    /**
     * Modified check_login_limits to respect recovery bypass
     */
    public static function check_login_limits($user, $username, $password)
    {
        if (is_wp_error($user)) {
            return $user;
        }

        if (wp_doing_ajax() || defined('REST_REQUEST') && REST_REQUEST) {
            return $user;
        }

        // IMPORTANT: Check if bypass cookie is set - if so, allow login without limits
        if (isset($_COOKIE[self::$bypass_cookie_name])) {
            $cookie_value = sanitize_user(wp_unslash($_COOKIE[self::$bypass_cookie_name]));
            if (get_transient(self::$bypass_transient_pref . $cookie_value)) {
                // If we're here with a valid bypass cookie, we should never block the user
                if (is_a($user, 'WP_User')) {
                    // Clear any lockout data to ensure login works
                    delete_user_meta($user->ID, self::$user_lockout_meta_key);
                }
                return $user;
            }
        }
        if (!empty($username)) {
            if (self::is_user_blocked($username)) {
                // Test Mode: Log simulation instead of blocking
                $found_user_for_test = get_user_by('login', $username);
                if ($found_user_for_test && TestModeService::shouldSimulate($found_user_for_test->ID)) {
                    TestModeService::logSimulation(
                        'login_limit',
                        'user_blocked',
                        __('User is on blocked list', 'ultimate-security'),                         
                        [
                            'username' => $username,
                        ]
                    );
                } else {
                    return new WP_Error(
                        'user_blocked',
                        __('This user has been blocked by the site administrator.', 'ultimate-security')
                    );
                }
            }

            $found_user = get_user_by('login', $username);

            if ($found_user) {
                self::maybe_clear_expired_lockout($found_user->ID);

                if (self::is_user_locked_out($found_user->ID)) {
                    // Test Mode: Log simulation instead of blocking
                    if (TestModeService::shouldSimulate($found_user->ID)) {
                        $lockout_expiry = self::get_user_lockout_expiry($found_user->ID);
                        $remaining = max(1, ceil(($lockout_expiry - time()) / 60));
                        TestModeService::logSimulation(
                            'login_limit',
                            'user_locked_out',
                            sprintf('User would be locked out for %d minutes', $remaining),
                            [
                                'username' => $username,
                                'lockout_expires' => $lockout_expiry,
                                'remaining_minutes' => $remaining,
                            ]
                        );
                    } else {
                        $lockout_expiry = self::get_user_lockout_expiry($found_user->ID);
                        $remaining = max(1, ceil(($lockout_expiry - time()) / 60));
                        // translators: %d is the number of minutes until the account is unlocked.
                        return new WP_Error('user_locked_out', sprintf(__('Too many failed login attempts. Your account is locked for %d minute.', 'Too many failed login attempts. Your account is locked for %d minutes.', $remaining, 'ultimate-security'), $remaining));
                    }
                }
            }
        }

        return $user;
    }

    public static function track_failed_login( $username ) {

		$user = get_user_by( 'login', $username );
		if ( ! $user ) {
			return;
		}

		// If currently locked: simply increment within‑lockout counter & maybe escalate.
		if ( self::is_user_locked_out( $user->ID ) ) {

			$lockout_attempts = (int) get_user_meta( $user->ID, self::$user_lockout_attempts_meta, true );
			$lockout_attempts ++;
			update_user_meta( $user->ID, self::$user_lockout_attempts_meta, $lockout_attempts );

			self::maybe_increase_lockout_phase( $user->ID, $lockout_attempts );
			return;
		}

		/* --- not currently locked: normal attempt counting --- */

		self::maybe_clear_expired_lockout( $user->ID );
		self::maybe_reset_attempts( $user->ID );

		$attempts = self::increment_user_attempts( $user->ID );

		$limit = max( 1, (int) ( self::$login_limit_options['login_attempts'] ?: 2 ) );
		if ( $attempts >= $limit ) {
			self::lock_out_user( $user->ID, 1 ); // always phase 1 the first time
		}
	}


    /**
     * Get attempts made during lockout period
     */
    protected static function get_lockout_attempts($user_id)
    {
        $attempts = get_user_meta($user_id, 'ultimate_security_lockout_attempts', true);
        return absint($attempts);
    }

    /**
     * Update attempts made during lockout period
     */
    protected static function update_lockout_attempts($user_id, $attempts)
    {
        update_user_meta($user_id, 'ultimate_security_lockout_attempts', $attempts);
    }

    /**
     * Check if we need to upgrade to phase 2 lockout
     */
    protected static function maybe_increase_lockout_phase( $user_id, $lockout_attempts ) {

		$current_phase = (int) get_user_meta( $user_id, self::$user_lockout_phase_meta_key, true );
		if ( $current_phase >= 2 ) {
			return; // already escalated
		}

		$threshold = max( 1, (int) ( self::$login_limit_options['increase_login_attempts'] ?: 5 ) );
		if ( $lockout_attempts < $threshold ) {
			return;
		}

		// escalate!
		self::lock_out_user( $user_id, 2 );

		do_action(
			'ultimate_security_log_event',
			'user_phase_upgraded',
			[
				'user_id'       => $user_id,
				'new_phase'     => 2,
				'attempts_during_lockout' => $lockout_attempts,
				'triggered_at'  => time(),
			]
		);
	}


    /**
     * Customize login error messages to show lockout notifications
     */
    public static function customize_login_errors($errors)
    {

        if (isset( $_POST['ultimate_security_login_form_field'] ) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['ultimate_security_login_form_field'])), 'ultimate_security_login_form_action' ) ) {
            $username = '';
            if (isset($_POST['log'])) {
                $username = sanitize_user(wp_unslash($_POST['log']));
            }

            if (!empty($username)) {
                if (self::is_user_blocked($username)) {
                    return __('This user has been blocked by the site administrator.', 'ultimate-security');
                }

                $user = get_user_by('login', $username);
                if ($user) {
                    self::maybe_clear_expired_lockout($user->ID);

                    if (self::is_user_locked_out($user->ID)) {
                        $lockout_expiry = self::get_user_lockout_expiry($user->ID);
                        $remaining = max(1, ceil(($lockout_expiry - time()) / 60));
                        // translators: %d is the number of minutes until the account is unlocked.
                        return sprintf(__('Too many failed login attempts. Your account is locked for %d minute.', 'Too many failed login attempts. Your account is locked for %d minutes.', $remaining, 'ultimate-security'), $remaining);
                    }
                }
            }
        }

        return $errors;
    }

    protected static function maybe_reset_attempts( $user_id ) {
		$reset_time = (int) get_user_meta( $user_id, self::$user_lockout_reset_meta_key, true );
		if ( $reset_time && time() > $reset_time ) {
			self::reset_user_lockout_data( $user_id );
		}
	}


    /**
     * Reset all lockout data for a specific user
     *
     * @param int $user_id The user ID to reset lockout data for
     */
    public static function reset_user_lockout_data( $user_id ) {
		delete_user_meta( $user_id, self::$user_attempts_meta_key );
		self::reset_lockout_only( $user_id );
		delete_user_meta( $user_id, self::$user_lockout_count_meta_key );
		delete_user_meta( $user_id, self::$user_lockout_reset_meta_key );
	}

    /**
     * Lock out a user based on their lockout history
     */
	protected static function lock_out_user( $user_id, $phase = 1 ) {

		if ( $phase === 2 ) {
			$hours            = max( 1, (int) ( self::$login_limit_options['increase_lockout_duration'] ?: 10 ) );
			$duration_seconds = $hours * HOUR_IN_SECONDS;
			$duration_minutes = $hours * MINUTE_IN_SECONDS;
		} else {
			$minutes          = max( 1, (int) ( self::$login_limit_options['lockout_duration'] ?: 5 ) );
			$duration_seconds = $minutes * MINUTE_IN_SECONDS;
			$duration_minutes = $minutes;
		}

		$expires = time() + $duration_seconds;

		update_user_meta( $user_id, self::$user_lockout_meta_key,  $expires );
		update_user_meta( $user_id, self::$user_locked_time_meta_key, time() );
		update_user_meta( $user_id, self::$user_lockout_phase_meta_key, $phase );
		delete_user_meta( $user_id, self::$user_lockout_attempts_meta ); // reset within‑lockout counter

		// keep track of how many *distinct* lockout incidents they’ve had
		$count = (int) get_user_meta( $user_id, self::$user_lockout_count_meta_key, true );
		update_user_meta( $user_id, self::$user_lockout_count_meta_key, $count + 1 );

		// set time when everything resets back to clean
		$reset_hours = max( 1, (int) ( self::$login_limit_options['retries_reset_duration'] ?: 12 ) );
		update_user_meta( $user_id, self::$user_lockout_reset_meta_key, time() + ( $reset_hours * HOUR_IN_SECONDS ) );

		do_action(
			'ultimate_security_log_event',
			'user_lockout',
			[
				'user_id'       => $user_id,
				'phase'         => $phase,
				'duration_m'    => $duration_minutes,
				'locked_at'     => time(),
				'expires_at'    => $expires,
				'lockout_count' => $count + 1,
			]
		);
	}

    /**
     * Get base lockout duration (phase 1)
     */
    protected static function get_base_lockout_duration()
    {
        $base_duration = absint(self::$login_limit_options['lockout_duration']);
        if (empty($base_duration)) {
            $base_duration = 5;
        }
        return $base_duration;
    }

    public static function clear_login_attempts( $username, $user ) {
		self::update_user_attempts( $user->ID, 0 );
		delete_user_meta( $user->ID, self::$user_lockout_attempts_meta );
		delete_user_meta( $user->ID, self::$user_lockout_phase_meta_key );
	}

    /**
     * Check for recovery URL and unlock if valid
     */
    public static function check_recovery_url()
    {
        // Check if key parameter exists in URL
        if (empty($_GET['key']) || empty(self::$login_limit_options['recovery_url'])) {
            return;
        }

        // Get the configured recovery URL to extract just the key part
        $configured_url = self::$login_limit_options['recovery_url'];
        $key_position = strpos($configured_url, '?key=');

        if ($key_position !== false) {
            $expected_key = substr($configured_url, $key_position + 5);

            // Compare with the provided key
            $provided_key = sanitize_text_field(wp_unslash($_GET['key']));

            if ($provided_key === $expected_key) {
                $cookie_name = self::$bypass_cookie_name;
                $cookie_value = StringHelper::random(32);
                setcookie($cookie_name, $cookie_value, 0, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);

                // Store in transient for verification (short-lived)
                set_transient(self::$bypass_transient_pref . $cookie_value, 1, 300); // Valid for 5 minutes

                // Redirect to wp-login.php
                wp_safe_redirect(wp_login_url());
                exit;
            }
        }
    }

    /**
     * Bypass function for recovery key - completely bypasses all login limits
     */
    public static function bypass_login_limits($user)
    {
        // If we have a valid user and a valid cookie, ensure login is possible
        if (is_a($user, 'WP_User')) {
            // Unlock this specific user by clearing all lockout data
            delete_user_meta($user->ID, self::$user_attempts_meta_key);
            delete_user_meta($user->ID, self::$user_lockout_meta_key);
            delete_user_meta($user->ID, self::$user_lockout_count_meta_key);
            delete_user_meta($user->ID, self::$user_lockout_reset_meta_key);
            delete_user_meta($user->ID, self::$user_locked_time_meta_key);
            delete_user_meta($user->ID, self::$user_lockout_phase_meta_key);
            delete_user_meta($user->ID, 'ultimate_security_lockout_attempts');

            // Log this bypass event
            do_action('ultimate_security_log_event', 'recovery_url_success', array(
                'user_id' => $user->ID,
                'time' => time()
            ));
        }

        // Make sure the lockout check is completely bypassed
        remove_filter('authenticate', array(self::class, 'check_login_limits'), 100);

        // After successful use of bypass, clean up cookie on next successful login
        add_action('wp_login', function ($user_login, $user) {
            if (isset($_COOKIE[self::$bypass_cookie_name])) {
                $cookie_value = sanitize_user(wp_unslash($_COOKIE[self::$bypass_cookie_name]));
                delete_transient(self::$bypass_transient_pref . $cookie_value);
                setcookie(self::$bypass_cookie_name, '', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
            }
        }, 10, 2);

        return $user;
    }

    /**
     * Get failed login attempts for a user
     */
    protected static function get_user_attempts( $user_id ) {
		return (int) get_user_meta( $user_id, self::$user_attempts_meta_key, true );
	}

    /**
     * Update failed login attempts count for a user
     */
    protected static function update_user_attempts( $user_id, $attempts ) {
		update_user_meta( $user_id, self::$user_attempts_meta_key, $attempts );
	}

	/**
	 * Atomically increment failed login attempts count for a user.
	 *
	 * @param int $user_id The user ID.
	 * @return int Updated attempts count.
	 */
	protected static function increment_user_attempts( $user_id ) {
		global $wpdb;

		$meta_key = self::$user_attempts_meta_key;
		$table    = $wpdb->usermeta;

		$values = get_user_meta( $user_id, $meta_key, false );
		if ( $values && count( $values ) > 1 ) {
			$total = 0;
			foreach ( $values as $value ) {
				$total += (int) $value;
			}
			delete_metadata( 'user', $user_id, $meta_key );
			update_user_meta( $user_id, $meta_key, $total );
		}

		add_user_meta( $user_id, $meta_key, 0, true );

		$result = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$table}
				 SET meta_value = CAST(meta_value AS UNSIGNED) + 1
				 WHERE user_id = %d AND meta_key = %s",
				$user_id,
				$meta_key
			)
		);

		if ( false === $result || 0 === $result ) {
			$attempts = self::get_user_attempts( $user_id ) + 1;
			self::update_user_attempts( $user_id, $attempts );
			return $attempts;
		}

		wp_cache_delete( $user_id, 'user_meta' );

		return (int) get_user_meta( $user_id, $meta_key, true );
	}

    /**
     * Check if user is currently locked out
     */
    protected static function is_user_locked_out( $user_id ) {
		$expires = (int) get_user_meta( $user_id, self::$user_lockout_meta_key, true );
		return $expires && ( $expires > time() );
	}

    /**
     * Check if a lockout has expired and clear it if so
     *
     * @param int $user_id User ID to check
     * @return bool True if lockout was cleared, false otherwise
     */
    protected static function maybe_clear_expired_lockout( $user_id ):void {
		$expires = (int) get_user_meta( $user_id, self::$user_lockout_meta_key, true );
		if ( $expires && $expires <= time() ) {
			self::reset_lockout_only( $user_id );
		}
	}
    protected static function reset_lockout_only( $user_id ) {
		delete_user_meta( $user_id, self::$user_lockout_meta_key );
		delete_user_meta( $user_id, self::$user_locked_time_meta_key );
		delete_user_meta( $user_id, self::$user_lockout_phase_meta_key );
		delete_user_meta( $user_id, self::$user_lockout_attempts_meta );
		delete_user_meta( $user_id, self::$user_attempts_meta_key );
	}

    /**
     * Get the timestamp when user's lockout expires
     */
    protected static function get_user_lockout_expiry($user_id)
    {
        $expiry = get_user_meta($user_id, self::$user_lockout_meta_key, true);
        return absint($expiry);
    }

    /**
     * Check if a username is in the blocked users list
     * Supports the nested array structure as shown in your example
     */
    protected static function is_user_blocked($username)
    {
        if (empty(self::$login_limit_options['block_users'])) {
            return false;
        }

        $block_users = self::$login_limit_options['block_users'];

        // Handle array structure with 'code' field
        if (is_array($block_users)) {
            foreach ($block_users as $user_entry) {
                // Handle the nested array structure from your example
                if (isset($user_entry['code']) && $user_entry['code'] === $username) {
                    return true;
                }
            }
        }
        // For backward compatibility with string format
        else if (is_string($block_users)) {
            $blocked_users = array_map('trim', explode(',', $block_users));
            return in_array($username, $blocked_users, true);
        }

        return false;
    }

    /**
     * Reset expired lockouts and apply reset duration policy
     * Called via cron every minute
     */
    public static function reset_expired_lockouts()
    {
        global $wpdb;

        // Get current time
        $now = time();

        // 1. Find and clear expired lockouts
        $meta_key = self::$user_lockout_meta_key;
        $users_to_unlock = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT user_id FROM $wpdb->usermeta
                WHERE meta_key = %s AND meta_value < %d AND meta_value > 0",
                $meta_key,
                $now
            )
        );

        if (!empty($users_to_unlock)) {
            foreach ($users_to_unlock as $user_id) {
                self::maybe_clear_expired_lockout($user_id);
            }

            do_action('ultimate_security_log_event', 'lockout_auto_clear', array(
                'count' => count($users_to_unlock),
                'cleared_at' => $now
            ));
        }

        // 2. Check for users whose reset time has expired (retries_reset_duration)
        $reset_meta_key = self::$user_lockout_reset_meta_key;
        $users_to_reset = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT user_id FROM $wpdb->usermeta
                WHERE meta_key = %s AND meta_value < %d AND meta_value > 0",
                $reset_meta_key,
                $now
            )
        );

        if (!empty($users_to_reset)) {
            foreach ($users_to_reset as $user_id) {
                // This will completely reset attempts and lockout history
                self::reset_user_lockout_data($user_id);
            }

            do_action('ultimate_security_log_event', 'lockout_history_reset', array(
                'count' => count($users_to_reset),
                'reset_at' => $now
            ));
        }
    }
}
