<?php

declare(strict_types=1);

namespace ShieldX\Security;

defined('ABSPATH') || exit;

use ShieldX\Core\EmailTemplate;
use ShieldX\Core\NotificationManager;
use ShieldX\Support\CryptoBox;

final class LoginSecurity
{
    private const META_ENABLED = 'shieldx_2fa_enabled';
    private const META_SECRET = 'shieldx_2fa_secret';
    private const META_PENDING_SECRET = 'shieldx_2fa_pending_secret';
    private const META_RECOVERY_HASHES = 'shieldx_2fa_recovery_hashes';
    private const META_ENABLED_AT = 'shieldx_2fa_enabled_at';
    private const META_RECOVERY_USED_AT = 'shieldx_2fa_recovery_used_at';
    private const OWNER_RECOVERY_TOKEN_TTL = 3600;
    private const RESET_CODE_TTL = 900;
    private const RESET_CODE_MAX_ATTEMPTS = 5;
    private const RESET_CODE_REQUEST_THROTTLE = 300;
    private const TWO_FACTOR_FAILURE_LIMIT = 10;
    private const TWO_FACTOR_FAILURE_WINDOW = 600;
    private const TWO_FACTOR_LOCK_SECONDS = 600;
    private const MAGIC_LOGIN_TTL = 900;
    private const MAGIC_LOGIN_MAX_ATTEMPTS = 5;
    private const MAGIC_LOGIN_REQUEST_THROTTLE = 60;
    private const MAGIC_LOGIN_USER_DAILY_REQUEST_LIMIT = 20;
    private const MAGIC_LOGIN_USER_DAILY_FAILURE_LIMIT = 50;
    private const MAGIC_LOGIN_USER_DAILY_WINDOW = 86400;
    private const MAGIC_LOGIN_IP_WINDOW = 300;
    private const MAGIC_LOGIN_IP_LIMIT = 10;
    private const MAGIC_LOGIN_NONCE_ACTION = 'shieldx_magic_login';
    private const MAGIC_LOGIN_NONCE_FIELD = 'shieldx_magic_nonce';
    private const SINGLE_USE_CLAIM_PREFIX = 'shieldx_login_once_';
    private const RECOVERY_CODE_CLAIM_TTL = 7776000;
    private const TRANSIENT_LOCK_SECONDS = 5;
    private const SESSION_LAST_SEEN_KEY = 'shieldx_last_seen';
    private const SESSION_LAST_SEEN_WRITE_INTERVAL = 300;
    public const META_LAST_LOGIN_AT = 'shieldx_last_login_at';

    /** @var LoginSecuritySettings */
    private $settings;

    /** @var AuditLogger|null */
    private $logger;

    public function __construct(LoginSecuritySettings $settings, ?AuditLogger $logger = null)
    {
        $this->settings = $settings;
        $this->logger = $logger;
    }

    public static function fromWordPress(?AuditLogger $logger = null): self
    {
        return new self(LoginSecuritySettings::fromWordPress(), $logger);
    }

    public static function sendActivationRecoveryEmail(): void
    {
        if (!NotificationManager::notificationsAddonActive()) {
            return;
        }

        self::fromWordPress()->sendOwnerRecoveryEmail(true, 'activation');
    }

    /**
     * @return array{ready:bool,openssl:bool,salts:bool}
     */
    public static function secretStorageStatus(): array
    {
        $openssl = CryptoBox::isAvailable();
        $auth = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $secure = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        $salts = $auth !== '' && $secure !== '';

        return [
            'ready' => $openssl && $salts,
            'openssl' => $openssl,
            'salts' => $salts,
        ];
    }

    public function boot(): void
    {
        if (!$this->settings->enabled()) {
            return;
        }

        if (!self::secretStorageStatus()['ready'] && function_exists('add_action')) {
            add_action('admin_notices', [$this, 'renderSecretStorageUnavailableNotice']);
        }

        add_action('login_form', [$this, 'renderLoginField']);
        add_filter('authenticate', [$this, 'enforceTwoFactor'], 1000, 3);
        add_filter('wp_is_application_passwords_available_for_user', [$this, 'applicationPasswordsAvailableForUser'], 10, 2);
        add_filter('rest_authentication_errors', [$this, 'blockRestApplicationPasswordForTwoFactorUsers'], 20);
        add_action('admin_init', [$this, 'maybeRedirectAdminSetup'], 1);
        add_action('init', [$this, 'enforceSessionPolicy'], 1);
        add_action('wp_login', [$this, 'recordSuccessfulLogin'], 10, 2);
        add_action('show_user_profile', [$this, 'renderProfileTwoFactor']);
        add_action('personal_options_update', [$this, 'handleProfileTwoFactorSetup']);

        if ($this->settings->magicLoginEnabled()) {
            add_action('login_footer', [$this, 'renderMagicLoginEntry']);
            add_action('login_form_shieldx_magic_login', [$this, 'handleMagicLoginRequest']);
        }
    }

    public function renderSecretStorageUnavailableNotice(): void
    {
        if (!function_exists('current_user_can') || !current_user_can('manage_options')) {
            return;
        }

        $message = __('SHIELD-X 2FA cannot access encrypted authenticator secrets. Enrolled accounts remain protected and cannot sign in until OpenSSL and WordPress salts are restored.', 'xerotact-shield-x-security');

        echo '<div class="notice notice-error"><p>' . esc_html($message) . '</p></div>';
    }

    public function settings(): LoginSecuritySettings
    {
        return $this->settings;
    }

    /**
     * @param mixed $user
     */
    public function renderProfileTwoFactor($user): void
    {
        $userId = $this->userObjectId($user);
        $currentUserId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        if ($userId <= 0 || $userId !== $currentUserId || (!$this->requiresTwoFactorForUserObject($user) && !$this->isTwoFactorEnabled($userId))) {
            return;
        }

        $enabled = $this->isTwoFactorEnabled($userId);
        $secret = $enabled ? '' : $this->pendingSecret($userId);
        ?>
        <h2 id="shieldx-profile-two-factor"><?php echo esc_html__('SHIELD-X Two-Factor Authentication', 'xerotact-shield-x-security'); ?></h2>
        <?php if ($enabled) : ?>
            <p><?php echo esc_html__('Authenticator 2FA is active for this account.', 'xerotact-shield-x-security'); ?></p>
        <?php else : ?>
            <p><?php echo esc_html__('This account must enroll in authenticator 2FA. Add the manual key to your authenticator app, then enter a current code.', 'xerotact-shield-x-security'); ?></p>
            <p><code><?php echo esc_html($this->formattedSecret($secret)); ?></code></p>
            <label for="shieldx_profile_totp"><?php echo esc_html__('Authenticator code', 'xerotact-shield-x-security'); ?></label>
            <input id="shieldx_profile_totp" type="text" name="shieldx_totp" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6">
            <p class="description"><?php echo esc_html__('Enter the code, then use Update Profile to enable 2FA. Recovery codes will be sent to your account email.', 'xerotact-shield-x-security'); ?></p>
        <?php endif; ?>
        <?php
    }

