<?php

namespace VulnTitan\MuFirewall;

use WP_Error;
use VulnTitan\Services\FirewallService;

require_once dirname(__DIR__) . '/Services/FirewallService.php';

class Runtime
{
    protected const FAILED_KEY_PREFIX = 'vulntitan_fw_failed_';
    protected const LOCK_KEY_PREFIX = 'vulntitan_fw_lock_';
    protected const XMLRPC_RATE_KEY_PREFIX = 'vulntitan_fw_xmlrpc_';
    protected const MAX_INSPECTION_TOKENS = 120;
    protected const MAX_TOKEN_LENGTH = 320;
    protected const MAX_NESTED_DEPTH = 3;
    protected const SQLI_SCORE_THRESHOLD = 4;
    protected const CMDI_SCORE_THRESHOLD = 4;
    protected const INSPECTION_EXCLUDED_KEYS = [
        '_wpnonce',
        '_ajax_nonce',
        '_wp_http_referer',
        'content',
        'post_content',
        'excerpt',
        'description',
        'comment',
    ];
    protected static bool $booted = false;

    public static function boot(): void
    {
        if (self::$booted) {
            return;
        }

        self::$booted = true;

        add_action('init', [__CLASS__, 'maybeBlockSuspiciousRequest'], 1);
        // Run late so lockout message is not overwritten by core auth errors.
        add_filter('authenticate', [__CLASS__, 'maybeBlockLockedLogin'], 99, 3);
        add_action('wp_login_failed', [__CLASS__, 'handleLoginFailed'], 10, 2);
        add_action('wp_login', [__CLASS__, 'handleLoginSuccess'], 10, 2);
    }

    public static function maybeBlockSuspiciousRequest(): void
    {
        if (!self::isFirewallEnabled()) {
            return;
        }

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

        $requestUri = (string)($_SERVER['REQUEST_URI'] ?? '');
        if ($requestUri === '') {
            return;
        }

        $decoded = rawurldecode($requestUri);
        $path = (string)(parse_url($decoded, PHP_URL_PATH) ?: '');
        $normalizedPath = strtolower($path);
        $requestMethod = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
        $ipAddress = FirewallService::detectClientIp();

        if (self::maybeBlockXmlrpcRequest($normalizedPath, $requestUri, $requestMethod, $ipAddress)) {
            return;
        }

        $isTraversal = strpos($decoded, '../') !== false
            || strpos($decoded, '..\\') !== false
            || strpos($requestUri, '%00') !== false
            || strpos($decoded, "\0") !== false;

        $isSensitiveFileProbe = (bool) preg_match('#/(?:wp-config\.php|\.env|composer\.(?:json|lock))$#', $normalizedPath);

        if ($isTraversal || $isSensitiveFileProbe) {
            $reason = $isTraversal
                ? 'Blocked suspicious path traversal pattern.'
                : 'Blocked direct access probe to sensitive file.';
            $ruleId = $isTraversal ? 'path_traversal' : 'sensitive_file_probe';
            $matchedPattern = $isTraversal ? '../ or null-byte sequence' : 'direct sensitive file access';

            self::blockRequest([
                'reason' => $reason,
                'rule_group' => 'request_guard',
                'rule_id' => $ruleId,
                'matched_pattern' => $matchedPattern,
                'details' => [
                    'request_method' => $requestMethod,
                    'normalized_path' => $normalizedPath,
                    'traversal' => $isTraversal ? 1 : 0,
                    'sensitive_probe' => $isSensitiveFileProbe ? 1 : 0,
                ],
            ], $requestUri, $normalizedPath);
        }

        if (!self::isWafRuleSetEnabled()) {
            return;
        }

        if (self::isRequestWhitelisted($requestUri, $normalizedPath)) {
            return;
        }

        $inspectionTokens = self::collectInspectionTokens($requestUri, $normalizedPath);
        if (!$inspectionTokens) {
            return;
        }

        $detection = null;
        if (self::isWafSqliEnabled()) {
            $detection = self::detectSqlInjectionPayload($inspectionTokens);
        }

        if ($detection === null && self::isWafCommandInjectionEnabled()) {
            $detection = self::detectCommandInjectionPayload($inspectionTokens);
        }

        if ($detection === null) {
            return;
        }

        self::blockRequest([
            'reason' => (string)($detection['reason'] ?? 'Blocked suspicious request payload.'),
            'rule_group' => (string)($detection['rule_group'] ?? 'waf'),
            'rule_id' => (string)($detection['rule_id'] ?? 'waf_payload_rule'),
            'matched_pattern' => (string)($detection['matched_pattern'] ?? ''),
            'details' => [
                'request_method' => $requestMethod,
                'normalized_path' => $normalizedPath,
                'score' => (int)($detection['score'] ?? 0),
                'matched_rule_ids' => $detection['matched_rule_ids'] ?? [],
                'sample' => (string)($detection['sample'] ?? ''),
                'token_count' => count($inspectionTokens),
            ],
            'context' => [
                'inspection_tokens' => array_slice($inspectionTokens, 0, 20),
            ],
        ], $requestUri, $normalizedPath);
    }

    public static function maybeBlockLockedLogin($user, $username, $password)
    {
        if (!self::isLoginProtectionEnabled()) {
            return $user;
        }

        $ipAddress = FirewallService::detectClientIp();
        if ($ipAddress === '') {
            return $user;
        }

        if (FirewallService::isLockoutIpAllowlisted($ipAddress)) {
            return $user;
        }

        $lockData = self::getLockData($ipAddress);
        if (!$lockData['locked']) {
            return $user;
        }

        $remainingSeconds = max(1, (int)$lockData['remaining_seconds']);
        $remainingMinutes = (int) ceil($remainingSeconds / MINUTE_IN_SECONDS);
        $usernameSafe = is_string($username) ? sanitize_user($username, true) : '';

        FirewallService::logEvent('login_blocked', [
            'ip_address' => $ipAddress,
            'username' => $usernameSafe,
            'reason' => 'Login attempt blocked due to active lockout.',
            'details' => [
                'remaining_seconds' => $remainingSeconds,
            ],
        ]);

        return new WP_Error(
            'vulntitan_firewall_locked',
            sprintf(
                __('Too many failed login attempts. Try again in %d minute(s).', 'vulntitan'),
                max(1, $remainingMinutes)
            )
        );
    }

    public static function handleLoginFailed(string $username, $error = null): void
    {
        if (!self::isLoginProtectionEnabled()) {
            return;
        }

        $ipAddress = FirewallService::detectClientIp();
        if ($ipAddress === '') {
            return;
        }

        if (FirewallService::isLockoutIpAllowlisted($ipAddress)) {
            return;
        }

        // If IP is already locked, don't increase counters or re-trigger lockout.
        $lockData = self::getLockData($ipAddress);
        if (!empty($lockData['locked'])) {
            return;
        }

        // Ignore failures that are already our own lockout response.
        if ($error instanceof WP_Error) {
            $codes = $error->get_error_codes();
            if (in_array('vulntitan_firewall_locked', $codes, true)) {
                return;
            }
        }

        $settings = FirewallService::getSettings();
        $maxAttempts = max(2, (int)($settings['max_attempts'] ?? 5));
        $lockoutMinutes = max(5, (int)($settings['lockout_minutes'] ?? 15));
        $attemptWindow = $lockoutMinutes * MINUTE_IN_SECONDS;

        $failedKey = self::failedAttemptsKey($ipAddress);
        $failedAttempts = (int)get_transient($failedKey);
        $failedAttempts++;

        set_transient($failedKey, $failedAttempts, $attemptWindow);

        FirewallService::logEvent('login_failed', [
            'ip_address' => $ipAddress,
            'username' => sanitize_user($username, true),
            'reason' => 'Invalid login credentials.',
            'details' => [
                'attempt' => $failedAttempts,
                'max_attempts' => $maxAttempts,
            ],
        ]);

        if ($failedAttempts < $maxAttempts) {
            return;
        }

        $lockUntil = time() + ($lockoutMinutes * MINUTE_IN_SECONDS);
        set_transient(
            self::lockKey($ipAddress),
            [
                'until' => $lockUntil,
                'attempts' => $failedAttempts,
            ],
            $lockoutMinutes * MINUTE_IN_SECONDS
        );
        delete_transient($failedKey);

        FirewallService::logEvent('login_lockout', [
            'ip_address' => $ipAddress,
            'username' => sanitize_user($username, true),
            'reason' => 'IP locked due to repeated failed logins.',
            'details' => [
                'attempts' => $failedAttempts,
                'lockout_minutes' => $lockoutMinutes,
                'lock_until' => gmdate('c', $lockUntil),
            ],
        ]);
    }