    public function handleProfileTwoFactorSetup(int $userId): void
    {
        if (!function_exists('wp_verify_nonce') || !function_exists('get_current_user_id') || $userId !== (int) get_current_user_id()) {
            return;
        }
        // phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The WordPress profile nonce is verified before the unslashed authenticator code is reduced to digits.
        $nonce = isset($_POST['_wpnonce']) && is_scalar($_POST['_wpnonce']) ? (string) wp_unslash($_POST['_wpnonce']) : '';
        if ($nonce === '' || wp_verify_nonce($nonce, 'update-user_' . $userId) === false) {
            return;
        }
        $code = isset($_POST['shieldx_totp']) && is_scalar($_POST['shieldx_totp'])
            ? preg_replace('/\D+/', '', (string) wp_unslash($_POST['shieldx_totp']))
            : '';
        // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        if ($code === '') {
            return;
        }
        $result = $this->enableForUser($userId, (string) $code);
        if (!empty($result['ok']) && !empty($result['codes']) && is_array($result['codes'])) {
            $this->sendRecoveryCodesEmail($userId, array_map('strval', $result['codes']));
        }
    }

    /**
     * @return array<string,mixed>
     */
    public function summary(): array
    {
        $currentUserId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $adminCounts = $this->adminEnrollmentCounts();
        $requiredCounts = $this->requiredRoleEnrollmentCounts();
        $safeSettings = $this->settings->all();
        unset(
            $safeSettings['owner_recovery_token_hash'],
            $safeSettings['owner_recovery_token_ip_hash'],
            $safeSettings['emergency_recovery_token'],
            $safeSettings['emergency_recovery_token_hash']
        );

        return array_merge($safeSettings, [
            'secret_storage' => self::secretStorageStatus(),
            'current_user' => $currentUserId > 0 ? $this->userState($currentUserId) : null,
            'admins_enrolled' => (int) ($adminCounts['enrolled'] ?? 0),
            'admins_pending' => (int) ($adminCounts['pending'] ?? 0),
            'admins_total' => (int) ($adminCounts['total'] ?? 0),
            'required_users_enrolled' => (int) ($requiredCounts['enrolled'] ?? 0),
            'required_users_pending' => (int) ($requiredCounts['pending'] ?? 0),
            'required_users_total' => (int) ($requiredCounts['total'] ?? 0),
            'owner_recovery_available' => $this->settings->ownerRecoveryTokenAvailable(),
            'emergency_recovery_available' => $this->settings->emergencyRecoveryTokenHash() !== '',
            'emergency_recovery_url_redacted' => true,
        ]);
    }

    /**
     * @return array<string,mixed>
     */
    public function userState(int $userId): array
    {
        $enabled = $this->isTwoFactorEnabled($userId);

        return [
            'user_id' => $userId,
            'enabled' => $enabled,
            'setup_required' => $this->settings->enabled() && $this->requiresTwoFactorForUserId($userId) && !$enabled,
            'enabled_at' => (string) get_user_meta($userId, self::META_ENABLED_AT, true),
            'recovery_codes_remaining' => count($this->recoveryHashes($userId)),
            'pending_secret_exists' => (string) get_user_meta($userId, self::META_PENDING_SECRET, true) !== '',
            'account_label' => $this->accountLabel($userId),
        ];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function adminUsers(int $limit = 100): array
    {
        if (!function_exists('get_users')) {
            return [];
        }

        $users = get_users([
            'role' => 'administrator',
            'orderby' => 'user_login',
            'order' => 'ASC',
            'number' => max(1, min(500, $limit)),
        ]);
        $currentUserId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $rows = [];

        foreach ($users as $user) {
            $userId = $this->userObjectId($user);

            if ($userId <= 0) {
                continue;
            }

            $state = $this->userState($userId);
            $roles = is_object($user) && isset($user->roles) && is_array($user->roles) ? array_values(array_map('strval', $user->roles)) : ['administrator'];

            $rows[] = [
                'user_id' => $userId,
                'login' => $this->userObjectValue($user, 'user_login', 'user-' . (string) $userId),
                'display_name' => $this->userObjectValue($user, 'display_name', ''),
                'email' => $this->userObjectValue($user, 'user_email', ''),
                'roles' => $roles,
                'registered_at' => $this->userObjectValue($user, 'user_registered', ''),
                'last_login_at' => $this->lastLoginAt($userId),
                'enabled' => !empty($state['enabled']),
                'setup_required' => !empty($state['setup_required']),
                'pending_secret_exists' => !empty($state['pending_secret_exists']),
                'enabled_at' => (string) ($state['enabled_at'] ?? ''),
                'recovery_codes_remaining' => (int) ($state['recovery_codes_remaining'] ?? 0),
                'is_current' => $currentUserId > 0 && $currentUserId === $userId,
            ];
        }

        return $rows;
    }

    public function pendingSecret(int $userId): string
    {
        $stored = (string) get_user_meta($userId, self::META_PENDING_SECRET, true);
        if ($stored !== '') {
            $secret = $this->secretForUser($userId, self::META_PENDING_SECRET);
            if ($secret !== '') {
                return $secret;
            }
        }

        $secret = TwoFactor::generateSecret();
        update_user_meta($userId, self::META_PENDING_SECRET, $this->protectSecret($secret));

        return $secret;
    }

    public function otpauthUriForUser(int $userId, string $secret): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        return TwoFactor::otpauthUri($siteName . ' SHIELD-X', $this->accountLabel($userId), $secret);
    }

    public function formattedSecret(string $secret): string
    {
        return trim(implode(' ', str_split(TwoFactor::normalizeSecret($secret), 4)));
    }

    /**
     * @return array{ok:bool,error:string,codes:array<int,string>}
     */
    public function enableForUser(int $userId, string $code): array
    {
        if (!$this->currentRequestCanManageTwoFactorUser($userId)) {
            return [
                'ok' => false,
                'error' => __('You do not have permission to change SHIELD-X 2FA for this administrator.', 'xerotact-shield-x-security'),
                'codes' => [],
            ];
        }

        $secret = $this->pendingSecret($userId);

        if (!TwoFactor::verifyTotp($secret, $code)) {
            return [
                'ok' => false,
                'error' => __('The authenticator code did not match. Check the time on the device and try again.', 'xerotact-shield-x-security'),
                'codes' => [],
            ];
        }

        $codes = TwoFactor::generateRecoveryCodes(10);
        update_user_meta($userId, self::META_SECRET, $this->protectSecret($secret));
        update_user_meta($userId, self::META_ENABLED, '1');
        update_user_meta($userId, self::META_ENABLED_AT, $this->now());
        update_user_meta($userId, self::META_RECOVERY_HASHES, $this->hashRecoveryCodes($codes));
        delete_user_meta($userId, self::META_PENDING_SECRET);

        $this->log('login_security.2fa_enabled', ['user_id' => $userId]);

        return [
            'ok' => true,
            'error' => '',
            'codes' => $codes,
        ];
    }

    /**
     * @return array<int,string>
     */
    public function regenerateRecoveryCodes(int $userId): array
    {
        if (!$this->currentRequestCanManageTwoFactorUser($userId)) {
            return [];
        }

        $codes = TwoFactor::generateRecoveryCodes(10);
        update_user_meta($userId, self::META_RECOVERY_HASHES, $this->hashRecoveryCodes($codes));
        $this->log('login_security.recovery_codes_regenerated', ['user_id' => $userId]);

        return $codes;
    }

    /**
     * @param array<int,string> $codes
     * @return array{sent:bool,email:string,error:string}
     */
    public function sendRecoveryCodesEmail(int $userId, array $codes): array
    {
        $codes = array_values(array_filter(array_map('strval', $codes), static function (string $code): bool {
            return trim($code) !== '';
        }));
        $email = '';
        $error = '';
        $sent = false;

        $user = function_exists('get_userdata') ? get_userdata($userId) : null;
        if (!$this->isSubjectUserObject($user)) {
            $error = __('Administrator account was not found for the 2FA recovery codes email.', 'xerotact-shield-x-security');
        } else {
            $email = $this->sanitizeSetupEmail($this->userObjectValue($user, 'user_email', ''));
        }

        if ($error === '' && $email === '') {
            $error = __('Administrator email is not available for the 2FA recovery codes email.', 'xerotact-shield-x-security');
        } elseif ($error === '' && empty($codes)) {
            $error = __('No 2FA recovery codes were generated to email.', 'xerotact-shield-x-security');
        } elseif ($error === '' && !function_exists('wp_mail')) {
            $error = __('WordPress mail is not available in this context.', 'xerotact-shield-x-security');
        }

        if ($error === '') {
            $mailError = '';
            $sent = $this->safeWpMail(
                $email,
                $this->recoveryCodesSubject(),
                $this->recoveryCodesEmailBody($userId, $codes),
                EmailTemplate::headers(),
                $mailError
            );

            if (!$sent) {
                $error = $mailError !== '' ? $mailError : __('wp_mail returned false. Check SMTP or host mail settings.', 'xerotact-shield-x-security');
            }
        }

        $this->log($sent ? 'login_security.recovery_codes_email_sent' : 'login_security.recovery_codes_email_failed', [
            'user_id' => $userId,
            'email' => $email,
            'error' => $error,
        ]);

        return [
            'sent' => $sent,
            'email' => $email,
            'error' => $error,
        ];
    }

    public function disableForUser(int $userId): void
    {
        if (!$this->currentRequestCanManageTwoFactorUser($userId)) {
            return;
        }

        delete_user_meta($userId, self::META_ENABLED);
        delete_user_meta($userId, self::META_SECRET);
        delete_user_meta($userId, self::META_PENDING_SECRET);
        delete_user_meta($userId, self::META_RECOVERY_HASHES);
        delete_user_meta($userId, self::META_ENABLED_AT);
        delete_user_meta($userId, self::META_RECOVERY_USED_AT);
        $this->log('login_security.2fa_disabled', ['user_id' => $userId]);
    }

    /**
     * @return array{sent:bool,email:string,error:string,expires_in:int}
     */
    public function requestTwoFactorResetCode(int $userId): array
    {
        $email = $this->settings->recoveryEmail();
        $error = '';
        $sent = false;

        if (!$this->isTwoFactorEnabled($userId)) {
            $error = __('SHIELD-X 2FA is not enabled for this account.', 'xerotact-shield-x-security');
        } elseif ($email === '') {
            $error = __('Recovery email is not configured.', 'xerotact-shield-x-security');
        } elseif ($this->resetCodeRequestThrottled($userId)) {
            $error = __('A SHIELD-X 2FA reset code was requested recently. Wait a few minutes before requesting another code.', 'xerotact-shield-x-security');
        } elseif (!function_exists('wp_mail')) {
            $error = __('WordPress mail is not available in this context.', 'xerotact-shield-x-security');
        } elseif (!function_exists('set_transient')) {
            $error = __('WordPress transient storage is not available for reset codes.', 'xerotact-shield-x-security');
        } else {
            $code = $this->generateResetCode();
            $payload = [
                'hash' => $this->hashResetCode($code),
                'created_at' => time(),
                'expires_at' => time() + self::RESET_CODE_TTL,
                'attempts' => 0,
                'email' => $email,
            ];

            set_transient($this->resetCodeTransientKey($userId), $payload, self::RESET_CODE_TTL);

            $mailError = '';
            $sent = $this->safeWpMail(
                $email,
                $this->resetCodeSubject(),
                $this->resetCodeBody($userId, $code),
                EmailTemplate::headers(),
                $mailError
            );

            if (!$sent) {
                $error = $mailError !== '' ? $mailError : __('wp_mail returned false. Check SMTP or host mail settings.', 'xerotact-shield-x-security');
                if (function_exists('delete_transient')) {
                    delete_transient($this->resetCodeTransientKey($userId));
                }
            }
        }

        $this->settings->save([
            'recovery_email_status' => $sent ? 'sent' : 'failed',
            'recovery_email_error' => $error,
        ]);

        $this->log($sent ? 'login_security.2fa_reset_code_sent' : 'login_security.2fa_reset_code_failed', [
            'user_id' => $userId,
            'email' => $email,
            'error' => $error,
        ]);

        return [
            'sent' => $sent,
            'email' => $email,
            'error' => $error,
            'expires_in' => self::RESET_CODE_TTL,
        ];
    }

    /**
     * @return array{ok:bool,error:string}
     */
    public function confirmTwoFactorResetCode(int $userId, string $code): array
    {
        $code = $this->normalizeResetCode($code);

        if (!$this->currentRequestCanManageTwoFactorUser($userId)) {
            return [
                'ok' => false,
                'error' => __('You do not have permission to reset SHIELD-X 2FA for this administrator.', 'xerotact-shield-x-security'),
            ];
        }

        if ($userId <= 0 || $code === '' || !function_exists('get_transient')) {
            return [
                'ok' => false,
                'error' => __('Enter the reset code from the SHIELD-X email.', 'xerotact-shield-x-security'),
            ];
        }

        $key = $this->resetCodeTransientKey($userId);
        $payload = get_transient($key);

        if (!is_array($payload) || empty($payload['hash']) || (int) ($payload['expires_at'] ?? 0) < time()) {
            if (function_exists('delete_transient')) {
                delete_transient($key);
            }

            return [
                'ok' => false,
                'error' => __('The SHIELD-X 2FA reset code is expired or missing. Request a new code.', 'xerotact-shield-x-security'),
            ];
        }

        $expected = (string) $payload['hash'];
        if (!hash_equals($expected, $this->hashResetCode($code))) {
            $attempts = (int) ($payload['attempts'] ?? 0) + 1;

            if ($attempts >= self::RESET_CODE_MAX_ATTEMPTS) {
                if (function_exists('delete_transient')) {
                    delete_transient($key);
                }
            } elseif (function_exists('set_transient')) {
                $payload['attempts'] = $attempts;
                set_transient($key, $payload, max(60, (int) ($payload['expires_at'] ?? time()) - time()));
            }

            return [
                'ok' => false,
                'error' => __('The SHIELD-X 2FA reset code did not match. 2FA is still enabled.', 'xerotact-shield-x-security'),
            ];
        }

        $this->disableForUser($userId);
        if (function_exists('delete_transient')) {
            delete_transient($key);
        }

        $this->log('login_security.2fa_reset_code_confirmed', ['user_id' => $userId]);

        return [
            'ok' => true,
            'error' => '',
        ];
    }

    /**
     * @param array<int,string> $codes
     */
    public function storeCodesForDisplay(int $userId, array $codes): string
    {
        try {
            $id = bin2hex(random_bytes(6));
        } catch (\Throwable $error) {
            return '';
        }

        if (function_exists('set_transient')) {
            $minute = defined('MINUTE_IN_SECONDS') ? MINUTE_IN_SECONDS : 60;
            set_transient($this->codesTransientKey($userId, $id), $codes, 10 * $minute);
        }

        return $id;
    }

    /**
     * @return array<int,string>
     */
    public function consumeCodesForDisplay(int $userId, string $id): array
    {
        if ($id === '' || !function_exists('get_transient')) {
            return [];
        }

        $key = $this->codesTransientKey($userId, $id);
        $codes = get_transient($key);

        if (function_exists('delete_transient')) {
            delete_transient($key);
        }

        return is_array($codes) ? array_values(array_map('strval', $codes)) : [];
    }

    /**
     * @return array{sent:bool,email:string,error:string,token:string}
     */
    public function sendOwnerRecoveryEmail(bool $rotateToken = true, string $reason = 'manual'): array
    {
        $ip = $this->serverRemoteAddress();
        $token = $rotateToken ? $this->settings->rotateOwnerRecoveryToken(LoginSecuritySettings::hashOwnerRecoveryIp($ip)) : '';
        $email = $this->settings->recoveryEmail();
        $error = '';
        $sent = false;
        $tokenForEmail = $token;

        if ($email === '') {
            $error = __('Recovery email is not configured.', 'xerotact-shield-x-security');
        } elseif (!function_exists('wp_mail')) {
            $error = __('WordPress mail is not available in this context.', 'xerotact-shield-x-security');
        } else {
            $tokenForEmail = $token !== '' ? $token : TwoFactor::randomToken(32);
            if ($token === '') {
                $createdAt = time();
                $this->settings->save([
                    'owner_recovery_token_hash' => LoginSecuritySettings::hashOwnerRecoveryToken($tokenForEmail),
                    'owner_recovery_token_created_at' => gmdate('c', $createdAt),
                    'owner_recovery_token_expires_at' => gmdate('c', $createdAt + self::OWNER_RECOVERY_TOKEN_TTL),
                    'owner_recovery_token_used_at' => '',
                    'owner_recovery_token_ip_hash' => LoginSecuritySettings::hashOwnerRecoveryIp($ip),
                ]);
            }

            $mailError = '';
            $sent = $this->safeWpMail(
                $email,
                $this->emailSubject($reason),
                $this->emailBody($tokenForEmail),
                EmailTemplate::headers(),
                $mailError
            );

            if (!$sent) {
                $error = $mailError !== '' ? $mailError : __('wp_mail returned false. Check SMTP or host mail settings.', 'xerotact-shield-x-security');
            }
        }

        $this->settings->save([
            'last_recovery_email_sent_at' => $sent ? $this->now() : (string) ($this->settings->all()['last_recovery_email_sent_at'] ?? ''),
            'activation_email_sent_at' => $reason === 'activation' && $sent ? $this->now() : (string) ($this->settings->all()['activation_email_sent_at'] ?? ''),
            'recovery_email_status' => $sent ? 'sent' : 'failed',
            'recovery_email_error' => $error,
        ]);

        $this->log($sent ? 'login_security.recovery_email_sent' : 'login_security.recovery_email_failed', [
            'email' => $email,
            'reason' => $reason,
            'error' => $error,
            'expires_in' => self::OWNER_RECOVERY_TOKEN_TTL,
        ]);

        return [
            'sent' => $sent,
            'email' => $email,
            'error' => $error,
            'token' => $sent ? $tokenForEmail : '',
        ];
    }

    /**
     * @return array{sent:int,failed:int,skipped:int,total:int}
     */
    public function sendTwoFactorSetupEmailsToPendingUsers(): array
    {
        $summary = [
            'sent' => 0,
            'failed' => 0,
            'skipped' => 0,
            'total' => 0,
        ];

        if (!$this->settings->enabled() || $this->settings->twoFactorRequiredRoles() === [] || !function_exists('get_users')) {
            return $summary;
        }

        $usersById = [];
        foreach ($this->settings->twoFactorRequiredRoles() as $role) {
            foreach ((array) get_users([
                'role' => $role,
                'orderby' => 'user_login',
                'order' => 'ASC',
                'number' => 500,
            ]) as $user) {
                $userId = $this->userObjectId($user);
                if ($userId > 0) {
                    $usersById[$userId] = $user;
                }
            }
        }
        $users = array_values($usersById);

        foreach ($users as $user) {
            $userId = $this->userObjectId($user);
            if ($userId <= 0 || !$this->isSubjectUserObject($user)) {
                $summary['skipped']++;
                continue;
            }

            if ($this->isTwoFactorEnabled($userId)) {
                $summary['skipped']++;
                continue;
            }

            $summary['total']++;
            $email = $this->sanitizeSetupEmail($this->userObjectValue($user, 'user_email', ''));
            if ($email === '' || !function_exists('wp_mail')) {
                $summary['failed']++;
                $this->log('login_security.2fa_setup_email_failed', [
                    'user_id' => $userId,
                    'email' => $email,
                    'error' => $email === '' ? 'missing_email' : 'wp_mail_unavailable',
                ]);
                continue;
            }

            $mailError = '';
            $sent = $this->safeWpMail(
                $email,
                $this->twoFactorSetupSubject(),
                $this->twoFactorSetupEmailBody($userId),
                EmailTemplate::headers(),
                $mailError
            );

            if ($sent) {
                $summary['sent']++;
            } else {
                $summary['failed']++;
            }

            $this->log($sent ? 'login_security.2fa_setup_email_sent' : 'login_security.2fa_setup_email_failed', [
                'user_id' => $userId,
                'email' => $email,
                'error' => $sent ? '' : ($mailError !== '' ? $mailError : 'wp_mail_failed'),
            ]);
        }

        return $summary;
    }

    public function renderMagicLoginEntry(): void
    {
        if (!$this->settings->magicLoginEnabled() || $this->requestText('action') === 'shieldx_magic_login') {
            return;
        }

        $url = $this->magicLoginUrl();
        ?>
        <div class="shieldx-magic-login-entry" style="margin:16px 0;text-align:center;">
            <a href="<?php echo esc_url($url); ?>" style="display:inline-flex;align-items:center;justify-content:center;min-height:38px;padding:0 14px;border:1px solid #2f56e9;border-radius:4px;text-decoration:none;font-weight:600;color:#2f56e9;background:#fff;">
                <?php echo esc_html__('Magic Login ★', 'xerotact-shield-x-security'); ?>
            </a>
            <p style="margin:8px 0 0;color:#646970;font-size:12px;line-height:1.4;">
                <?php echo esc_html__('Passwordless login via secure email links or 6-digit codes with QR support.', 'xerotact-shield-x-security'); ?>
            </p>
        </div>
        <?php
    }

    public function handleMagicLoginRequest(): void
    {
        if (!$this->settings->magicLoginEnabled()) {
            $this->renderMagicLoginPage('', __('Magic Login is not enabled for this site.', 'xerotact-shield-x-security'));
            exit;
        }

        $method = $this->serverRequestMethod();

        if ($method === 'POST') {
            if (!$this->magicLoginNonceIsValid()) {
                $this->renderMagicLoginPage('', $this->magicLoginGenericVerificationError());
                exit;
            }

            if ($this->magicLoginIpThrottled()) {
                $this->log('login_security.magic_login_ip_throttled', ['ip' => $this->serverRemoteAddress()]);
                $this->renderMagicLoginPage('', __('Too many Magic Login attempts were submitted from this network. Wait a few minutes and try again.', 'xerotact-shield-x-security'));
                exit;
            }
        }

        $session = $this->requestPostText('shieldx_magic_session');
        if ($session !== '') {
            $result = $this->completeMagicLoginSession($session, $this->requestPostText('shieldx_totp_code'));
            $this->continueMagicLogin($result);
            exit;
        }

        $postedToken = $this->requestPostText('shieldx_magic_token');
        if ($postedToken !== '') {
            $result = $this->verifyMagicLoginToken($postedToken);
            $this->continueMagicLogin($result);
            exit;
        }

        $token = $method === 'GET' ? $this->requestText('shieldx_magic_token') : '';
        if ($token !== '') {
            $this->renderMagicLoginConfirmationPage($token);
            exit;
        }

        if ($method === 'POST') {
            $identifier = $this->requestPostText('shieldx_magic_identifier');
            $code = $this->requestPostText('shieldx_magic_code');

            if ($code !== '') {
                $result = $this->verifyMagicLoginCode($identifier, $code);
                $this->continueMagicLogin($result, $identifier);
                exit;
            }

            $request = $this->requestMagicLogin($identifier);
            $this->renderMagicLoginPage((string) ($request['message'] ?? ''), (string) ($request['error'] ?? ''), $identifier);
            exit;
        }

        $this->renderMagicLoginPage();
        exit;
    }

    /**
     * @return array{requested:bool,sent:bool,email:string,error:string,message:string,expires_in:int}
     */
    public function requestMagicLogin(string $identifier): array
    {
        $identifier = trim($identifier);
        $genericMessage = __('If that administrator account exists, SHIELD-X has sent a one-time magic login email.', 'xerotact-shield-x-security');

        if (!$this->settings->magicLoginEnabled()) {
            return [
                'requested' => false,
                'sent' => false,
                'email' => '',
                'error' => __('Magic Login is not enabled for this site.', 'xerotact-shield-x-security'),
                'message' => '',
                'expires_in' => self::MAGIC_LOGIN_TTL,
            ];
        }

        if ($identifier === '') {
            return [
                'requested' => false,
                'sent' => false,
                'email' => '',
                'error' => __('Enter an administrator username or email address.', 'xerotact-shield-x-security'),
                'message' => '',
                'expires_in' => self::MAGIC_LOGIN_TTL,
            ];
        }

        if (!function_exists('set_transient') || !function_exists('wp_mail')) {
            $error = __('WordPress mail and transient storage are required for Magic Login.', 'xerotact-shield-x-security');
            $this->recordMagicLoginMailStatus(false, $error);

            return [
                'requested' => false,
                'sent' => false,
                'email' => '',
                'error' => $error,
                'message' => '',
                'expires_in' => self::MAGIC_LOGIN_TTL,
            ];
        }

        $user = $this->adminUserByIdentifier($identifier);
        if (!$this->isSubjectUserObject($user)) {
            $this->log('login_security.magic_login_requested_unknown', ['identifier' => $identifier]);

            return [
                'requested' => true,
                'sent' => false,
                'email' => '',
                'error' => '',
                'message' => $genericMessage,
                'expires_in' => self::MAGIC_LOGIN_TTL,
            ];
        }

        $userId = (int) $user->ID;
        $email = $this->userObjectValue($user, 'user_email', '');
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->log('login_security.magic_login_email_missing', ['user_id' => $userId]);
            $this->recordMagicLoginMailStatus(false, __('Administrator account does not have a valid email address.', 'xerotact-shield-x-security'));

            return [
                'requested' => true,
                'sent' => false,
                'email' => '',
                'error' => '',
                'message' => $genericMessage,
                'expires_in' => self::MAGIC_LOGIN_TTL,
            ];
        }

        if ($this->magicLoginRequestThrottled($userId) || $this->magicLoginUserBudgetExceeded($userId, 'request', self::MAGIC_LOGIN_USER_DAILY_REQUEST_LIMIT)) {
            $this->log('login_security.magic_login_throttled', ['user_id' => $userId]);

            return [
                'requested' => true,
                'sent' => false,
                'email' => $email,
                'error' => '',
                'message' => $genericMessage,
                'expires_in' => self::MAGIC_LOGIN_TTL,
            ];
        }

        $token = TwoFactor::randomToken(32);
        $code = $this->generateMagicLoginCode();
        $tokenKey = $this->magicTokenTransientKey($token);
        $userKey = $this->magicUserTransientKey($userId);
        $payload = [
            'user_id' => $userId,
            'token_hash' => $this->hashMagicLoginToken($token),
            'code_hash' => $this->hashMagicLoginCode($userId, $code),
            'token_key' => $tokenKey,
            'user_key' => $userKey,
            'created_at' => time(),
            'expires_at' => time() + self::MAGIC_LOGIN_TTL,
            'attempts' => 0,
        ];

        set_transient($tokenKey, $payload, self::MAGIC_LOGIN_TTL);
        set_transient($userKey, $payload, self::MAGIC_LOGIN_TTL);

        $link = $this->magicLoginUrl(['shieldx_magic_token' => $token]);
        $mailError = '';
        $sent = $this->safeWpMail(
            $email,
            $this->magicLoginSubject(),
            $this->magicLoginEmailBody($userId, $link, $code),
            EmailTemplate::headers(),
            $mailError
        );

        if (!$sent) {
            $this->deleteMagicLoginPayload($payload);
        }

        $this->recordMagicLoginMailStatus($sent, $sent ? '' : ($mailError !== '' ? $mailError : __('wp_mail() returned false. Check SMTP or host mail settings.', 'xerotact-shield-x-security')));

        $this->log($sent ? 'login_security.magic_login_sent' : 'login_security.magic_login_failed', [
            'user_id' => $userId,
            'email' => $email,
        ]);

        return [
            'requested' => true,
            'sent' => $sent,
            'email' => $email,
            'error' => $sent ? '' : __('SHIELD-X could not send the Magic Login email. Check SMTP or host mail settings.', 'xerotact-shield-x-security'),
            'message' => $sent ? $genericMessage : '',
            'expires_in' => self::MAGIC_LOGIN_TTL,
        ];
    }

    private function recordMagicLoginMailStatus(bool $sent, string $error = ''): void
    {
        $settings = $this->settings->all();
        $this->settings->save([
            'last_magic_login_email_sent_at' => $sent ? $this->now() : (string) ($settings['last_magic_login_email_sent_at'] ?? ''),
            'magic_login_email_status' => $sent ? 'sent' : 'failed',
            'magic_login_email_error' => $sent ? '' : $error,
        ]);
    }

    /**
     * @return array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string}
     */
    public function verifyMagicLoginToken(string $token): array
    {
        $token = $this->normalizeMagicToken($token);
        if ($token === '' || !function_exists('get_transient')) {
            return $this->magicLoginResult(false, __('The Magic Login link is invalid or expired.', 'xerotact-shield-x-security'));
        }

        $payload = get_transient($this->magicTokenTransientKey($token));
        if (!$this->isValidMagicLoginPayload($payload) || !hash_equals((string) $payload['token_hash'], $this->hashMagicLoginToken($token))) {
            return $this->magicLoginResult(false, __('The Magic Login link is invalid or expired.', 'xerotact-shield-x-security'));
        }

        if (!$this->claimSingleUseToken('magic_login', (string) $payload['token_hash'], self::MAGIC_LOGIN_TTL)) {
            return $this->magicLoginResult(false, __('The Magic Login link is invalid or expired.', 'xerotact-shield-x-security'));
        }

        $this->deleteMagicLoginPayload($payload);

        return $this->prepareMagicLoginResult((int) $payload['user_id'], 'link');
    }

    /**
     * @return array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string}
     */
    public function verifyMagicLoginCode(string $identifier, string $code): array
    {
        $genericError = $this->magicLoginGenericVerificationError();
        $code = $this->normalizeMagicLoginCode($code);
        if ($identifier === '' || $code === '' || !function_exists('get_transient')) {
            return $this->magicLoginResult(false, $genericError);
        }

        $user = $this->adminUserByIdentifier($identifier);
        if (!$this->isSubjectUserObject($user)) {
            return $this->magicLoginResult(false, $genericError);
        }

        $userId = (int) $user->ID;
        $payload = get_transient($this->magicUserTransientKey($userId));
        if (!$this->isValidMagicLoginPayload($payload)) {
            return $this->magicLoginResult(false, $genericError);
        }

        if (!hash_equals((string) $payload['code_hash'], $this->hashMagicLoginCode($userId, $code))) {
            return $this->recordMagicLoginFailure($payload, $genericError);
        }

        if (!$this->claimSingleUseToken('magic_login', (string) $payload['token_hash'], self::MAGIC_LOGIN_TTL)) {
            return $this->magicLoginResult(false, $genericError);
        }

        $this->deleteMagicLoginPayload($payload);

        return $this->prepareMagicLoginResult($userId, 'code');
    }

    /**
     * @return array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string}
     */
    public function completeMagicLoginSession(string $session, string $code): array
    {
        $session = $this->normalizeMagicToken($session);
        $code = trim($code);

        if ($session === '' || $code === '' || !function_exists('get_transient')) {
            return $this->magicLoginResult(false, __('Enter a valid SHIELD-X authenticator code or recovery code.', 'xerotact-shield-x-security'));
        }

        $key = $this->magicSessionTransientKey($session);
        $payload = get_transient($key);

        if (!is_array($payload) || (int) ($payload['expires_at'] ?? 0) < time() || (int) ($payload['user_id'] ?? 0) <= 0) {
            if (function_exists('delete_transient')) {
                delete_transient($key);
            }

            return $this->magicLoginResult(false, __('Magic Login verification expired. Request a new email.', 'xerotact-shield-x-security'));
        }

        $userId = (int) $payload['user_id'];
        if (!$this->isTwoFactorEnabled($userId) || !$this->verifyLoginCode($userId, $code)) {
            $dailyBudgetExceeded = $this->magicLoginUserBudgetExceeded($userId, 'failure', self::MAGIC_LOGIN_USER_DAILY_FAILURE_LIMIT);
            $attempts = (int) ($payload['attempts'] ?? 0) + 1;

            if ($dailyBudgetExceeded || $attempts >= self::MAGIC_LOGIN_MAX_ATTEMPTS) {
                if (function_exists('delete_transient')) {
                    delete_transient($key);
                }
            } elseif (function_exists('set_transient')) {
                $payload['attempts'] = $attempts;
                set_transient($key, $payload, max(60, (int) ($payload['expires_at'] ?? time()) - time()));
            }

            return $this->magicLoginResult(false, __('Enter a valid SHIELD-X authenticator code or recovery code.', 'xerotact-shield-x-security'), $userId, true, $session, (string) ($payload['method'] ?? ''));
        }

        if (function_exists('delete_transient')) {
            delete_transient($key);
        }

        $method = (string) ($payload['method'] ?? 'magic');
        $this->log('login_security.magic_login', ['user_id' => $userId, 'method' => $method . '+2fa']);

        return $this->magicLoginResult(true, '', $userId, false, '', $method);
    }

    public function renderLoginField(): void
    {
        if (!$this->shouldRenderLoginField()) {
            return;
        }

        ?>
        <p>
            <label for="shieldx_totp_code"><?php echo esc_html__('SHIELD-X authenticator or recovery code', 'xerotact-shield-x-security'); ?><br>
                <input type="text" name="shieldx_totp_code" id="shieldx_totp_code" class="input" value="" size="20" autocomplete="one-time-code">
            </label>
        </p>
        <p>
            <label for="shieldx_recovery_token"><?php echo esc_html__('Owner recovery token', 'xerotact-shield-x-security'); ?><br>
                <input type="text" name="shieldx_recovery_token" id="shieldx_recovery_token" class="input" value="" size="20" autocomplete="off">
            </label>
            <small><?php echo esc_html__('Use this only for the long owner recovery token emailed by SHIELD-X. Regular backup codes go in the authenticator field above.', 'xerotact-shield-x-security'); ?></small>
        </p>
        <?php
    }

    private function shouldRenderLoginField(): bool
    {
        if (!$this->loginFormHasTwoFactorError()) {
            return false;
        }

        $identifier = $this->requestPostText('log');
        if ($identifier === '') {
            return false;
        }

        $user = $this->adminUserByIdentifier($identifier);
        if (!$this->isTwoFactorProtectedUserObject($user)) {
            return false;
        }

        return $this->isTwoFactorEnabled((int) $user->ID);
    }

    private function loginFormHasTwoFactorError(): bool
    {
        global $errors;

        if (!is_object($errors) || !method_exists($errors, 'get_error_codes')) {
            return false;
        }

        $codes = $errors->get_error_codes();
        if (!is_array($codes)) {
            return false;
        }

        return in_array('shieldx_2fa_required', array_map('strval', $codes), true);
    }

    /**
     * @param mixed $user
     * @param mixed $username
     * @param mixed $password
     * @return mixed
     */
    public function enforceTwoFactor($user, $username, $password)
    {
        if (!$this->settings->enabled() || $this->isWpError($user) || !$this->isUserObject($user)) {
            return $user;
        }

        $userId = (int) $user->ID;

        if (!$this->isTwoFactorProtectedUserObject($user)) {
            return $user;
        }

        if (!$this->isTwoFactorEnabled($userId)) {
            if ($this->requiresTwoFactorForUserObject($user) && !$this->isInteractiveLoginRequest() && class_exists('\WP_Error')) {
                return new \WP_Error(
                    'shieldx_2fa_enrollment_required',
                    __('This account must enroll in SHIELD-X two-factor authentication before alternate authentication can be used.', 'xerotact-shield-x-security')
                );
            }

            return $user;
        }

        $code = $this->requestPostText('shieldx_totp_code');
        $recoveryToken = $this->requestPostText('shieldx_recovery_token');

        if ($this->consumeOwnerRecoveryTokenForUser($user, $recoveryToken)) {
            return $user;
        }

        $canVerifyTotp = $this->canVerifyTwoFactorForUser($userId);
        if ($this->twoFactorUserLocked($userId) && class_exists('\WP_Error')) {
            return new \WP_Error(
                'shieldx_2fa_locked',
                __('Too many invalid SHIELD-X 2FA attempts. Wait a few minutes and try again.', 'xerotact-shield-x-security')
            );
        }

        if ($this->verifyLoginCode($userId, $code)) {
            $this->clearTwoFactorFailures($userId);
            return $user;
        }

        if ($code !== '') {
            $this->recordTwoFactorFailure($userId);
        }

        // A pre-issued recovery code remains a valid second factor, but never
        // let unavailable encrypted TOTP storage degrade into a normal login.
        if (!$canVerifyTotp && class_exists('\WP_Error')) {
            return new \WP_Error(
                'shieldx_2fa_secret_storage_unavailable',
                __('Two-factor authentication is temporarily unavailable. Contact the site administrator.', 'xerotact-shield-x-security')
            );
        }

        if (class_exists('\WP_Error')) {
            return new \WP_Error(
                'shieldx_2fa_required',
                __('Enter a valid SHIELD-X authenticator code or recovery code.', 'xerotact-shield-x-security')
            );
        }

        return $user;
    }

    /**
     * @param mixed $available
     * @param mixed $user
     */
    public function applicationPasswordsAvailableForUser($available, $user): bool
    {
        if (!$this->settings->enabled() || !$this->isUserObject($user)) {
            return (bool) $available;
        }

        $userId = $this->userObjectId($user);
        if ($userId <= 0 || !$this->isTwoFactorProtectedUserObject($user)) {
            return (bool) $available;
        }

        return false;
    }

    /**
     * @param mixed $result
     * @return mixed
     */
    public function blockRestApplicationPasswordForTwoFactorUsers($result)
    {
        if (!$this->settings->enabled() || $this->isWpError($result) || !$this->requestUsesBasicAuth()) {
            return $result;
        }

        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $user = $userId > 0 && function_exists('get_userdata') ? get_userdata($userId) : null;
        if ($userId <= 0 || !$this->isTwoFactorProtectedUserObject($user)) {
            return $result;
        }

        if (class_exists('\WP_Error')) {
            return new \WP_Error(
                'shieldx_2fa_application_password_blocked',
                __('Application Passwords are disabled for roles protected by SHIELD-X 2FA.', 'xerotact-shield-x-security'),
                ['status' => 401]
            );
        }

        return $result;
    }

    public function maybeRedirectAdminSetup(): void
    {
        if (!$this->settings->enabled() || $this->settings->twoFactorRequiredRoles() === []) {
            return;
        }

        if ((function_exists('wp_doing_ajax') && wp_doing_ajax()) || (defined('WP_CLI') && WP_CLI)) {
            return;
        }

        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;

        if ($userId <= 0 || !$this->requiresTwoFactorForUserId($userId) || $this->isTwoFactorEnabled($userId)) {
            return;
        }

        $capability = defined('SHIELDX_MANAGE_CAPABILITY') ? (string) SHIELDX_MANAGE_CAPABILITY : 'manage_options';
        if (!function_exists('current_user_can') || !current_user_can($capability)) {
            return;
        }

        $page = '';
        // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only admin route detection for the 2FA enrollment notice.
        if (isset($_GET['page']) && is_scalar($_GET['page'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Page query value is unslashed here and reduced to a sanitized key below.
            $rawPage = function_exists('wp_unslash') ? wp_unslash($_GET['page']) : $_GET['page'];
            $page = is_scalar($rawPage) ? $this->sanitizeKey((string) $rawPage) : '';
        }
        // phpcs:enable WordPress.Security.NonceVerification.Recommended
        $requestUri = '';
        if (isset($_SERVER['REQUEST_URI']) && is_scalar($_SERVER['REQUEST_URI'])) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Request URI is used for read-only routing and normalized before basename comparison.
            $rawUri = function_exists('wp_unslash') ? wp_unslash($_SERVER['REQUEST_URI']) : $_SERVER['REQUEST_URI'];
            $requestUri = is_scalar($rawUri) ? (string) $rawUri : '';
        }
        $path = explode('?', explode('#', $requestUri, 2)[0], 2)[0];
        $basename = basename($path);

        if (in_array($page, ['shield-x-login-security', 'shield-x-setup'], true) || in_array($basename, ['admin-post.php', 'admin-ajax.php'], true)) {
            return;
        }

        if (function_exists('wp_safe_redirect') && function_exists('admin_url')) {
            wp_safe_redirect(add_query_arg('shieldx_setup', 'required', admin_url('admin.php?page=shield-x-login-security')));
            exit;
        }
    }

    /**
     * @param mixed $user
     */
    public function recordSuccessfulLogin(string $userLogin, $user): void
    {
        unset($userLogin);
        if (!$this->isUserObject($user) || !function_exists('update_user_meta')) {
            return;
        }

        update_user_meta((int) $user->ID, self::META_LAST_LOGIN_AT, gmdate('c'));
    }

    public function enforceSessionPolicy(): void
    {
        if (!$this->settings->sessionPolicyEnabled()
            || (defined('WP_CLI') && WP_CLI)
            || (function_exists('wp_doing_cron') && wp_doing_cron())
            || !function_exists('is_user_logged_in')
            || !is_user_logged_in()
        ) {
            return;
        }

        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        if ($userId <= 0 || !$this->requiresSessionPolicyForUserId($userId)) {
            return;
        }

        $token = $this->currentSessionToken();
        $manager = $this->sessionManager($userId);
        if ($token === '' || !$manager) {
            return;
        }

        $session = $manager->get($token);
        if (!is_array($session)) {
            return;
        }

        $now = time();
        $loginAt = max(0, (int) ($session['login'] ?? 0));
        $lastSeen = max($loginAt, (int) ($session[self::SESSION_LAST_SEEN_KEY] ?? $loginAt));
        $maximumAgeExpired = $loginAt > 0 && ($now - $loginAt) > $this->settings->sessionMaximumAge();
        $idleExpired = $lastSeen > 0 && ($now - $lastSeen) > $this->settings->sessionIdleTimeout();

        if ($maximumAgeExpired || $idleExpired) {
            $manager->destroy($token);
            if (function_exists('wp_clear_auth_cookie')) {
                wp_clear_auth_cookie();
            }
            $this->log('login_security.session_expired', [
                'user_id' => $userId,
                'reason' => $maximumAgeExpired ? 'maximum_age' : 'idle_timeout',
            ]);
            $this->endExpiredSessionRequest();

            return;
        }

        if (($now - $lastSeen) >= self::SESSION_LAST_SEEN_WRITE_INTERVAL) {
            $session[self::SESSION_LAST_SEEN_KEY] = $now;
            $manager->update($token, $session);
        }
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function sessionReview(int $limit = 100): array
    {
        if (!function_exists('get_users')) {
            return [];
        }

        $limit = max(1, min(250, $limit));
        $users = get_users([
            'number' => $limit,
            'orderby' => 'user_login',
            'order' => 'ASC',
            'fields' => 'all',
        ]);
        $currentUserId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $review = [];

        foreach ((array) $users as $user) {
            if (!$this->isUserObject($user) || (!$this->requiresSessionPolicyForUserObject($user) && !$this->requiresTwoFactorForUserObject($user))) {
                continue;
            }

            $manager = $this->sessionManager((int) $user->ID);
            $sessions = $manager ? $manager->get_all() : [];
            $review[] = [
                'user_id' => (int) $user->ID,
                'user_login' => (string) ($user->user_login ?? ''),
                'display_name' => (string) ($user->display_name ?? ''),
                'roles' => $this->userRoles($user),
                'sessions' => is_array($sessions) ? count($sessions) : 0,
                'current_user' => (int) $user->ID === $currentUserId,
                'two_factor_required' => $this->requiresTwoFactorForUserObject($user),
                'two_factor_enabled' => $this->isTwoFactorEnabled((int) $user->ID),
            ];
        }

        return $review;
    }

    public function revokeOtherSessions(): bool
    {
        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $token = $this->currentSessionToken();
        $manager = $this->sessionManager($userId);
        if ($userId <= 0 || $token === '' || !$manager) {
            return false;
        }

        $manager->destroy_others($token);
        $this->log('login_security.sessions_revoked', ['user_id' => $userId, 'scope' => 'other']);

        return true;
    }

    public function revokeAllSessionsForUser(int $userId): bool
    {
        $manager = $this->sessionManager($userId);
        if ($userId <= 0 || !$manager) {
            return false;
        }

        $currentUserId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $currentToken = $userId === $currentUserId ? $this->currentSessionToken() : '';
        if ($currentToken !== '') {
            $manager->destroy_others($currentToken);
        } else {
            $manager->destroy_all();
        }
        $this->log('login_security.sessions_revoked', [
            'user_id' => $userId,
            'scope' => $currentToken !== '' ? 'other_preserve_current' : 'all',
        ]);

        return true;
    }

    /**
     * @param mixed $user
     */
    public function requiresTwoFactorForUserObject($user): bool
    {
        return $this->userHasAnyRole($user, $this->settings->twoFactorRequiredRoles());
    }

    public function requiresTwoFactorForUserId(int $userId): bool
    {
        $user = $userId > 0 && function_exists('get_userdata') ? get_userdata($userId) : null;

        return $this->requiresTwoFactorForUserObject($user);
    }

    /**
     * @param mixed $user
     */
    public function requiresSessionPolicyForUserObject($user): bool
    {
        return $this->userHasAnyRole($user, $this->settings->sessionProtectedRoles());
    }

    public function requiresSessionPolicyForUserId(int $userId): bool
    {
        $user = $userId > 0 && function_exists('get_userdata') ? get_userdata($userId) : null;

        return $this->requiresSessionPolicyForUserObject($user);
    }

    public function verifyLoginCode(int $userId, string $code): bool
    {
        $code = trim($code);

        if ($code === '') {
            return false;
        }

        $secret = $this->secretForUser($userId, self::META_SECRET);

        if ($secret !== '' && TwoFactor::verifyTotpOnce($secret, $code, 'totp:' . (string) $userId)) {
            $this->log('login_security.2fa_login', ['user_id' => $userId, 'method' => 'totp']);
            return true;
        }

        return $this->consumeRecoveryCode($userId, $code);
    }

    /**
     * @return array{ok:bool,error:string,user_id:int,method:string}
     */
    public function verifyEmergencyCodeForAdmin(string $identifier, string $code): array
    {
        $identifier = trim($identifier);
        $code = trim($code);

        if ($identifier === '' || $code === '') {
            return [
                'ok' => false,
                'error' => __('Enter an administrator username/email and authenticator code.', 'xerotact-shield-x-security'),
                'user_id' => 0,
                'method' => '',
            ];
        }

        $user = $this->adminUserByIdentifier($identifier);
        $genericError = __('Enter a valid SHIELD-X authenticator code or recovery code.', 'xerotact-shield-x-security');

        if (!$this->isSubjectUserObject($user)) {
            return [
                'ok' => false,
                'error' => $genericError,
                'user_id' => 0,
                'method' => '',
            ];
        }

        $userId = (int) $user->ID;
        if (!$this->isTwoFactorEnabled($userId)) {
            return [
                'ok' => false,
                'error' => $genericError,
                'user_id' => 0,
                'method' => '',
            ];
        }

        $secret = $this->secretForUser($userId, self::META_SECRET);
        if ($secret !== '' && TwoFactor::verifyTotpOnce($secret, $code, 'totp:' . (string) $userId)) {
            $this->log('login_security.emergency_2fa', ['user_id' => $userId, 'method' => 'totp']);

            return [
                'ok' => true,
                'error' => '',
                'user_id' => $userId,
                'method' => 'totp',
            ];
        }

        if ($this->consumeRecoveryCode($userId, $code)) {
            $this->log('login_security.emergency_2fa', ['user_id' => $userId, 'method' => 'recovery_code']);

            return [
                'ok' => true,
                'error' => '',
                'user_id' => $userId,
                'method' => 'recovery_code',
            ];
        }

        return [
            'ok' => false,
            'error' => $genericError,
            'user_id' => 0,
            'method' => '',
        ];
    }

    public function isTwoFactorEnabled(int $userId): bool
    {
        // Treat explicit enrolment as authoritative. A missing OpenSSL module or
        // an unreadable encrypted secret must never silently turn 2FA off.
        return (string) get_user_meta($userId, self::META_ENABLED, true) === '1';
    }

    /**
     * @return array<int,string>
     */
    public static function userMetaKeys(): array
    {
        return [
            self::META_ENABLED,
            self::META_SECRET,
            self::META_PENDING_SECRET,
            self::META_RECOVERY_HASHES,
            self::META_ENABLED_AT,
            self::META_RECOVERY_USED_AT,
            self::META_LAST_LOGIN_AT,
        ];
    }

    /**
     * @param mixed $user
     */
    private function consumeOwnerRecoveryTokenForUser($user, string $token): bool
    {
        $ip = $this->serverRemoteAddress();
        if ($token === '' || !$this->settings->matchesOwnerRecoveryToken($token, $ip) || !$this->isSubjectUserObject($user)) {
            return false;
        }

        if (!$this->claimSingleUseToken('owner_recovery', $this->settings->ownerRecoveryTokenHash(), self::OWNER_RECOVERY_TOKEN_TTL)) {
            return false;
        }

        $this->settings->markOwnerRecoveryTokenUsed();
        update_user_meta((int) $user->ID, self::META_RECOVERY_USED_AT, $this->now());
        $this->log('login_security.owner_recovery_used', [
            'user_id' => (int) $user->ID,
            'ip' => $ip,
        ]);

        return true;
    }

    private function consumeRecoveryCode(int $userId, string $code): bool
    {
        $hashes = $this->recoveryHashes($userId);

        foreach ($hashes as $index => $hash) {
            if (TwoFactor::verifyRecoveryCode($code, $hash)) {
                if (!$this->claimSingleUseToken('recovery_code', (string) $userId . '|' . $hash, self::RECOVERY_CODE_CLAIM_TTL)) {
                    return false;
                }

                unset($hashes[$index]);
                update_user_meta($userId, self::META_RECOVERY_HASHES, array_values($hashes));
                $this->log('login_security.2fa_login', ['user_id' => $userId, 'method' => 'recovery_code']);

                return true;
            }
        }

        return false;
    }

    /**
     * @return array<int,string>
     */
    private function recoveryHashes(int $userId): array
    {
        $hashes = get_user_meta($userId, self::META_RECOVERY_HASHES, true);

        if (!is_array($hashes)) {
            return [];
        }

        return array_values(array_filter(array_map('strval', $hashes)));
    }

    /**
     * @param array<int,string> $codes
     * @return array<int,string>
     */
    private function hashRecoveryCodes(array $codes): array
    {
        return array_values(array_map(static function (string $code): string {
            return TwoFactor::hashRecoveryCode($code);
        }, $codes));
    }

    private function currentRequestCanManageTwoFactorUser(int $userId): bool
    {
        $user = $userId > 0 && function_exists('get_userdata') ? get_userdata($userId) : null;
        if (!$this->isUserObject($user)) {
            return false;
        }

        $currentUserId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        if ($currentUserId > 0 && $currentUserId === $userId) {
            return true;
        }

        $capability = defined('SHIELDX_MANAGE_CAPABILITY') ? (string) SHIELDX_MANAGE_CAPABILITY : 'manage_options';

        return function_exists('current_user_can')
            && current_user_can($capability)
            && current_user_can('edit_user', $userId);
    }

    /**
     * @param mixed $user
     */
    private function isSubjectUserObject($user): bool
    {
        if (!$this->isUserObject($user)) {
            return false;
        }

        if (method_exists($user, 'has_cap')) {
            return (bool) $user->has_cap('manage_options');
        }

        $caps = isset($user->allcaps) && is_array($user->allcaps) ? $user->allcaps : [];

        return !empty($caps['manage_options']);
    }

    /**
     * @param mixed $user
     */
    private function isTwoFactorProtectedUserObject($user): bool
    {
        if (!$this->isUserObject($user)) {
            return false;
        }

        $userId = (int) $user->ID;

        return $this->isTwoFactorEnabled($userId) || $this->requiresTwoFactorForUserObject($user);
    }

    /**
     * @param mixed             $user
     * @param array<int,string> $roles
     */
    private function userHasAnyRole($user, array $roles): bool
    {
        if (!$this->isUserObject($user) || $roles === []) {
            return false;
        }

        return array_intersect($this->userRoles($user), $roles) !== [];
    }

    /**
     * @param mixed $user
     * @return array<int,string>
     */
    private function userRoles($user): array
    {
        if (!$this->isUserObject($user)) {
            return [];
        }

        $roles = isset($user->roles) && is_array($user->roles) ? $user->roles : [];

        return array_values(array_unique(array_filter(array_map('strval', $roles))));
    }

    private function isInteractiveLoginRequest(): bool
    {
        if ((defined('XMLRPC_REQUEST') && XMLRPC_REQUEST) || (defined('REST_REQUEST') && REST_REQUEST)) {
            return false;
        }

        $requestUri = isset($_SERVER['REQUEST_URI']) && is_scalar($_SERVER['REQUEST_URI'])
            ? sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_URI']))
            : '';
        $path = explode('?', $requestUri, 2)[0];

        return basename($path) === 'wp-login.php';
    }

    private function currentSessionToken(): string
    {
        if (!function_exists('wp_get_session_token')) {
            return '';
        }

        return trim((string) wp_get_session_token());
    }

    /**
     * @return object|null
     */
    private function sessionManager(int $userId)
    {
        if ($userId <= 0 || !class_exists('\WP_Session_Tokens')) {
            return null;
        }

        return \WP_Session_Tokens::get_instance($userId);
    }

    private function endExpiredSessionRequest(): void
    {
        $nonBrowser = (defined('REST_REQUEST') && REST_REQUEST)
            || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST)
            || (function_exists('wp_doing_ajax') && wp_doing_ajax());
        if ($nonBrowser && function_exists('wp_die')) {
            wp_die(
                esc_html__('Your SHIELD-X protected session expired. Sign in again.', 'xerotact-shield-x-security'),
                esc_html__('Session expired', 'xerotact-shield-x-security'),
                ['response' => 401]
            );
        }

        if (function_exists('wp_safe_redirect') && function_exists('wp_login_url')) {
            wp_safe_redirect(wp_login_url());
            exit;
        }
    }

    /**
     * @param mixed $user
     */
    private function isUserObject($user): bool
    {
        return is_object($user) && isset($user->ID);
    }

    /**
     * @param mixed $user
     */
    private function userObjectId($user): int
    {
        if (is_object($user) && isset($user->ID)) {
            return (int) $user->ID;
        }

        if (is_array($user) && isset($user['ID'])) {
            return (int) $user['ID'];
        }

        return 0;
    }

    /**
     * @param mixed $user
     */
    private function userObjectValue($user, string $key, string $fallback): string
    {
        if (is_object($user) && isset($user->{$key}) && is_scalar($user->{$key})) {
            return (string) $user->{$key};
        }

        if (is_array($user) && isset($user[$key]) && is_scalar($user[$key])) {
            return (string) $user[$key];
        }

        return $fallback;
    }

    /**
     * @param mixed $value
     */
    private function isWpError($value): bool
    {
        return class_exists('\WP_Error') && $value instanceof \WP_Error;
    }

    /**
     * @return array{total:int,enrolled:int,pending:int}
     */
    private function adminEnrollmentCounts(): array
    {
        if (!function_exists('get_users')) {
            return ['total' => 0, 'enrolled' => 0, 'pending' => 0];
        }

        $users = get_users([
            'role' => 'administrator',
            'fields' => ['ID'],
        ]);
        $total = 0;
        $enrolled = 0;

        foreach ($users as $user) {
            $userId = is_object($user) && isset($user->ID) ? (int) $user->ID : 0;

            if ($userId <= 0) {
                continue;
            }

            $total++;
            if ($this->isTwoFactorEnabled($userId)) {
                $enrolled++;
            }
        }

        return [
            'total' => $total,
            'enrolled' => $enrolled,
            'pending' => max(0, $total - $enrolled),
        ];
    }

    /**
     * @return array{total:int,enrolled:int,pending:int}
     */
    private function requiredRoleEnrollmentCounts(): array
    {
        if (!function_exists('get_users') || $this->settings->twoFactorRequiredRoles() === []) {
            return ['total' => 0, 'enrolled' => 0, 'pending' => 0];
        }

        $users = get_users([
            'fields' => ['ID', 'roles'],
            'number' => 5000,
        ]);
        $total = 0;
        $enrolled = 0;

        foreach ($users as $user) {
            if (!$this->requiresTwoFactorForUserObject($user)) {
                continue;
            }

            $userId = is_object($user) && isset($user->ID) ? (int) $user->ID : 0;
            if ($userId <= 0) {
                continue;
            }

            $total++;
            if ($this->isTwoFactorEnabled($userId)) {
                $enrolled++;
            }
        }

        return [
            'total' => $total,
            'enrolled' => $enrolled,
            'pending' => max(0, $total - $enrolled),
        ];
    }

    private function accountLabel(int $userId): string
    {
        $user = function_exists('get_userdata') ? get_userdata($userId) : null;

        if (is_object($user) && !empty($user->user_email)) {
            return (string) $user->user_email;
        }

        return 'user-' . (string) $userId;
    }

    private function lastLoginAt(int $userId): string
    {
        if (!function_exists('get_user_meta')) {
            return '';
        }

        foreach (['shieldx_last_login_at', 'last_login', 'wp_last_login', 'itsec_last_login'] as $key) {
            $value = get_user_meta($userId, $key, true);

            if (!is_scalar($value)) {
                continue;
            }

            $value = trim((string) $value);

            if ($value === '') {
                continue;
            }

            if (ctype_digit($value)) {
                $timestamp = (int) $value;
                return $timestamp > 0 ? gmdate('Y-m-d H:i:s', $timestamp) : '';
            }

            return $value;
        }

        return '';
    }

    /**
     * @return object|null
     */
    private function adminUserByIdentifier(string $identifier)
    {
        if (!function_exists('get_user_by')) {
            return null;
        }

        $identifier = trim($identifier);
        if ($identifier === '') {
            return null;
        }

        if (ctype_digit($identifier)) {
            $user = get_user_by('id', (int) $identifier);
            return $this->isSubjectUserObject($user) ? $user : null;
        }

        $field = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'login';
        $user = get_user_by($field, $identifier);

        return $this->isSubjectUserObject($user) ? $user : null;
    }

    private function emergencyRecoveryUrl(string $token): string
    {
        unset($token);

        if (function_exists('home_url')) {
            return home_url('/index.php?shieldx_recovery=1');
        }

        return '/index.php?shieldx_recovery=1';
    }

    public function emergencyRecoveryUrlForToken(string $token): string
    {
        return $this->emergencyRecoveryUrl($token);
    }

    public function emergencyRecoveryEndpoint(): string
    {
        if (function_exists('home_url')) {
            return home_url('/index.php?shieldx_recovery=1');
        }

        return '/index.php?shieldx_recovery=1';
    }

    public function emergencyRecoveryHeaderName(): string
    {
        return 'X-ShieldX-Recovery-Token';
    }

    public function emergencyRecoveryTokenLabel(string $token): string
    {
        $token = trim($token);
        if ($token === '') {
            return '';
        }

        return $token;
    }

    private function emailSubject(string $reason): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        /* translators: Placeholder values are generated from SHIELD-X status data, file paths, counts, or action names. */
        return sprintf(__('SHIELD-X recovery access for %s', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress');
    }

    private function emailBody(string $token): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';
        $siteUrl = function_exists('home_url') ? (string) home_url('/') : '';
        $loginUrl = function_exists('wp_login_url') ? (string) wp_login_url() : 'wp-login.php';
        $title = $this->emailSubject('manual');

        $content = EmailTemplate::paragraphs([
            /* translators: Placeholder values are generated from SHIELD-X status data, file paths, counts, or action names. */
            sprintf(__('SHIELD-X login security is active on %s.', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress'),
            __('Admins will be guided to set up an authenticator app before using SHIELD-X admin screens.', 'xerotact-shield-x-security'),
            __('If authenticator access is lost, open the login page, sign in with the normal administrator password, and paste the token below into the SHIELD-X owner recovery token field.', 'xerotact-shield-x-security'),
        ]);
        $content .= EmailTemplate::details([
            __('Site', 'xerotact-shield-x-security') => $siteUrl,
            __('Login page', 'xerotact-shield-x-security') => $loginUrl,
        ]);
        $content .= EmailTemplate::codeBlock($token);
        $content .= EmailTemplate::button(__('Open WordPress login', 'xerotact-shield-x-security'), $loginUrl);
        $content .= EmailTemplate::paragraphs([
            __('This token expires in 60 minutes and can be used only from the network that requested it.', 'xerotact-shield-x-security'),
            __('Keep this token private. Sending a new recovery email from SHIELD-X rotates it.', 'xerotact-shield-x-security'),
        ]);

        return EmailTemplate::render($title, $content, __('SHIELD-X recovery access token', 'xerotact-shield-x-security'));
    }

    private function resetCodeSubject(): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        /* translators: Placeholder values are generated from SHIELD-X status data, file paths, counts, or action names. */
        return sprintf(__('SHIELD-X 2FA reset code for %s', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress');
    }

    private function resetCodeBody(int $userId, string $code): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';
        $siteUrl = function_exists('home_url') ? (string) home_url('/') : '';
        $loginSecurityUrl = function_exists('admin_url') ? (string) admin_url('admin.php?page=shield-x-login-security#shieldx-authenticator-2fa') : '';
        $title = $this->resetCodeSubject();

        $content = EmailTemplate::paragraphs([
            /* translators: Placeholder values are generated from SHIELD-X status data, file paths, counts, or action names. */
            sprintf(__('A SHIELD-X 2FA reset was requested for %s.', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress'),
            __('Enter this one-time code in SHIELD-X Login Security to disable authenticator 2FA for that account:', 'xerotact-shield-x-security'),
        ]);
        $content .= EmailTemplate::details([
            __('Site', 'xerotact-shield-x-security') => $siteUrl,
            __('Account', 'xerotact-shield-x-security') => $this->accountLabel($userId),
        ]);
        $content .= EmailTemplate::codeBlock($code);
        $content .= EmailTemplate::paragraphs([
            __('This code expires in 15 minutes. If you did not request it, change the administrator password and review SHIELD-X Activity.', 'xerotact-shield-x-security'),
        ]);
        if ($loginSecurityUrl !== '') {
            $content .= EmailTemplate::button(__('Open Login Security', 'xerotact-shield-x-security'), $loginSecurityUrl);
        }

        return EmailTemplate::render($title, $content, __('SHIELD-X 2FA reset code', 'xerotact-shield-x-security'));
    }

    private function recoveryCodesSubject(): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        /* translators: %s: site name. */
        return sprintf(__('SHIELD-X 2FA recovery codes for %s', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress');
    }

    /**
     * @param array<int,string> $codes
     */
    private function recoveryCodesEmailBody(int $userId, array $codes): string
    {
        $siteUrl = function_exists('home_url') ? (string) home_url('/') : '';
        $loginSecurityUrl = function_exists('admin_url') ? (string) admin_url('admin.php?page=shield-x-login-security#shieldx-authenticator-2fa') : '';
        $title = __('Your SHIELD-X 2FA backup codes', 'xerotact-shield-x-security');

        $content = EmailTemplate::paragraphs([
            __('Authenticator 2FA was enabled for this administrator account.', 'xerotact-shield-x-security'),
            __('Save these backup codes somewhere private. Each code can be used once if the authenticator app is unavailable.', 'xerotact-shield-x-security'),
        ]);
        $content .= EmailTemplate::details([
            __('Site', 'xerotact-shield-x-security') => $siteUrl,
            __('Account', 'xerotact-shield-x-security') => $this->accountLabel($userId),
        ]);
        $content .= '<p style="margin:16px 0 8px;color:#101935;font-size:15px;font-weight:700;">' . EmailTemplate::escape(__('Backup codes', 'xerotact-shield-x-security')) . '</p>';
        $content .= EmailTemplate::codeList($codes);
        $content .= EmailTemplate::paragraphs([
            __('If you did not enable SHIELD-X 2FA, change the administrator password immediately and review SHIELD-X Activity.', 'xerotact-shield-x-security'),
        ]);
        if ($loginSecurityUrl !== '') {
            $content .= EmailTemplate::button(__('Open Login Security', 'xerotact-shield-x-security'), $loginSecurityUrl);
        }

        return EmailTemplate::render($title, $content, __('SHIELD-X 2FA backup codes', 'xerotact-shield-x-security'));
    }

    private function codesTransientKey(int $userId, string $id): string
    {
        return 'shieldx_2fa_codes_' . (string) $userId . '_' . $id;
    }

    private function resetCodeTransientKey(int $userId): string
    {
        return 'shieldx_2fa_reset_' . (string) $userId;
    }

    private function resetCodeRequestTransientKey(int $userId): string
    {
        return 'shieldx_2fa_reset_request_' . (string) $userId;
    }

    private function twoFactorFailureTransientKey(int $userId): string
    {
        return 'shieldx_2fa_fail_' . hash('sha256', (string) $userId);
    }

    private function twoFactorUserLocked(int $userId): bool
    {
        if (!function_exists('get_transient')) {
            return false;
        }

        $record = get_transient($this->twoFactorFailureTransientKey($userId));

        return is_array($record) && (int) ($record['lock_until'] ?? 0) > time();
    }

    private function recordTwoFactorFailure(int $userId): void
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return;
        }

        $key = $this->twoFactorFailureTransientKey($userId);
        $now = time();
        $record = get_transient($key);
        $count = is_array($record) ? (int) ($record['count'] ?? 0) : 0;
        $expiresAt = is_array($record) ? (int) ($record['expires_at'] ?? 0) : 0;
        $lockUntil = is_array($record) ? (int) ($record['lock_until'] ?? 0) : 0;

        if ($expiresAt < $now && $lockUntil < $now) {
            $count = 0;
            $expiresAt = $now + self::TWO_FACTOR_FAILURE_WINDOW;
        }

        $count++;
        if ($count >= self::TWO_FACTOR_FAILURE_LIMIT) {
            $lockUntil = $now + self::TWO_FACTOR_LOCK_SECONDS;
            $this->log('login_security.2fa_lockout', [
                'user_id' => $userId,
                'limit' => self::TWO_FACTOR_FAILURE_LIMIT,
                'lock_seconds' => self::TWO_FACTOR_LOCK_SECONDS,
            ]);
        }

        set_transient($key, [
            'count' => $count,
            'expires_at' => $expiresAt,
            'lock_until' => $lockUntil,
        ], max(self::TWO_FACTOR_FAILURE_WINDOW, self::TWO_FACTOR_LOCK_SECONDS));
    }

    private function clearTwoFactorFailures(int $userId): void
    {
        if (function_exists('delete_transient')) {
            delete_transient($this->twoFactorFailureTransientKey($userId));
        }
    }

    private function resetCodeRequestThrottled(int $userId): bool
    {
        if ($userId <= 0 || !function_exists('get_transient') || !function_exists('set_transient')) {
            return false;
        }

        return $this->claimTransientThrottleWindow($this->resetCodeRequestTransientKey($userId), self::RESET_CODE_REQUEST_THROTTLE);
    }

    private function magicTokenTransientKey(string $token): string
    {
        return 'shieldx_magic_token_' . substr($this->hashMagicLoginToken($token), 0, 32);
    }

    private function magicUserTransientKey(int $userId): string
    {
        return 'shieldx_magic_user_' . (string) $userId;
    }

    private function magicSessionTransientKey(string $token): string
    {
        return 'shieldx_magic_session_' . substr($this->hashMagicLoginToken($token), 0, 32);
    }

    private function magicRequestTransientKey(int $userId): string
    {
        return 'shieldx_magic_request_' . substr(hash('sha256', (string) $userId), 0, 32);
    }

    private function magicUserBudgetTransientKey(int $userId, string $scope): string
    {
        return 'shieldx_magic_user_' . preg_replace('/[^a-z0-9_]+/i', '', $scope) . '_' . substr(hash('sha256', (string) $userId), 0, 32);
    }

    private function magicIpTransientKey(): string
    {
        return 'shieldx_magic_ip_' . substr(hash('sha256', $this->serverRemoteAddress()), 0, 32);
    }

    private function magicLoginIpThrottled(): bool
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return false;
        }

        $counter = $this->incrementTransientCounter($this->magicIpTransientKey(), self::MAGIC_LOGIN_IP_WINDOW);

        return is_array($counter) && (int) $counter['count'] > self::MAGIC_LOGIN_IP_LIMIT;
    }

    private function magicLoginNonceIsValid(): bool
    {
        if (!function_exists('wp_verify_nonce')) {
            return true;
        }

        $nonce = $this->requestPostText(self::MAGIC_LOGIN_NONCE_FIELD);

        return $nonce !== '' && wp_verify_nonce($nonce, self::MAGIC_LOGIN_NONCE_ACTION) !== false;
    }

    private function magicLoginGenericVerificationError(): string
    {
        return __('Magic Login could not be verified. Request a new email and try again.', 'xerotact-shield-x-security');
    }

    private function serverRequestMethod(): string
    {
        $method = $this->serverText('REQUEST_METHOD', 'GET');

        return strtoupper(function_exists('sanitize_key') ? sanitize_key((string) $method) : preg_replace('/[^A-Za-z]/', '', (string) $method));
    }

    private function serverRemoteAddress(): string
    {
        // Use the same proxy-aware resolver as the WAF so trusted reverse
        // proxies do not collapse every login throttle and recovery binding
        // onto one shared REMOTE_ADDR value.
        return MinimalWaf::clientIpFromServer($_SERVER, WafSettings::fromWordPress());
    }

    private function generateMagicLoginCode(): string
    {
        return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
    }

    private function normalizeMagicLoginCode(string $code): string
    {
        $code = preg_replace('/\D+/', '', trim($code)) ?: '';

        return strlen($code) === 6 ? $code : '';
    }

    private function normalizeMagicToken(string $token): string
    {
        $token = strtolower(trim($token));

        return preg_match('/^[a-f0-9]{32,128}$/', $token) === 1 ? $token : '';
    }

    private function hashMagicLoginToken(string $token): string
    {
        return hash_hmac('sha256', trim($token), $this->magicLoginSalt() . '|token');
    }

    private function hashMagicLoginCode(int $userId, string $code): string
    {
        return hash_hmac('sha256', (string) $userId . '|' . $this->normalizeMagicLoginCode($code), $this->magicLoginSalt() . '|code');
    }

    private function magicLoginSalt(): string
    {
        $auth = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $nonce = function_exists('wp_salt') ? (string) wp_salt('nonce') : '';
        if ($auth === '' || $nonce === '') {
            throw new \RuntimeException('WordPress salts are required for SHIELD-X Magic Login.');
        }

        return $auth . '|' . $nonce . '|shieldx-magic-login';
    }

    private function magicLoginRequestThrottled(int $userId): bool
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return false;
        }

        return $this->claimTransientThrottleWindow($this->magicRequestTransientKey($userId), self::MAGIC_LOGIN_REQUEST_THROTTLE);
    }

    private function magicLoginUserBudgetExceeded(int $userId, string $scope, int $limit): bool
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return false;
        }

        $counter = $this->incrementTransientCounter($this->magicUserBudgetTransientKey($userId, $scope), self::MAGIC_LOGIN_USER_DAILY_WINDOW);
        if (!is_array($counter)) {
            return false;
        }

        return (int) $counter['count'] > max(1, $limit);
    }

    private function claimTransientThrottleWindow(string $key, int $ttlSeconds): bool
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return false;
        }

        if (!$this->acquireTransientLock($key)) {
            return true;
        }

        try {
            if (get_transient($key) !== false) {
                return true;
            }

            return set_transient($key, '1', max(1, $ttlSeconds)) === false;
        } finally {
            $this->releaseTransientLock($key);
        }
    }

    /**
     * @return array{count:int,expires_at:int}|null
     */
    private function incrementTransientCounter(string $key, int $windowSeconds): ?array
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return null;
        }

        $now = time();
        if (!$this->acquireTransientLock($key)) {
            return [
                'count' => PHP_INT_MAX,
                'expires_at' => $now + max(1, $windowSeconds),
            ];
        }

        try {
            $record = get_transient($key);
            $count = is_array($record) ? (int) ($record['count'] ?? 0) : 0;
            $expiresAt = is_array($record) ? (int) ($record['expires_at'] ?? 0) : 0;

            if ($expiresAt < $now) {
                $count = 0;
                $expiresAt = $now + max(1, $windowSeconds);
            }

            $count++;
            $payload = [
                'count' => $count,
                'expires_at' => $expiresAt,
            ];

            if (set_transient($key, $payload, max(60, $expiresAt - $now)) === false) {
                $payload['count'] = PHP_INT_MAX;
            }

            return $payload;
        } finally {
            $this->releaseTransientLock($key);
        }
    }

    private function claimSingleUseToken(string $scope, string $fingerprint, int $ttlSeconds): bool
    {
        $scope = preg_replace('/[^a-z0-9_]+/', '_', strtolower(trim($scope))) ?: 'token';
        $fingerprint = strtolower(trim($fingerprint));
        if ($fingerprint === '') {
            return false;
        }

        $key = self::SINGLE_USE_CLAIM_PREFIX . $scope . '_' . substr(hash('sha256', $fingerprint), 0, 40);
        $now = time();

        if (function_exists('add_option')) {
            if (add_option($key, (string) $now, '', 'no')) {
                return true;
            }

            if ($ttlSeconds > 0 && function_exists('get_option') && function_exists('delete_option')) {
                $claimedAt = (int) get_option($key, 0);
                if ($claimedAt > 0 && $claimedAt < $now - $ttlSeconds) {
                    delete_option($key);

                    return add_option($key, (string) $now, '', 'no');
                }
            }

            return false;
        }

        if (function_exists('wp_cache_add')) {
            return (bool) wp_cache_add($key, $now, 'shieldx_login_security', max(1, $ttlSeconds));
        }

        return false;
    }

    private function acquireTransientLock(string $key): bool
    {
        if (!function_exists('add_option') || !function_exists('get_option') || !function_exists('delete_option')) {
            return false;
        }

        $shieldxLockKey = $this->transientLockKey($key);
        $lockedAt = time();
        if (add_option($shieldxLockKey, $lockedAt, '', 'no')) {
            return true;
        }

        $existing = (int) get_option($shieldxLockKey, 0);
        if ($existing <= 0 || $existing > $lockedAt - self::TRANSIENT_LOCK_SECONDS) {
            return false;
        }

        delete_option($shieldxLockKey);

        return add_option($shieldxLockKey, $lockedAt, '', 'no');
    }

    private function releaseTransientLock(string $key): void
    {
        if (function_exists('delete_option')) {
            delete_option($this->transientLockKey($key));
        }
    }

    private function transientLockKey(string $key): string
    {
        return 'shieldx_login_lock_' . substr(hash('sha256', $key), 0, 40);
    }

    /**
     * @param mixed $payload
     */
    private function isValidMagicLoginPayload($payload): bool
    {
        return is_array($payload)
            && (int) ($payload['user_id'] ?? 0) > 0
            && (string) ($payload['token_hash'] ?? '') !== ''
            && (string) ($payload['code_hash'] ?? '') !== ''
            && (string) ($payload['token_key'] ?? '') !== ''
            && (string) ($payload['user_key'] ?? '') !== ''
            && (int) ($payload['expires_at'] ?? 0) >= time();
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function deleteMagicLoginPayload(array $payload): void
    {
        if (!function_exists('delete_transient')) {
            return;
        }

        foreach (['token_key', 'user_key'] as $key) {
            $transient = (string) ($payload[$key] ?? '');
            if ($transient !== '') {
                delete_transient($transient);
            }
        }
    }

    /**
     * @param array<string,mixed> $payload
     */
    private function updateMagicLoginPayload(array $payload): void
    {
        if (!function_exists('set_transient')) {
            return;
        }

        $ttl = max(60, (int) ($payload['expires_at'] ?? time()) - time());
        foreach (['token_key', 'user_key'] as $key) {
            $transient = (string) ($payload[$key] ?? '');
            if ($transient !== '') {
                set_transient($transient, $payload, $ttl);
            }
        }
    }

    /**
     * @param array<string,mixed> $payload
     * @return array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string}
     */
    private function recordMagicLoginFailure(array $payload, string $message): array
    {
        $userId = (int) ($payload['user_id'] ?? 0);
        if ($userId > 0 && $this->magicLoginUserBudgetExceeded($userId, 'failure', self::MAGIC_LOGIN_USER_DAILY_FAILURE_LIMIT)) {
            $this->deleteMagicLoginPayload($payload);

            return $this->magicLoginResult(false, $message, $userId);
        }

        $attempts = (int) ($payload['attempts'] ?? 0) + 1;
        $payload['attempts'] = $attempts;

        if ($attempts >= self::MAGIC_LOGIN_MAX_ATTEMPTS) {
            $this->deleteMagicLoginPayload($payload);
        } else {
            $this->updateMagicLoginPayload($payload);
        }

        return $this->magicLoginResult(false, $message, $userId);
    }

    /**
     * @return array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string}
     */
    private function prepareMagicLoginResult(int $userId, string $method): array
    {
        $user = function_exists('get_userdata') ? get_userdata($userId) : null;
        if (!$this->isSubjectUserObject($user)) {
            return $this->magicLoginResult(false, __('The Magic Login link is invalid or expired.', 'xerotact-shield-x-security'));
        }

        if ($this->isTwoFactorEnabled($userId)) {
            $session = $this->createMagicLoginSession($userId, $method);

            return $this->magicLoginResult(true, '', $userId, true, $session, $method);
        }

        $this->log('login_security.magic_login', ['user_id' => $userId, 'method' => $method]);

        return $this->magicLoginResult(true, '', $userId, false, '', $method);
    }

    private function createMagicLoginSession(int $userId, string $method): string
    {
        $token = TwoFactor::randomToken(32);
        if (function_exists('set_transient')) {
            set_transient($this->magicSessionTransientKey($token), [
                'user_id' => $userId,
                'method' => $method,
                'created_at' => time(),
                'expires_at' => time() + self::MAGIC_LOGIN_TTL,
                'attempts' => 0,
            ], self::MAGIC_LOGIN_TTL);
        }

        return $token;
    }

    /**
     * @return array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string}
     */
    private function magicLoginResult(bool $ok, string $error = '', int $userId = 0, bool $requiresTwoFactor = false, string $session = '', string $method = ''): array
    {
        return [
            'ok' => $ok,
            'error' => $error,
            'user_id' => $userId,
            'requires_2fa' => $requiresTwoFactor,
            'session' => $session,
            'method' => $method,
        ];
    }

    /**
     * @param array{ok:bool,error:string,user_id:int,requires_2fa:bool,session:string,method:string} $result
     */
    private function continueMagicLogin(array $result, string $identifier = ''): void
    {
        if (!empty($result['ok']) && !empty($result['requires_2fa'])) {
            $this->renderMagicLoginChallengePage((string) ($result['session'] ?? ''));
            return;
        }

        if (!empty($result['ok']) && (int) ($result['user_id'] ?? 0) > 0) {
            $this->signInMagicLoginUser((int) $result['user_id']);
            return;
        }

        if (!empty($result['requires_2fa']) && (string) ($result['session'] ?? '') !== '') {
            $this->renderMagicLoginChallengePage((string) $result['session'], (string) ($result['error'] ?? ''));
            return;
        }

        $this->renderMagicLoginPage('', (string) ($result['error'] ?? __('Magic Login could not be verified.', 'xerotact-shield-x-security')), $identifier);
    }

    private function signInMagicLoginUser(int $userId): void
    {
        $user = function_exists('get_userdata') ? get_userdata($userId) : null;
        if (!$this->isSubjectUserObject($user) || !function_exists('wp_set_auth_cookie') || !function_exists('wp_safe_redirect')) {
            $this->renderMagicLoginPage('', __('WordPress could not complete Magic Login for this account.', 'xerotact-shield-x-security'));
            return;
        }

        if (function_exists('wp_set_current_user')) {
            wp_set_current_user($userId);
        }

        wp_set_auth_cookie($userId, false, function_exists('is_ssl') && is_ssl());

        if (function_exists('do_action')) {
            try {
                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Magic Login completes a normal WordPress login and must fire the core wp_login hook for compatibility.
                do_action('wp_login', $this->userObjectValue($user, 'user_login', 'user-' . (string) $userId), $user);
            } catch (\Throwable $error) {
                $this->log('login_security.wp_login_listener_failed', [
                    'user_id' => $userId,
                    'error' => $error->getMessage(),
                ]);
            }
        }

        wp_safe_redirect(function_exists('admin_url') ? admin_url() : 'wp-admin/');
        exit;
    }

    /**
     * @param array<string,string> $args
     */
    private function magicLoginUrl(array $args = []): string
    {
        $base = function_exists('wp_login_url') ? (string) wp_login_url() : 'wp-login.php';
        $args = array_merge(['action' => 'shieldx_magic_login'], $args);

        if (function_exists('add_query_arg')) {
            return add_query_arg($args, $base);
        }

        $separator = strpos($base, '?') === false ? '?' : '&';

        return $base . $separator . http_build_query($args, '', '&', PHP_QUERY_RFC3986);
    }

    private function magicLoginSubject(): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        /* translators: %s: site name. */
        return sprintf(__('SHIELD-X Magic Login for %s', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress');
    }

    private function twoFactorSetupSubject(): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        /* translators: %s: site name. */
        return sprintf(__('Set up SHIELD-X 2FA for %s', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress');
    }

    private function twoFactorSetupEmailBody(int $userId): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';
        $siteUrl = function_exists('home_url') ? (string) home_url('/') : '';
        $setupUrl = function_exists('admin_url') ? (string) admin_url('profile.php#shieldx-profile-two-factor') : 'wp-admin/profile.php#shieldx-profile-two-factor';
        $account = $this->accountLabel($userId);
        $title = __('Set up SHIELD-X authenticator 2FA', 'xerotact-shield-x-security');

        $content = EmailTemplate::paragraphs([
            /* translators: %s: site name. */
            sprintf(__('Required SHIELD-X authenticator 2FA is enabled for your role on %s.', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress'),
            __('Sign in to WordPress and open your authenticated profile to create the authenticator secret. The setup key is never sent by email.', 'xerotact-shield-x-security'),
        ]);
        $content .= EmailTemplate::details([
            __('Site', 'xerotact-shield-x-security') => $siteUrl,
            __('Account', 'xerotact-shield-x-security') => $account,
        ]);
        $content .= '<ol style="margin:16px 0 18px;padding-left:22px;color:#4b5568;font-size:15px;">'
            . '<li style="margin:0 0 8px;">' . EmailTemplate::escape(__('Open SHIELD-X Login Security from your signed-in WordPress admin session.', 'xerotact-shield-x-security')) . '</li>'
            . '<li style="margin:0 0 8px;">' . EmailTemplate::escape(__('Scan the setup QR shown in the admin screen, or copy the manual key shown there.', 'xerotact-shield-x-security')) . '</li>'
            . '<li style="margin:0 0 8px;">' . EmailTemplate::escape(__('Enter the 6-digit code from the app, then save the recovery codes shown after verification.', 'xerotact-shield-x-security')) . '</li>'
            . '</ol>';
        $content .= EmailTemplate::button(__('Open SHIELD-X 2FA setup', 'xerotact-shield-x-security'), $setupUrl);
        $content .= EmailTemplate::paragraphs([
            __('This email intentionally does not contain the setup QR, authenticator URI, or manual TOTP key.', 'xerotact-shield-x-security'),
        ]);

        return EmailTemplate::render($title, $content, __('SHIELD-X authenticator setup notice', 'xerotact-shield-x-security'));
    }

    private function magicLoginEmailBody(int $userId, string $link, string $code): string
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';
        $siteUrl = function_exists('home_url') ? (string) home_url('/') : '';
        $account = $this->accountLabel($userId);
        $expires = gmdate('Y-m-d H:i:s', time() + self::MAGIC_LOGIN_TTL) . ' UTC';
        $qr = QrCode::svg($link, 180);
        $title = __('SHIELD-X Magic Login', 'xerotact-shield-x-security');

        $content = EmailTemplate::paragraphs([
            /* translators: %s: site name. */
            sprintf(__('A passwordless administrator login was requested for %s.', 'xerotact-shield-x-security'), $siteName !== '' ? $siteName : 'WordPress'),
        ]);
        $content .= EmailTemplate::details([
            __('Site', 'xerotact-shield-x-security') => $siteUrl,
            __('Account', 'xerotact-shield-x-security') => $account,
        ]);
        $content .= EmailTemplate::button(__('Log in with Magic Login', 'xerotact-shield-x-security'), $link);
        $content .= EmailTemplate::paragraphs([
            __('You can also scan this QR code to open the same one-time login link:', 'xerotact-shield-x-security'),
        ]);
        $content .= '<div style="margin:16px 0;padding:14px;background:#ffffff;border:1px solid #dce6f5;border-radius:8px;display:inline-block;">' . $qr . '</div>';
        $content .= EmailTemplate::paragraphs([
            __('Or enter this 6-digit code on the Magic Login screen:', 'xerotact-shield-x-security'),
        ]);
        $content .= EmailTemplate::codeBlock($code);
        $content .= EmailTemplate::paragraphs([
            /* translators: %s: expiration time in UTC. */
            sprintf(__('This link and code expire at %s and can be used only once.', 'xerotact-shield-x-security'), $expires),
            __('If you did not request this email, change the administrator password and review SHIELD-X Activity.', 'xerotact-shield-x-security'),
        ]);

        return EmailTemplate::render($title, $content, __('SHIELD-X Magic Login request', 'xerotact-shield-x-security'));
    }

    private function renderMagicLoginPage(string $message = '', string $error = '', string $identifier = ''): void
    {
        if (function_exists('login_header')) {
            login_header(__('SHIELD-X Magic Login', 'xerotact-shield-x-security'), '', null);
        } else {
            echo '<!doctype html><html><body>';
        }

        $action = $this->magicLoginUrl();
        ?>
        <div class="shieldx-magic-login-page">
            <h2><?php echo esc_html__('Magic Login ★', 'xerotact-shield-x-security'); ?></h2>
            <p><?php echo esc_html__('Passwordless login via secure email links or 6-digit codes with QR support.', 'xerotact-shield-x-security'); ?></p>
            <?php if ($message !== '') : ?>
                <div class="message"><p><?php echo esc_html($message); ?></p></div>
            <?php endif; ?>
            <?php if ($error !== '') : ?>
                <div id="login_error"><p><?php echo esc_html($error); ?></p></div>
            <?php endif; ?>
            <form method="post" action="<?php echo esc_url($action); ?>">
                <?php $this->renderMagicLoginNonceField(); ?>
                <p>
                    <label for="shieldx_magic_identifier"><?php echo esc_html__('Administrator username or email', 'xerotact-shield-x-security'); ?><br>
                        <input type="text" name="shieldx_magic_identifier" id="shieldx_magic_identifier" class="input" value="<?php echo esc_attr($identifier); ?>" size="20" autocomplete="username" required>
                    </label>
                </p>
                <p class="submit">
                    <button type="submit" class="button button-primary button-large"><?php echo esc_html__('Email magic link and code', 'xerotact-shield-x-security'); ?></button>
                </p>
            </form>
            <hr>
            <form method="post" action="<?php echo esc_url($action); ?>">
                <?php $this->renderMagicLoginNonceField(); ?>
                <p>
                    <label for="shieldx_magic_identifier_code"><?php echo esc_html__('Administrator username or email', 'xerotact-shield-x-security'); ?><br>
                        <input type="text" name="shieldx_magic_identifier" id="shieldx_magic_identifier_code" class="input" value="<?php echo esc_attr($identifier); ?>" size="20" autocomplete="username">
                    </label>
                </p>
                <p>
                    <label for="shieldx_magic_code"><?php echo esc_html__('6-digit Magic Login code', 'xerotact-shield-x-security'); ?><br>
                        <input type="text" name="shieldx_magic_code" id="shieldx_magic_code" class="input" value="" size="20" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9 ]{6,10}">
                    </label>
                </p>
                <p class="submit">
                    <button type="submit" class="button button-large"><?php echo esc_html__('Verify code', 'xerotact-shield-x-security'); ?></button>
                </p>
            </form>
            <p id="backtoblog"><a href="<?php echo esc_url(function_exists('wp_login_url') ? wp_login_url() : 'wp-login.php'); ?>"><?php echo esc_html__('Use password login', 'xerotact-shield-x-security'); ?></a></p>
        </div>
        <?php
        if (function_exists('login_footer')) {
            login_footer();
        } else {
            echo '</body></html>';
        }
    }

    private function renderMagicLoginConfirmationPage(string $token): void
    {
        $token = $this->normalizeMagicToken($token);
        if (function_exists('login_header')) {
            login_header(__('Confirm SHIELD-X Magic Login', 'xerotact-shield-x-security'), '', null);
        } else {
            echo '<!doctype html><html><body>';
        }

        ?>
        <div class="shieldx-magic-login-page">
            <h2><?php echo esc_html__('Confirm Magic Login', 'xerotact-shield-x-security'); ?></h2>
            <p><?php echo esc_html__('For safety, SHIELD-X does not consume email login links during a GET request. Confirm from this browser to continue.', 'xerotact-shield-x-security'); ?></p>
            <?php if ($token === '') : ?>
                <div id="login_error"><p><?php echo esc_html__('The Magic Login link is invalid or expired.', 'xerotact-shield-x-security'); ?></p></div>
            <?php else : ?>
                <form method="post" action="<?php echo esc_url($this->magicLoginUrl()); ?>">
                    <?php $this->renderMagicLoginNonceField(); ?>
                    <input type="hidden" name="shieldx_magic_token" value="<?php echo esc_attr($token); ?>">
                    <p class="submit">
                        <button type="submit" class="button button-primary button-large"><?php echo esc_html__('Continue Magic Login', 'xerotact-shield-x-security'); ?></button>
                    </p>
                </form>
            <?php endif; ?>
            <p id="backtoblog"><a href="<?php echo esc_url(function_exists('wp_login_url') ? wp_login_url() : 'wp-login.php'); ?>"><?php echo esc_html__('Use password login', 'xerotact-shield-x-security'); ?></a></p>
        </div>
        <?php
        if (function_exists('login_footer')) {
            login_footer();
        } else {
            echo '</body></html>';
        }
    }

    private function renderMagicLoginChallengePage(string $session, string $error = ''): void
    {
        if (function_exists('login_header')) {
            login_header(__('SHIELD-X Magic Login 2FA', 'xerotact-shield-x-security'), '', null);
        } else {
            echo '<!doctype html><html><body>';
        }

        ?>
        <div class="shieldx-magic-login-page">
            <h2><?php echo esc_html__('Verify SHIELD-X 2FA', 'xerotact-shield-x-security'); ?></h2>
            <p><?php echo esc_html__('This administrator account has authenticator 2FA enabled. Enter the current authenticator or recovery code to finish Magic Login.', 'xerotact-shield-x-security'); ?></p>
            <?php if ($error !== '') : ?>
                <div id="login_error"><p><?php echo esc_html($error); ?></p></div>
            <?php endif; ?>
            <form method="post" action="<?php echo esc_url($this->magicLoginUrl()); ?>">
                <?php $this->renderMagicLoginNonceField(); ?>
                <input type="hidden" name="shieldx_magic_session" value="<?php echo esc_attr($session); ?>">
                <p>
                    <label for="shieldx_magic_totp_code"><?php echo esc_html__('SHIELD-X authenticator or recovery code', 'xerotact-shield-x-security'); ?><br>
                        <input type="text" name="shieldx_totp_code" id="shieldx_magic_totp_code" class="input" value="" size="20" autocomplete="one-time-code" required>
                    </label>
                </p>
                <p class="submit">
                    <button type="submit" class="button button-primary button-large"><?php echo esc_html__('Complete Magic Login', 'xerotact-shield-x-security'); ?></button>
                </p>
            </form>
        </div>
        <?php
        if (function_exists('login_footer')) {
            login_footer();
        } else {
            echo '</body></html>';
        }
    }

    private function renderMagicLoginNonceField(): void
    {
        if (function_exists('wp_nonce_field')) {
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_nonce_field() returns WordPress-generated hidden nonce markup.
            echo wp_nonce_field(self::MAGIC_LOGIN_NONCE_ACTION, self::MAGIC_LOGIN_NONCE_FIELD, true, false);
        }
    }

    private function sanitizeSetupEmail(string $email): string
    {
        $email = trim($email);
        if (function_exists('sanitize_email')) {
            $email = sanitize_email($email);
        }

        return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : '';
    }

    private function generateResetCode(): string
    {
        return strtoupper(substr(TwoFactor::randomToken(4), 0, 8));
    }

    private function normalizeResetCode(string $code): string
    {
        return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', trim($code)) ?: '');
    }

    private function hashResetCode(string $code): string
    {
        $salt = function_exists('wp_salt') ? (string) wp_salt('nonce') : '';
        if ($salt === '') {
            throw new \RuntimeException('WordPress nonce salts are required for SHIELD-X reset-code hashes.');
        }

        return hash_hmac('sha256', $this->normalizeResetCode($code), $salt);
    }

    private function secretForUser(int $userId, string $metaKey): string
    {
        $stored = (string) get_user_meta($userId, $metaKey, true);
        if ($stored === '') {
            return '';
        }

        $secret = $this->revealSecret($stored);
        if ($secret === '') {
            return '';
        }

        return $secret;
    }

    private function canVerifyTwoFactorForUser(int $userId): bool
    {
        return !empty(self::secretStorageStatus()['ready'])
            && $this->secretForUser($userId, self::META_SECRET) !== '';
    }

    private function protectSecret(string $secret): string
    {
        $secret = TwoFactor::normalizeSecret($secret);
        if ($secret === '') {
            throw new \RuntimeException('OpenSSL encryption is required for SHIELD-X 2FA secret storage.');
        }

        $protected = CryptoBox::protect($secret, $this->secretEncryptionKey());
        if ($protected === '') {
            throw new \RuntimeException('Could not encrypt the SHIELD-X 2FA secret.');
        }

        return $protected;
    }

    private function revealSecret(string $stored): string
    {
        $stored = trim($stored);
        if (!CryptoBox::isV2($stored)) {
            return '';
        }

        return TwoFactor::normalizeSecret(CryptoBox::reveal($stored, $this->secretEncryptionKey()));
    }

    private function secretEncryptionKey(): string
    {
        return hash('sha256', $this->secretMacKey(), true);
    }

    private function secretMacKey(): string
    {
        $auth = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $secure = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        if ($auth === '' || $secure === '') {
            throw new \RuntimeException('WordPress authentication salts are required for SHIELD-X 2FA secret encryption.');
        }

        return $auth . '|' . $secure . '|shieldx-2fa-secret';
    }

    private function requestUsesBasicAuth(): bool
    {
        if ($this->serverText('PHP_AUTH_USER') !== '') {
            return true;
        }

        foreach (['HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION'] as $key) {
            $value = $this->serverText($key);
            if (stripos(trim($value), 'basic ') === 0) {
                return true;
            }
        }

        return false;
    }

    private function serverText(string $key, string $default = ''): string
    {
        if (!isset($_SERVER[$key]) || !is_scalar($_SERVER[$key])) {
            return $default;
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Dynamic server value is unslashed here and sanitized before return.
        $value = function_exists('wp_unslash') ? wp_unslash($_SERVER[$key]) : $_SERVER[$key];
        $value = is_scalar($value) ? (string) $value : $default;

        return trim($this->sanitizeText($value));
    }

    private function requestText(string $key): string
    {
        $value = '';

        // phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Dynamic request values are sanitized here; callers enforce admin nonces or signed-token controls before state changes.
        if (isset($_POST[$key]) && is_scalar($_POST[$key])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Dynamic posted value is unslashed here and sanitized before return; callers enforce nonce or signed-token controls before state changes.
            $value = (string) (function_exists('wp_unslash') ? wp_unslash($_POST[$key]) : $_POST[$key]);
        } elseif (isset($_GET[$key]) && is_scalar($_GET[$key])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Dynamic query value is unslashed here and sanitized before return; query reads are non-mutating or signed-token checks.
            $value = (string) (function_exists('wp_unslash') ? wp_unslash($_GET[$key]) : $_GET[$key]);
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended

        return trim($this->sanitizeText($value));
    }

    private function requestPostText(string $key): string
    {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Dynamic posted values are sanitized here; callers enforce admin nonces or signed-token controls before state changes.
        if (!isset($_POST[$key]) || !is_scalar($_POST[$key])) {
            return '';
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Dynamic posted value is unslashed here and sanitized before return; callers enforce nonce or signed-token controls before state changes.
        $value = (string) (function_exists('wp_unslash') ? wp_unslash($_POST[$key]) : $_POST[$key]);
        // phpcs:enable WordPress.Security.NonceVerification.Missing

        return trim($this->sanitizeText($value));
    }

    private function sanitizeText(string $value): string
    {
        if (function_exists('sanitize_text_field')) {
            return sanitize_text_field($value);
        }

        if (function_exists('wp_strip_all_tags')) {
            return trim(wp_strip_all_tags($value));
        }

        return trim((string) preg_replace('/<[^>]*>/', '', $value));
    }

    private function sanitizeKey(string $value): string
    {
        return function_exists('sanitize_key') ? sanitize_key($value) : strtolower((string) preg_replace('/[^a-z0-9_-]+/', '', $value));
    }

    /**
     * @param array<string,mixed> $context
     */
    private function log(string $event, array $context): void
    {
        if ($this->logger) {
            $this->logger->info($event, $context);
        }
    }

    /**
     * @param array<int,string> $headers
     */
    private function safeWpMail(string $recipient, string $subject, string $message, array $headers, string &$error): bool
    {
        $error = '';
        try {
            return (bool) wp_mail($recipient, $subject, $message, $headers);
        } catch (\Throwable $throwable) {
            $error = sprintf(
                /* translators: %s is an exception message from WordPress mail delivery. */
                __('wp_mail failed before sending: %s', 'xerotact-shield-x-security'),
                $throwable->getMessage()
            );
            \ShieldX\Support\ErrorLog::write('SHIELD-X login security mail failed: ' . $throwable->getMessage());

            return false;
        }
    }

    private function now(): string
    {
        if (function_exists('current_time')) {
            $timestamp = strtotime((string) current_time('c', true));
            if ($timestamp !== false) {
                return gmdate('c', $timestamp);
            }
        }

        return gmdate('c');
    }
}