    public static function handleLoginSuccess(string $userLogin): void
    {
        if (!self::isLoginProtectionEnabled()) {
            return;
        }

        $ipAddress = FirewallService::detectClientIp();
        if ($ipAddress === '') {
            return;
        }

        $failedKey = self::failedAttemptsKey($ipAddress);
        $lockKey = self::lockKey($ipAddress);

        $failedAttempts = (int)get_transient($failedKey);
        $hadLock = get_transient($lockKey);

        delete_transient($failedKey);
        delete_transient($lockKey);

        if ($failedAttempts > 0 || !empty($hadLock)) {
            FirewallService::logEvent('login_recovered', [
                'ip_address' => $ipAddress,
                'username' => sanitize_user($userLogin, true),
                'reason' => 'Successful login after previous failed attempts.',
                'details' => [
                    'cleared_failed_attempts' => $failedAttempts,
                ],
            ]);
        }
    }

    protected static function isFirewallEnabled(): bool
    {
        $settings = FirewallService::getSettings();

        return !empty($settings['enabled']);
    }

    protected static function isWafRuleSetEnabled(): bool
    {
        $settings = FirewallService::getSettings();

        if (empty($settings['enabled'])) {
            return false;
        }

        return !empty($settings['waf_sqli_enabled']) || !empty($settings['waf_command_injection_enabled']);
    }

    protected static function isWafSqliEnabled(): bool
    {
        $settings = FirewallService::getSettings();

        return !empty($settings['enabled']) && !empty($settings['waf_sqli_enabled']);
    }

    protected static function isWafCommandInjectionEnabled(): bool
    {
        $settings = FirewallService::getSettings();

        return !empty($settings['enabled']) && !empty($settings['waf_command_injection_enabled']);
    }

    protected static function maybeBlockXmlrpcRequest(string $normalizedPath, string $requestUri, string $requestMethod, string $ipAddress): bool
    {
        if ($normalizedPath !== '/xmlrpc.php' && strpos($normalizedPath, '/xmlrpc.php') !== 0) {
            return false;
        }

        $policy = FirewallService::getXmlrpcPolicy();
        if ($policy === 'allow') {
            return false;
        }

        if ($ipAddress !== '' && FirewallService::isXmlrpcIpAllowlisted($ipAddress)) {
            return false;
        }

        if ($policy === 'disable') {
            self::blockRequest([
                'reason' => 'Blocked XML-RPC request by policy.',
                'rule_group' => 'xmlrpc_policy',
                'rule_id' => 'xmlrpc_disabled',
                'matched_pattern' => 'xmlrpc.php',
                'details' => [
                    'request_method' => $requestMethod,
                    'policy' => $policy,
                ],
            ], $requestUri, $normalizedPath);

            return true;
        }

        $settings = FirewallService::getSettings();
        $maxAttempts = max(1, (int) ($settings['xmlrpc_rate_limit_attempts'] ?? 20));
        $windowMinutes = max(1, (int) ($settings['xmlrpc_rate_limit_window_minutes'] ?? 10));
        $rateKey = self::xmlrpcRateKey($ipAddress);
        $count = (int) get_transient($rateKey);
        $count++;
        set_transient($rateKey, $count, $windowMinutes * MINUTE_IN_SECONDS);

        if ($count <= $maxAttempts) {
            return false;
        }

        self::blockRequest([
            'reason' => 'XML-RPC rate limit exceeded.',
            'rule_group' => 'xmlrpc_policy',
            'rule_id' => 'xmlrpc_rate_limit',
            'matched_pattern' => 'xmlrpc.php',
            'response_code' => 429,
            'details' => [
                'request_method' => $requestMethod,
                'policy' => $policy,
                'attempts' => $count,
                'max_attempts' => $maxAttempts,
                'window_minutes' => $windowMinutes,
            ],
        ], $requestUri, $normalizedPath);

        return true;
    }

    protected static function isRequestWhitelisted(string $requestUri, string $requestPath): bool
    {
        $settings = FirewallService::getSettings();
        $whitelist = $settings['waf_whitelist_paths'] ?? [];

        if (!is_array($whitelist) || !$whitelist) {
            return false;
        }

        $uriCandidates = [
            strtolower($requestUri),
            strtolower(rawurldecode($requestUri)),
            strtolower($requestPath),
        ];

        foreach ($whitelist as $pattern) {
            if (!is_scalar($pattern)) {
                continue;
            }

            $normalizedPattern = strtolower(trim((string) $pattern));
            if ($normalizedPattern === '') {
                continue;
            }

            foreach ($uriCandidates as $candidate) {
                if (self::matchesWhitelistPattern($candidate, $normalizedPattern)) {
                    return true;
                }
            }
        }

        return false;
    }

    protected static function matchesWhitelistPattern(string $value, string $pattern): bool
    {
        $value = trim($value);
        $pattern = trim($pattern);

        if ($value === '' || $pattern === '') {
            return false;
        }

        if (strpos($pattern, '*') !== false) {
            $quoted = preg_quote($pattern, '/');
            $regex = '/^' . str_replace('\*', '.*', $quoted) . '$/i';
            return (bool) preg_match($regex, $value);
        }

        return $value === $pattern || strpos($value, $pattern) !== false;
    }

    protected static function collectInspectionTokens(string $requestUri, string $requestPath): array
    {
        $tokens = [];

        self::appendInspectionToken($tokens, 'uri', $requestUri);
        self::appendInspectionToken($tokens, 'decoded_uri', rawurldecode($requestUri));
        self::appendInspectionToken($tokens, 'path', $requestPath);
        self::appendInspectionToken($tokens, 'referer', (string)($_SERVER['HTTP_REFERER'] ?? ''));
        self::appendInspectionToken($tokens, 'user_agent', (string)($_SERVER['HTTP_USER_AGENT'] ?? ''));

        self::collectTokensFromInput($tokens, $_GET ?? [], 'get');
        self::collectTokensFromInput($tokens, $_POST ?? [], 'post');

        return $tokens;
    }

    protected static function collectTokensFromInput(array &$tokens, $input, string $prefix, int $depth = 0): void
    {
        if (!is_array($input) || $depth > self::MAX_NESTED_DEPTH || count($tokens) >= self::MAX_INSPECTION_TOKENS) {
            return;
        }

        foreach ($input as $key => $value) {
            if (count($tokens) >= self::MAX_INSPECTION_TOKENS) {
                break;
            }

            $keyString = strtolower(preg_replace('/[^a-z0-9_\-]/i', '', (string) $key));
            if ($keyString !== '' && in_array($keyString, self::INSPECTION_EXCLUDED_KEYS, true)) {
                continue;
            }

            $label = $prefix . '.' . ($keyString !== '' ? $keyString : 'item');

            if (is_array($value)) {
                self::collectTokensFromInput($tokens, $value, $label, $depth + 1);
                continue;
            }

            if (is_scalar($value) || $value === null) {
                self::appendInspectionToken($tokens, $label, (string) $value);
            }
        }
    }

    protected static function appendInspectionToken(array &$tokens, string $label, string $value): void
    {
        if (count($tokens) >= self::MAX_INSPECTION_TOKENS) {
            return;
        }

        $cleanLabel = preg_replace('/[^a-z0-9_\.\-]/i', '', strtolower($label));
        $cleanValue = preg_replace('/[\x00-\x1F\x7F]/u', ' ', $value);
        $cleanValue = trim((string) $cleanValue);

        if ($cleanValue === '') {
            return;
        }

        $cleanValue = preg_replace('/\s+/', ' ', $cleanValue);
        $cleanValue = substr((string) $cleanValue, 0, self::MAX_TOKEN_LENGTH);
        $tokens[] = ($cleanLabel !== '' ? $cleanLabel : 'payload') . '=' . $cleanValue;
    }

    protected static function detectSqlInjectionPayload(array $tokens): ?array
    {
        $rules = [
            ['id' => 'sqli_union_select', 'label' => 'UNION SELECT', 'regex' => '/\bunion\b\s+(?:all\s+)?\bselect\b/i', 'score' => 4],
            ['id' => 'sqli_tautology', 'label' => 'Tautology OR/AND 1=1', 'regex' => '/\b(?:or|and)\b\s+1\s*=\s*1\b/i', 'score' => 4],
            ['id' => 'sqli_sleep_benchmark', 'label' => 'Time-based SQL function', 'regex' => '/\b(?:sleep|benchmark|updatexml|extractvalue|load_file)\s*\(/i', 'score' => 4],
            ['id' => 'sqli_outfile', 'label' => 'INTO OUTFILE/DUMPFILE', 'regex' => '/\binto\s+(?:outfile|dumpfile)\b/i', 'score' => 4],
            ['id' => 'sqli_comment_tail', 'label' => 'SQL comment after quote', 'regex' => '/(?:\'|\"|`)\s*(?:or|and)\s+[a-z0-9_]+\s*=\s*[a-z0-9_]+\s*(?:--|#|\/\*)/i', 'score' => 4],
            ['id' => 'sqli_keyword_chain', 'label' => 'SELECT FROM WHERE keyword chain', 'regex' => '/\bselect\b.{0,40}\bfrom\b.{0,40}\bwhere\b/i', 'score' => 2],
        ];

        return self::runDetectionRules(
            $tokens,
            $rules,
            self::SQLI_SCORE_THRESHOLD,
            'waf_sqli',
            'Blocked request due to potential SQL injection payload.'
        );
    }

    protected static function detectCommandInjectionPayload(array $tokens): ?array
    {
        $rules = [
            ['id' => 'cmd_sep_exec', 'label' => 'Command separator + shell command', 'regex' => '/(?:;|\|\||&&|\|)\s*(?:cat|ls|id|whoami|uname|wget|curl|bash|sh|python|perl|php|nc|netcat|chmod|chown|rm|mv|cp)\b/i', 'score' => 4],
            ['id' => 'cmd_subshell', 'label' => 'Subshell execution', 'regex' => '/\$\([^)]{0,160}(?:cat|ls|id|whoami|uname|wget|curl|bash|sh|python|perl|php|nc|netcat|chmod|chown|rm|mv|cp)[^)]*\)/i', 'score' => 4],
            ['id' => 'cmd_backtick_exec', 'label' => 'Backtick command execution', 'regex' => '/`[^`]{0,160}(?:cat|ls|id|whoami|uname|wget|curl|bash|sh|python|perl|php|nc|netcat|chmod|chown|rm|mv|cp)[^`]*`/i', 'score' => 4],
            ['id' => 'cmd_shell_binary', 'label' => 'Direct shell binary reference', 'regex' => '/\b(?:\/bin\/sh|\/bin\/bash|cmd\.exe|powershell(?:\.exe)?)\b/i', 'score' => 3],
            ['id' => 'cmd_netcat_reverse', 'label' => 'Netcat reverse-shell pattern', 'regex' => '/\b(?:nc|netcat)\b.{0,40}\b(?:-e|\/bin\/sh|\/bin\/bash)\b/i', 'score' => 4],
            ['id' => 'cmd_download_pipe', 'label' => 'Download + pipe/chain execution', 'regex' => '/\b(?:curl|wget)\b\s+\S+\s*(?:\||;|&&)/i', 'score' => 4],
        ];

        return self::runDetectionRules(
            $tokens,
            $rules,
            self::CMDI_SCORE_THRESHOLD,
            'waf_command_injection',
            'Blocked request due to potential command injection payload.'
        );
    }

    protected static function runDetectionRules(
        array $tokens,
        array $rules,
        int $threshold,
        string $ruleGroup,
        string $reason
    ): ?array {
        $score = 0;
        $matchedRuleIds = [];
        $firstMatch = null;

        foreach ($tokens as $token) {
            $tokenString = (string) $token;

            foreach ($rules as $rule) {
                $ruleId = (string)($rule['id'] ?? '');
                $regex = (string)($rule['regex'] ?? '');
                $ruleScore = (int)($rule['score'] ?? 0);
                $ruleLabel = (string)($rule['label'] ?? $ruleId);

                if ($ruleId === '' || $regex === '' || isset($matchedRuleIds[$ruleId])) {
                    continue;
                }

                if (!preg_match($regex, $tokenString)) {
                    continue;
                }

                $matchedRuleIds[$ruleId] = true;
                $score += max(1, $ruleScore);

                if ($firstMatch === null) {
                    $firstMatch = [
                        'rule_id' => $ruleId,
                        'matched_pattern' => $ruleLabel,
                        'sample' => substr($tokenString, 0, 220),
                    ];
                }

                if ($score >= $threshold) {
                    return [
                        'rule_group' => $ruleGroup,
                        'rule_id' => (string)($firstMatch['rule_id'] ?? $ruleId),
                        'matched_pattern' => (string)($firstMatch['matched_pattern'] ?? $ruleLabel),
                        'sample' => (string)($firstMatch['sample'] ?? ''),
                        'matched_rule_ids' => array_keys($matchedRuleIds),
                        'score' => $score,
                        'reason' => $reason,
                    ];
                }
            }
        }

        return null;
    }

    protected static function blockRequest(array $data, string $requestUri, string $requestPath = ''): void
    {
        $reason = (string)($data['reason'] ?? 'Forbidden request.');
        $ruleGroup = (string)($data['rule_group'] ?? 'waf');
        $ruleId = (string)($data['rule_id'] ?? 'request_blocked');
        $matchedPattern = (string)($data['matched_pattern'] ?? '');
        $details = is_array($data['details'] ?? null) ? $data['details'] : [];
        $context = is_array($data['context'] ?? null) ? $data['context'] : [];
        $normalizedPath = $requestPath !== '' ? $requestPath : (string)(parse_url(rawurldecode($requestUri), PHP_URL_PATH) ?: '');
        $approvalDetails = self::collectApprovalDetails($ruleGroup, $requestUri, $normalizedPath);

        if ($approvalDetails) {
            $details = array_merge($details, $approvalDetails);
        }

        FirewallService::logEvent('request_blocked', [
            'ip_address' => FirewallService::detectClientIp(),
            'reason' => $reason,
            'request_uri' => $requestUri,
            'event_source' => 'waf_runtime',
            'rule_group' => $ruleGroup,
            'rule_id' => $ruleId,
            'matched_pattern' => $matchedPattern,
            'response_code' => 403,
            'blocked' => 1,
            'details' => $details,
            'context' => $context,
        ]);

        status_header(403);
        nocache_headers();
        header('Content-Type: text/plain; charset=utf-8');
        echo 'Forbidden';
        exit;
    }

    protected static function collectApprovalDetails(string $ruleGroup, string $requestUri, string $requestPath): array
    {
        if (!self::shouldRequestApproval($ruleGroup)) {
            return [];
        }

        $normalizedPath = strtolower(trim($requestPath));
        $isAdminAjax = strpos($normalizedPath, 'admin-ajax.php') !== false;
        $isAdminPost = strpos($normalizedPath, 'admin-post.php') !== false;
        $isRest = strpos($normalizedPath, '/wp-json') === 0;

        if (!$isAdminAjax && !$isAdminPost && !$isRest) {
            return [];
        }

        $action = '';
        if (isset($_REQUEST['action']) && is_scalar($_REQUEST['action'])) {
            $action = sanitize_key((string) wp_unslash($_REQUEST['action']));
        }

        $restRoute = '';
        if (isset($_REQUEST['rest_route']) && is_scalar($_REQUEST['rest_route'])) {
            $restRoute = trim((string) wp_unslash($_REQUEST['rest_route']));
        } elseif ($isRest && $normalizedPath !== '') {
            $restRoute = trim(substr($normalizedPath, strlen('/wp-json')), '/');
        }

        if (($isAdminAjax || $isAdminPost) && $action === '') {
            return [];
        }

        return [
            'approval_status' => 'pending',
            'approval_type' => $isRest ? 'rest' : 'admin_ajax',
            'approval_action' => $action,
            'approval_rest_route' => $restRoute,
            'approval_request_path' => $normalizedPath,
            'approval_request_uri' => $requestUri,
        ];
    }

    protected static function shouldRequestApproval(string $ruleGroup): bool
    {
        if (strpos($ruleGroup, 'waf') !== 0) {
            return false;
        }

        if (!function_exists('is_user_logged_in') || !is_user_logged_in()) {
            return false;
        }

        if (function_exists('current_user_can') && !current_user_can('manage_options')) {
            return false;
        }

        return true;
    }

    protected static function isLoginProtectionEnabled(): bool
    {
        $settings = FirewallService::getSettings();

        return !empty($settings['enabled']) && !empty($settings['login_protection_enabled']);
    }

    protected static function getLockData(string $ipAddress): array
    {
        $lock = get_transient(self::lockKey($ipAddress));
        if (!$lock) {
            return [
                'locked' => false,
                'remaining_seconds' => 0,
            ];
        }

        $remainingSeconds = 0;
        if (is_array($lock) && isset($lock['until'])) {
            $remainingSeconds = (int)$lock['until'] - time();
        }

        if ($remainingSeconds <= 0) {
            delete_transient(self::lockKey($ipAddress));
            return [
                'locked' => false,
                'remaining_seconds' => 0,
            ];
        }

        return [
            'locked' => true,
            'remaining_seconds' => $remainingSeconds,
        ];
    }

    protected static function failedAttemptsKey(string $ipAddress): string
    {
        return self::FAILED_KEY_PREFIX . md5($ipAddress);
    }

    protected static function lockKey(string $ipAddress): string
    {
        return self::LOCK_KEY_PREFIX . md5($ipAddress);
    }

    protected static function xmlrpcRateKey(string $ipAddress): string
    {
        return self::XMLRPC_RATE_KEY_PREFIX . md5($ipAddress === '' ? 'unknown' : $ipAddress);
    }
}
