<?php

namespace VulnTitan\Services;

class FirewallService
{
    protected const OPTION_SETTINGS = 'vulntitan_firewall_settings';
    protected const OPTION_MU_STATUS = 'vulntitan_firewall_mu_status';
    protected const OPTION_SCHEMA_VERSION = 'vulntitan_firewall_schema_version';
    protected const OPTION_LEARNING_DISMISSED = 'vulntitan_firewall_learning_dismissed';
    protected const MU_LOADER_FILENAME = 'vulntitan-firewall.php';
    protected const DEFAULT_LOG_RETENTION_DAYS = 30;
    protected const SCHEMA_VERSION = 2;
    protected const BLOCKED_EVENT_TYPES = ['request_blocked', 'login_blocked', 'login_lockout'];

    protected static bool $tableChecked = false;
    protected static bool $tableReady = false;

    public static function getDefaultSettings(): array
    {
        return [
            'enabled' => 1,
            'login_protection_enabled' => 1,
            'custom_login_slug' => '',
            'two_factor_enabled' => 0,
            'two_factor_roles' => ['administrator', 'editor'],
            'trusted_device_days' => 30,
            'captcha_provider' => 'none',
            'captcha_site_key' => '',
            'captcha_secret_key' => '',
            'captcha_login_enabled' => 0,
            'captcha_register_enabled' => 0,
            'captcha_lostpassword_enabled' => 0,
            'captcha_comment_enabled' => 0,
            'xmlrpc_policy' => 'allow',
            'xmlrpc_allowlist_ips' => [],
            'lockout_allowlist_ips' => [],
            'xmlrpc_rate_limit_attempts' => 20,
            'xmlrpc_rate_limit_window_minutes' => 10,
            'weak_password_blocking_enabled' => 0,
            'waf_sqli_enabled' => 1,
            'waf_command_injection_enabled' => 1,
            'waf_whitelist_paths' => [],
            'learning_mode_enabled' => 0,
            'learning_suggestion_threshold' => 3,
            'learning_suggestion_window_days' => 7,
            'trust_cloudflare' => 0,
            'trusted_proxies' => [],
            'max_attempts' => 5,
            'lockout_minutes' => 15,
            'comment_shield_enabled' => 0,
            'form_shield_enabled' => 0,
            'form_provider_cf7_enabled' => 0,
            'form_provider_fluentforms_enabled' => 0,
            'cf7_shield_enabled' => 0,
            'submission_min_submit_seconds' => 3,
            'submission_max_links' => 2,
            'submission_rate_limit_attempts' => 3,
            'submission_rate_limit_window_minutes' => 10,
            'comment_min_submit_seconds' => 3,
            'comment_max_links' => 2,
            'comment_rate_limit_attempts' => 3,
            'comment_rate_limit_window_minutes' => 10,
            'comment_suspicious_action' => 'hold',
            'log_retention_days' => self::DEFAULT_LOG_RETENTION_DAYS,
            'weekly_summary_email_enabled' => 1,
            'weekly_summary_email_recipient' => sanitize_email((string) get_option('admin_email', '')),
        ];
    }

    public static function getSettings(): array
    {
        $defaults = self::getDefaultSettings();
        $stored = get_option(self::OPTION_SETTINGS, []);

        if (!is_array($stored)) {
            $stored = [];
        }

        if (!array_key_exists('form_shield_enabled', $stored)) {
            $stored['form_shield_enabled'] = $stored['cf7_shield_enabled'] ?? $stored['comment_shield_enabled'] ?? $defaults['form_shield_enabled'];
        }

        if (!array_key_exists('form_provider_cf7_enabled', $stored)) {
            $stored['form_provider_cf7_enabled'] = $stored['cf7_shield_enabled'] ?? $stored['form_shield_enabled'] ?? $stored['comment_shield_enabled'] ?? $defaults['form_provider_cf7_enabled'];
        }

        if (!array_key_exists('form_provider_fluentforms_enabled', $stored)) {
            $stored['form_provider_fluentforms_enabled'] = $defaults['form_provider_fluentforms_enabled'];
        }

        if (!array_key_exists('cf7_shield_enabled', $stored)) {
            $stored['cf7_shield_enabled'] = $stored['form_provider_cf7_enabled'] ?? $stored['form_shield_enabled'] ?? $stored['comment_shield_enabled'] ?? $defaults['cf7_shield_enabled'];
        }

        if (!array_key_exists('submission_min_submit_seconds', $stored)) {
            $stored['submission_min_submit_seconds'] = $stored['comment_min_submit_seconds'] ?? $defaults['submission_min_submit_seconds'];
        }

        if (!array_key_exists('submission_max_links', $stored)) {
            $stored['submission_max_links'] = $stored['comment_max_links'] ?? $defaults['submission_max_links'];
        }

        if (!array_key_exists('submission_rate_limit_attempts', $stored)) {
            $stored['submission_rate_limit_attempts'] = $stored['comment_rate_limit_attempts'] ?? $defaults['submission_rate_limit_attempts'];
        }

        if (!array_key_exists('submission_rate_limit_window_minutes', $stored)) {
            $stored['submission_rate_limit_window_minutes'] = $stored['comment_rate_limit_window_minutes'] ?? $defaults['submission_rate_limit_window_minutes'];
        }

        return self::sanitizeSettings(array_merge($defaults, $stored));
    }

    public static function saveSettings(array $input): array
    {
        $current = self::getSettings();
        $normalized = self::sanitizeSettings(array_merge($current, $input));

        update_option(self::OPTION_SETTINGS, $normalized, false);

        return $normalized;
    }

    public static function getCustomLoginSlug(): string
    {
        $settings = self::getSettings();

        return (string) ($settings['custom_login_slug'] ?? '');
    }

    public static function isCustomLoginEnabled(): bool
    {
        return self::getCustomLoginSlug() !== '';
    }

    public static function getCustomLoginUrl(array $queryArgs = [], string $scheme = 'login'): string
    {
        $slug = self::getCustomLoginSlug();
        if ($slug === '') {
            return '';
        }

        $url = home_url('/' . user_trailingslashit($slug), $scheme);

        if ($queryArgs) {
            $url = add_query_arg($queryArgs, $url);
        }

        return $url;
    }

    public static function getLoginAccessData(): array
    {
        $slug = self::getCustomLoginSlug();

        return [
            'enabled' => $slug !== '',
            'slug' => $slug,
            'url' => $slug !== '' ? self::getCustomLoginUrl() : '',
        ];
    }

    public static function getXmlrpcPolicy(): string
    {
        $settings = self::getSettings();

        return (string) ($settings['xmlrpc_policy'] ?? 'allow');
    }

    public static function isXmlrpcIpAllowlisted(string $ipAddress): bool
    {
        $ipAddress = self::sanitizeIp($ipAddress);
        if ($ipAddress === '') {
            return false;
        }

        $settings = self::getSettings();
        $allowlist = $settings['xmlrpc_allowlist_ips'] ?? [];

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

        return in_array($ipAddress, $allowlist, true);
    }

    public static function isLockoutIpAllowlisted(string $ipAddress): bool
    {
        $ipAddress = self::sanitizeIp($ipAddress);
        if ($ipAddress === '') {
            return false;
        }

        $settings = self::getSettings();
        $allowlist = $settings['lockout_allowlist_ips'] ?? [];

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

        return in_array($ipAddress, $allowlist, true);
    }

    public static function clearLoginLockout(string $ipAddress): bool
    {
        $ipAddress = self::sanitizeIp($ipAddress);
        if ($ipAddress === '') {
            return false;
        }

        delete_transient('vulntitan_fw_failed_' . md5($ipAddress));
        delete_transient('vulntitan_fw_lock_' . md5($ipAddress));

        return true;
    }

    public static function isWeeklySummaryEmailEnabled(): bool
    {
        $settings = self::getSettings();

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

    public static function getWeeklySummaryEmailRecipient(): string
    {
        $settings = self::getSettings();
        $recipient = sanitize_email((string) ($settings['weekly_summary_email_recipient'] ?? ''));

        if ($recipient === '') {
            $recipient = sanitize_email((string) get_option('admin_email', ''));
        }

        return $recipient;
    }

    public static function validateCustomLoginSlug($input): array
    {
        $raw = is_scalar($input) ? trim((string) $input) : '';
        $slug = self::sanitizeCustomLoginSlug($input);

        if ($raw !== '' && $slug === '') {
            return [
                'slug' => '',
                'error' => 'invalid',
            ];
        }

        if ($slug !== '' && self::customLoginSlugConflicts($slug)) {
            return [
                'slug' => '',
                'error' => 'conflict',
            ];
        }

        return [
            'slug' => $slug,
            'error' => '',
        ];
    }

    public static function getTableName(): string
    {
        global $wpdb;

        return $wpdb->prefix . 'vulntitan_firewall_logs';
    }

    public static function ensureTable(): bool
    {
        if (self::$tableChecked) {
            return self::$tableReady;
        }

        self::$tableChecked = true;
        $tableExists = self::tableExists();
        $storedSchemaVersion = (int) get_option(self::OPTION_SCHEMA_VERSION, 0);
        $hasRequiredColumns = $tableExists && self::hasRequiredSchemaColumns();
        $needsSchemaSync = !$tableExists || !$hasRequiredColumns || $storedSchemaVersion < self::SCHEMA_VERSION;

        if ($needsSchemaSync) {
            self::$tableReady = self::createTable();
        } else {
            self::$tableReady = true;
        }

        if (self::$tableReady && $storedSchemaVersion !== self::SCHEMA_VERSION) {
            update_option(self::OPTION_SCHEMA_VERSION, self::SCHEMA_VERSION, false);
        }

        return self::$tableReady;
    }

    public static function createTable(): bool
    {
        global $wpdb;

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $tableName = self::getTableName();
        $charsetCollate = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE {$tableName} (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            event_uuid CHAR(36) NOT NULL DEFAULT '',
            site_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
            event_type VARCHAR(64) NOT NULL,
            event_action VARCHAR(16) NOT NULL DEFAULT 'log',
            event_source VARCHAR(64) NOT NULL DEFAULT '',
            severity TINYINT(3) UNSIGNED NOT NULL DEFAULT 1,
            blocked TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
            response_code SMALLINT(5) UNSIGNED NOT NULL DEFAULT 0,
            ip_address VARCHAR(45) NOT NULL DEFAULT '',
            ip_hash CHAR(64) NOT NULL DEFAULT '',
            forwarded_for VARCHAR(255) NOT NULL DEFAULT '',
            country_code CHAR(2) NOT NULL DEFAULT '',
            username VARCHAR(191) NOT NULL DEFAULT '',
            user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
            session_id VARCHAR(128) NOT NULL DEFAULT '',
            request_id VARCHAR(64) NOT NULL DEFAULT '',
            request_method VARCHAR(16) NOT NULL DEFAULT '',
            request_scheme VARCHAR(16) NOT NULL DEFAULT '',
            request_host VARCHAR(191) NOT NULL DEFAULT '',
            request_uri TEXT NULL,
            request_path VARCHAR(191) NOT NULL DEFAULT '',
            query_string TEXT NULL,
            referer TEXT NULL,
            user_agent TEXT NULL,
            matched_pattern VARCHAR(255) NOT NULL DEFAULT '',
            rule_id VARCHAR(128) NOT NULL DEFAULT '',
            rule_group VARCHAR(128) NOT NULL DEFAULT '',
            rule_version VARCHAR(64) NOT NULL DEFAULT '',
            is_authenticated TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
            is_ajax TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
            is_rest TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
            is_xmlrpc TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
            is_wp_login TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
            reason VARCHAR(255) NOT NULL DEFAULT '',
            details LONGTEXT NULL,
            context_json LONGTEXT NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY event_uuid (event_uuid),
            KEY event_type (event_type),
            KEY event_action (event_action),
            KEY blocked_created_at (blocked, created_at),
            KEY ip_address (ip_address),
            KEY user_id (user_id),
            KEY rule_id (rule_id),
            KEY created_at (created_at)
        ) {$charsetCollate};";

        dbDelta($sql);

        self::$tableReady = self::tableExists() && self::hasRequiredSchemaColumns();
        self::$tableChecked = true;

        if (self::$tableReady) {
            update_option(self::OPTION_SCHEMA_VERSION, self::SCHEMA_VERSION, false);
        }

        return self::$tableReady;
    }

    public static function logEvent(string $eventType, array $data = []): void
    {
        if (!self::ensureTable()) {
            return;
        }

        global $wpdb;

        $server = is_array($_SERVER) ? $_SERVER : [];
        $tableName = self::getTableName();
        $normalizedType = self::sanitizeEventType($eventType);
        $ipAddress = self::sanitizeIp((string)($data['ip_address'] ?? self::detectClientIp()));
        $siteId = max(0, (int)($data['site_id'] ?? (function_exists('get_current_blog_id') ? get_current_blog_id() : 0)));
        $userId = max(0, (int)($data['user_id'] ?? self::detectCurrentUserId()));
        $username = sanitize_user((string)($data['username'] ?? self::detectCurrentUsername($userId)), true);
        $requestUri = self::sanitizeRequestUri((string)($data['request_uri'] ?? ($server['REQUEST_URI'] ?? '')));
        $requestPath = self::sanitizeRequestPath((string)($data['request_path'] ?? self::extractRequestPath($requestUri)));
        $queryString = self::sanitizeRequestUri((string)($data['query_string'] ?? ($server['QUERY_STRING'] ?? self::extractQueryString($requestUri))));
        $method = strtoupper(sanitize_text_field((string)($data['request_method'] ?? ($server['REQUEST_METHOD'] ?? ''))));
        $eventAction = self::sanitizeEventAction((string)($data['event_action'] ?? self::inferEventAction($normalizedType)));
        $blockedDefault = self::isBlockedEventType($normalizedType) || $eventAction === 'block';
        $blocked = self::normalizeFlag($data['blocked'] ?? $blockedDefault);
        $responseCode = self::sanitizeResponseCode($data['response_code'] ?? self::inferResponseCode($normalizedType, $blocked));
        $severity = self::sanitizeSeverity($data['severity'] ?? self::inferSeverity($normalizedType));
        $reason = self::sanitizeShortText((string)($data['reason'] ?? ''), 255);
        $detailsRaw = $data['details'] ?? [];
        $contextRaw = $data['context'] ?? [];

        if (!is_array($contextRaw)) {
            $contextRaw = ['value' => (string) $contextRaw];
        }

        if (!is_array($detailsRaw)) {
            $detailsRaw = ['value' => (string) $detailsRaw];
        }

        $details = wp_json_encode($detailsRaw, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $context = wp_json_encode($contextRaw, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

        $eventUuid = self::sanitizeUuid((string)($data['event_uuid'] ?? self::generateEventUuid()));
        $eventSource = self::sanitizeEventType((string)($data['event_source'] ?? 'firewall'));
        $requestScheme = self::sanitizeRequestScheme((string)($data['request_scheme'] ?? self::detectRequestScheme($server)));
        $requestHost = self::sanitizeRequestHost((string)($data['request_host'] ?? self::detectRequestHost($server)));
        $forwardedFor = self::sanitizeForwardedFor((string)($data['forwarded_for'] ?? ($server['HTTP_X_FORWARDED_FOR'] ?? '')));
        $countryCode = self::sanitizeCountryCode((string)($data['country_code'] ?? self::detectCountryCode($server)));
        $sessionId = self::sanitizeShortText((string)($data['session_id'] ?? self::detectSessionId()), 128);
        $requestId = self::sanitizeRequestId((string)($data['request_id'] ?? self::detectRequestId($server)));
        $referer = self::sanitizeRequestUri((string)($data['referer'] ?? ($server['HTTP_REFERER'] ?? '')));
        $userAgent = self::sanitizeRequestUri((string)($data['user_agent'] ?? ($server['HTTP_USER_AGENT'] ?? '')));
        $matchedPattern = self::sanitizeShortText((string)($data['matched_pattern'] ?? ''), 255);
        $ruleId = self::sanitizeShortText((string)($data['rule_id'] ?? ''), 128);
        $ruleGroup = self::sanitizeShortText((string)($data['rule_group'] ?? ''), 128);
        $ruleVersion = self::sanitizeShortText((string)($data['rule_version'] ?? ''), 64);
        $isAuthenticated = self::normalizeFlag($data['is_authenticated'] ?? self::isLoggedIn());
        $isAjax = self::normalizeFlag($data['is_ajax'] ?? self::detectIsAjaxRequest($server));
        $isRest = self::normalizeFlag($data['is_rest'] ?? self::detectIsRestRequest($requestPath));
        $isXmlrpc = self::normalizeFlag($data['is_xmlrpc'] ?? self::detectIsXmlRpcRequest($requestPath));
        $isWpLogin = self::normalizeFlag($data['is_wp_login'] ?? self::detectIsWpLoginRequest($requestPath));
        $createdAt = self::sanitizeMysqlDatetime((string)($data['created_at'] ?? gmdate('Y-m-d H:i:s')));

        $wpdb->insert(
            $tableName,
            [
                'event_uuid' => $eventUuid,
                'site_id' => $siteId,
                'event_type' => $normalizedType,
                'event_action' => $eventAction,
                'event_source' => $eventSource,
                'severity' => $severity,
                'blocked' => $blocked,
                'response_code' => $responseCode,
                'ip_address' => $ipAddress,
                'ip_hash' => self::buildIpHash($ipAddress),
                'forwarded_for' => $forwardedFor,
                'country_code' => $countryCode,
                'username' => $username,
                'user_id' => $userId,
                'session_id' => $sessionId,
                'request_id' => $requestId,
                'request_method' => substr($method, 0, 16),
                'request_scheme' => $requestScheme,
                'request_host' => $requestHost,
                'request_uri' => $requestUri,
                'request_path' => $requestPath,
                'query_string' => $queryString,
                'referer' => $referer,
                'user_agent' => $userAgent,
                'matched_pattern' => $matchedPattern,
                'rule_id' => $ruleId,
                'rule_group' => $ruleGroup,
                'rule_version' => $ruleVersion,
                'is_authenticated' => $isAuthenticated,
                'is_ajax' => $isAjax,
                'is_rest' => $isRest,
                'is_xmlrpc' => $isXmlrpc,
                'is_wp_login' => $isWpLogin,
                'reason' => $reason,
                'details' => $details ?: '',
                'context_json' => $context ?: '',
                'created_at' => $createdAt,
            ]
        );
    }

    public static function getSummary(int $hours = 24): array
    {
        if (!self::ensureTable()) {
            return [
                'window_hours' => max(1, $hours),
                'total' => 0,
                'blocked' => 0,
                'forms_blocked' => 0,
                'login_failed' => 0,
                'lockouts' => 0,
                'spam_blocked' => 0,
                'spam_held' => 0,
            ];
        }

        global $wpdb;

        $tableName = self::getTableName();
        $windowHours = max(1, $hours);
        $since = gmdate('Y-m-d H:i:s', time() - ($windowHours * HOUR_IN_SECONDS));

        $total = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s", $since)
        );

        $blocked = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s AND (blocked = 1 OR event_type IN ('request_blocked', 'login_blocked', 'login_lockout'))",
                $since
            )
        );

        $loginFailed = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s AND event_type = 'login_failed'", $since)
        );

        $formsBlocked = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s AND event_type = 'request_blocked' AND event_source IN ('contact_form_7', 'fluent_forms')",
                $since
            )
        );

        $lockouts = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s AND event_type = 'login_lockout'", $since)
        );

        $spamBlocked = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s AND event_type IN ('comment_spam_blocked', 'comment_rate_limited')",
                $since
            )
        );

        $spamHeld = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$tableName} WHERE created_at >= %s AND event_type = 'comment_spam_held'", $since)
        );

        return [
            'window_hours' => $windowHours,
            'total' => $total,
            'blocked' => $blocked,
            'forms_blocked' => $formsBlocked,
            'login_failed' => $loginFailed,
            'lockouts' => $lockouts,
            'spam_blocked' => $spamBlocked,
            'spam_held' => $spamHeld,
        ];
    }

    public static function getRecentLogs(int $limit = 80): array
    {
        if (!self::ensureTable()) {
            return [];
        }

        global $wpdb;

        $tableName = self::getTableName();
        $safeLimit = max(1, min(200, $limit));

        $rows = $wpdb->get_results(
            "SELECT id, event_uuid, event_type, event_action, event_source, severity, blocked, response_code,
                    ip_address, forwarded_for, country_code, username, user_id, session_id, request_id,
                    request_method, request_scheme, request_host, request_uri, request_path, query_string,
                    referer, user_agent, matched_pattern, rule_id, rule_group, is_authenticated, is_ajax,
                    is_rest, is_xmlrpc, is_wp_login, reason, details, context_json, created_at
             FROM {$tableName}
             ORDER BY id DESC
             LIMIT {$safeLimit}",
            ARRAY_A
        );

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

        foreach ($rows as &$row) {
            $row['details'] = self::decodeDetails((string)($row['details'] ?? ''));
            $row['context'] = self::decodeDetails((string)($row['context_json'] ?? ''));
            unset($row['context_json']);
            $row['created_at_local'] = self::toLocalDate((string)($row['created_at'] ?? ''));
        }
        unset($row);

        return $rows;
    }

    public static function getPendingApprovals(int $limit = 60): array
    {
        if (!self::ensureTable()) {
            return [];
        }

        global $wpdb;

        $tableName = self::getTableName();
        $safeLimit = max(1, min(200, $limit));
        $threshold = gmdate('Y-m-d H:i:s', time() - (14 * DAY_IN_SECONDS));
        $likePattern = '%"approval_status":"pending"%';

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, event_uuid, event_type, event_action, event_source, severity, blocked, response_code,
                        ip_address, forwarded_for, country_code, username, user_id, session_id, request_id,
                        request_method, request_scheme, request_host, request_uri, request_path, query_string,
                        referer, user_agent, matched_pattern, rule_id, rule_group, is_authenticated, is_ajax,
                        is_rest, is_xmlrpc, is_wp_login, reason, details, context_json, created_at
                 FROM {$tableName}
                 WHERE event_type = 'request_blocked'
                   AND rule_group IN ('waf_sqli', 'waf_command_injection', 'waf')
                   AND details LIKE %s
                   AND created_at >= %s
                 ORDER BY id DESC
                 LIMIT {$safeLimit}",
                $likePattern,
                $threshold
            ),
            ARRAY_A
        );

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

        foreach ($rows as &$row) {
            $row['details'] = self::decodeDetails((string)($row['details'] ?? ''));
            $row['context'] = self::decodeDetails((string)($row['context_json'] ?? ''));
            unset($row['context_json']);
            $row['created_at_local'] = self::toLocalDate((string)($row['created_at'] ?? ''));
            $details = is_array($row['details']) ? $row['details'] : [];
            $row['approval_action'] = isset($details['approval_action']) ? self::sanitizeShortText((string) $details['approval_action'], 80) : '';
            $row['approval_rest_route'] = isset($details['approval_rest_route']) ? self::sanitizeShortText((string) $details['approval_rest_route'], 160) : '';
            $row['approval_pattern'] = self::buildApprovalPattern($row);
        }
        unset($row);

        return $rows;
    }

    public static function getPendingApprovalCount(): int
    {
        if (!self::ensureTable()) {
            return 0;
        }

        global $wpdb;

        $tableName = self::getTableName();
        $threshold = gmdate('Y-m-d H:i:s', time() - (14 * DAY_IN_SECONDS));
        $likePattern = '%"approval_status":"pending"%';

        $count = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*)
                 FROM {$tableName}
                 WHERE event_type = 'request_blocked'
                   AND rule_group IN ('waf_sqli', 'waf_command_injection', 'waf')
                   AND details LIKE %s
                   AND created_at >= %s",
                $likePattern,
                $threshold
            )
        );

        return max(0, $count);
    }

    public static function getApprovalCandidateById(int $logId): ?array
    {
        $logId = max(0, $logId);
        if ($logId === 0) {
            return null;
        }

        $row = self::getLogById($logId);
        if (!$row) {
            return null;
        }

        if (($row['event_type'] ?? '') !== 'request_blocked') {
            return null;
        }

        $ruleGroup = (string)($row['rule_group'] ?? '');
        if (!in_array($ruleGroup, ['waf_sqli', 'waf_command_injection', 'waf'], true)) {
            return null;
        }

        $details = is_array($row['details'] ?? null) ? $row['details'] : [];
        if (($details['approval_status'] ?? '') !== 'pending') {
            return null;
        }

        $row['approval_action'] = isset($details['approval_action']) ? self::sanitizeShortText((string) $details['approval_action'], 80) : '';
        $row['approval_rest_route'] = isset($details['approval_rest_route']) ? self::sanitizeShortText((string) $details['approval_rest_route'], 160) : '';
        $row['approval_pattern'] = self::buildApprovalPattern($row);

        return $row;
    }

    public static function updateApprovalStatus(int $logId, string $status, int $userId, string $pattern = ''): bool
    {
        if (!self::ensureTable()) {
            return false;
        }

        $logId = max(0, $logId);
        if ($logId === 0) {
            return false;
        }

        $allowedStatus = ['approved', 'dismissed'];
        if (!in_array($status, $allowedStatus, true)) {
            return false;
        }

        $row = self::getLogById($logId);
        if (!$row) {
            return false;
        }

        $details = is_array($row['details'] ?? null) ? $row['details'] : [];
        $details['approval_status'] = $status;
        $details['approval_updated_at'] = gmdate('Y-m-d H:i:s');

        if ($userId > 0) {
            $details['approval_updated_by'] = $userId;
        }

        if ($pattern !== '') {
            $details['approval_pattern'] = self::sanitizeShortText($pattern, 190);
        }

        $encoded = wp_json_encode($details, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($encoded === false) {
            return false;
        }

        global $wpdb;
        $tableName = self::getTableName();

        $updated = $wpdb->update(
            $tableName,
            ['details' => $encoded],
            ['id' => $logId],
            ['%s'],
            ['%d']
        );

        return $updated !== false;
    }

    public static function getLogById(int $logId): ?array
    {
        if (!self::ensureTable()) {
            return null;
        }

        $logId = max(0, $logId);
        if ($logId === 0) {
            return null;
        }

        global $wpdb;

        $tableName = self::getTableName();
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT id, event_uuid, event_type, event_action, event_source, severity, blocked, response_code,
                        ip_address, forwarded_for, country_code, username, user_id, session_id, request_id,
                        request_method, request_scheme, request_host, request_uri, request_path, query_string,
                        referer, user_agent, matched_pattern, rule_id, rule_group, is_authenticated, is_ajax,
                        is_rest, is_xmlrpc, is_wp_login, reason, details, context_json, created_at
                 FROM {$tableName}
                 WHERE id = %d
                 LIMIT 1",
                $logId
            ),
            ARRAY_A
        );

        if (!is_array($row)) {
            return null;
        }

        $row['details'] = self::decodeDetails((string)($row['details'] ?? ''));
        $row['context'] = self::decodeDetails((string)($row['context_json'] ?? ''));
        unset($row['context_json']);
        $row['created_at_local'] = self::toLocalDate((string)($row['created_at'] ?? ''));

        return $row;
    }

    public static function getLearningSuggestions(int $limit = 20): array
    {
        $settings = self::getSettings();
        if (empty($settings['learning_mode_enabled'])) {
            return [];
        }

        if (!self::ensureTable()) {
            return [];
        }

        global $wpdb;

        $safeLimit = max(1, min(50, $limit));
        $minHits = max(2, min(20, (int)($settings['learning_suggestion_threshold'] ?? 3)));
        $windowDays = max(1, min(30, (int)($settings['learning_suggestion_window_days'] ?? 7)));
        $threshold = gmdate('Y-m-d H:i:s', time() - ($windowDays * DAY_IN_SECONDS));
        $likePattern = '%"approval_status":"pending"%';
        $queryLimit = max(200, min(2000, $safeLimit * 80));
        $tableName = self::getTableName();

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, rule_group, rule_id, request_method, request_host, request_uri, request_path, details, created_at
                 FROM {$tableName}
                 WHERE event_type = 'request_blocked'
                   AND rule_group IN ('waf_sqli', 'waf_command_injection', 'waf')
                   AND details LIKE %s
                   AND created_at >= %s
                 ORDER BY id DESC
                 LIMIT {$queryLimit}",
                $likePattern,
                $threshold
            ),
            ARRAY_A
        );

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

        $dismissed = self::getLearningDismissedPatterns();
        $whitelist = $settings['waf_whitelist_paths'] ?? [];
        if (!is_array($whitelist)) {
            $whitelist = [];
        }

        $grouped = [];

        foreach ($rows as $row) {
            $details = self::decodeDetails((string)($row['details'] ?? ''));
            if (($details['approval_status'] ?? '') !== 'pending') {
                continue;
            }

            $row['details'] = $details;
            $pattern = self::buildApprovalPattern($row);
            if ($pattern === '') {
                continue;
            }

            if (in_array($pattern, $whitelist, true) || in_array($pattern, $dismissed, true)) {
                continue;
            }

            $key = strtolower($pattern);
            if (!isset($grouped[$key])) {
                $grouped[$key] = [
                    'pattern' => $pattern,
                    'count' => 0,
                    'rule_group' => (string)($row['rule_group'] ?? ''),
                    'rule_id' => (string)($row['rule_id'] ?? ''),
                    'approval_action' => isset($details['approval_action']) ? self::sanitizeShortText((string) $details['approval_action'], 80) : '',
                    'approval_rest_route' => isset($details['approval_rest_route']) ? self::sanitizeShortText((string) $details['approval_rest_route'], 160) : '',
                    'request_path' => (string)($row['request_path'] ?? ''),
                    'request_uri' => (string)($row['request_uri'] ?? ''),
                    'last_seen' => (string)($row['created_at'] ?? ''),
                ];
            }

            $grouped[$key]['count']++;

            if ((string)($row['created_at'] ?? '') > (string)$grouped[$key]['last_seen']) {
                $grouped[$key]['last_seen'] = (string)($row['created_at'] ?? '');
            }
        }

        $filtered = array_filter($grouped, function ($item) use ($minHits) {
            return (int)($item['count'] ?? 0) >= $minHits;
        });

        if (!$filtered) {
            return [];
        }

        $sorted = array_values($filtered);
        usort($sorted, function ($a, $b) {
            $countDiff = (int)($b['count'] ?? 0) - (int)($a['count'] ?? 0);
            if ($countDiff !== 0) {
                return $countDiff;
            }
            return strcmp((string)($b['last_seen'] ?? ''), (string)($a['last_seen'] ?? ''));
        });

        $sorted = array_slice($sorted, 0, $safeLimit);

        foreach ($sorted as &$item) {
            $item['last_seen_local'] = self::toLocalDate((string)($item['last_seen'] ?? ''));
        }
        unset($item);

        return $sorted;
    }

    public static function applyLearningSuggestion(string $pattern): array
    {
        $pattern = self::normalizeLearningPattern($pattern);
        if ($pattern === '') {
            return [
                'success' => false,
                'message' => 'invalid_pattern',
            ];
        }

        $settings = self::getSettings();
        $whitelist = $settings['waf_whitelist_paths'] ?? [];
        if (!is_array($whitelist)) {
            $whitelist = [];
        }

        if (!in_array($pattern, $whitelist, true)) {
            $whitelist[] = $pattern;
            $settings = self::saveSettings([
                'waf_whitelist_paths' => $whitelist,
            ]);
        }

        self::removeLearningDismissedPattern($pattern);

        return [
            'success' => true,
            'settings' => $settings,
        ];
    }

    public static function dismissLearningSuggestion(string $pattern): bool
    {
        $pattern = self::normalizeLearningPattern($pattern);
        if ($pattern === '') {
            return false;
        }

        $dismissed = self::getLearningDismissedPatterns();
        if (!in_array($pattern, $dismissed, true)) {
            $dismissed[] = $pattern;
        }

        self::storeLearningDismissedPatterns($dismissed);

        return true;
    }

    protected static function normalizeLearningPattern(string $pattern): string
    {
        $pattern = trim($pattern);
        if ($pattern === '') {
            return '';
        }

        $normalized = self::sanitizeWhitelistPaths([$pattern]);
        return $normalized[0] ?? '';
    }

    protected static function getLearningDismissedPatterns(): array
    {
        $stored = get_option(self::OPTION_LEARNING_DISMISSED, []);
        if (!is_array($stored)) {
            $stored = [];
        }

        return self::sanitizeWhitelistPaths($stored);
    }

    protected static function storeLearningDismissedPatterns(array $patterns): void
    {
        $patterns = self::sanitizeWhitelistPaths($patterns);
        if (count($patterns) > 200) {
            $patterns = array_slice($patterns, -200);
        }

        update_option(self::OPTION_LEARNING_DISMISSED, $patterns, false);
    }

    protected static function removeLearningDismissedPattern(string $pattern): void
    {
        $pattern = self::normalizeLearningPattern($pattern);
        if ($pattern === '') {
            return;
        }

        $dismissed = self::getLearningDismissedPatterns();
        $filtered = array_values(array_filter($dismissed, function ($item) use ($pattern) {
            return $item !== $pattern;
        }));

        if ($filtered !== $dismissed) {
            self::storeLearningDismissedPatterns($filtered);
        }
    }

    protected static function buildApprovalPattern(array $row): string
    {
        $details = is_array($row['details'] ?? null) ? $row['details'] : [];
        $requestPath = (string)($row['request_path'] ?? '');
        $requestUri = (string)($row['request_uri'] ?? '');
        $normalizedPath = strtolower(trim($requestPath));
        $action = isset($details['approval_action']) ? sanitize_key((string) $details['approval_action']) : '';
        $restRoute = isset($details['approval_rest_route']) ? (string) $details['approval_rest_route'] : '';

        if ($normalizedPath !== '') {
            if (strpos($normalizedPath, 'admin-ajax.php') !== false) {
                if ($action === '') {
                    return '';
                }

                return substr('/wp-admin/admin-ajax.php?action=' . $action . '*', 0, 190);
            }

            if (strpos($normalizedPath, 'admin-post.php') !== false) {
                if ($action === '') {
                    return '';
                }

                return substr('/wp-admin/admin-post.php?action=' . $action . '*', 0, 190);
            }
        }

        if ($restRoute !== '') {
            $route = '/' . ltrim($restRoute, '/');
            return substr('/wp-json' . $route, 0, 190);
        }

        if ($requestPath !== '') {
            return substr($requestPath, 0, 190);
        }

        if ($requestUri !== '') {
            $path = (string)(parse_url($requestUri, PHP_URL_PATH) ?: '');
            if ($path !== '') {
                return substr($path, 0, 190);
            }
        }

        return '';
    }

    public static function clearLogs(): int
    {
        if (!self::ensureTable()) {
            return 0;
        }

        global $wpdb;
        $tableName = self::getTableName();

        $deleted = $wpdb->query("DELETE FROM {$tableName}");

        return $deleted === false ? 0 : (int) $deleted;
    }

    public static function cleanupOldLogs(?int $days = null): int
    {
        if (!self::ensureTable()) {
            return 0;
        }

        global $wpdb;

        $settings = self::getSettings();
        $retentionDays = $days ?? (int)($settings['log_retention_days'] ?? self::DEFAULT_LOG_RETENTION_DAYS);
        $retentionDays = max(1, min(365, $retentionDays));
        $threshold = gmdate('Y-m-d H:i:s', time() - ($retentionDays * DAY_IN_SECONDS));
        $tableName = self::getTableName();

        $deleted = $wpdb->query(
            $wpdb->prepare("DELETE FROM {$tableName} WHERE created_at < %s", $threshold)
        );

        return $deleted === false ? 0 : (int) $deleted;
    }

    public static function getWeeklyDigestData(int $days = 7): array
    {
        $windowDays = max(1, min(30, $days));
        $timezone = self::getWpTimezone();
        $todayStartLocal = (new \DateTimeImmutable('now', $timezone))->setTime(0, 0, 0);
        $periodEndLocal = $todayStartLocal;
        $periodStartLocal = $periodEndLocal->modify('-' . $windowDays . ' days');
        $periodStartUtc = $periodStartLocal->setTimezone(new \DateTimeZone('UTC'));
        $periodEndUtc = $periodEndLocal->setTimezone(new \DateTimeZone('UTC'));

        $data = [
            'window_days' => $windowDays,
            'period' => [
                'key' => $periodStartLocal->format('Ymd') . '-' . $periodEndLocal->format('Ymd'),
                'start_gmt' => $periodStartUtc->format('Y-m-d H:i:s'),
                'end_gmt' => $periodEndUtc->format('Y-m-d H:i:s'),
                'start_local' => $periodStartLocal->format('Y-m-d H:i:s'),
                'end_local' => $periodEndLocal->format('Y-m-d H:i:s'),
                'start_local_display' => self::formatLocalTimestamp($periodStartLocal->getTimestamp(), 'M j, Y'),
                'end_local_display' => self::formatLocalTimestamp($periodEndLocal->modify('-1 second')->getTimestamp(), 'M j, Y'),
            ],
            'totals' => self::emptyDigestCounts(),
            'daily' => [],
            'top_paths' => [],
            'top_rules' => [],
        ];

        for ($index = 0; $index < $windowDays; $index++) {
            $dayStartLocal = $periodStartLocal->modify('+' . $index . ' days');

            $data['daily'][] = [
                'date' => $dayStartLocal->format('Y-m-d'),
                'label' => self::formatLocalTimestamp($dayStartLocal->getTimestamp(), 'D, M j'),
                'counts' => self::emptyDigestCounts(),
            ];
        }

        if (!self::ensureTable()) {
            return $data;
        }

        $data['totals'] = self::queryDigestCountsForWindow(
            $periodStartUtc->format('Y-m-d H:i:s'),
            $periodEndUtc->format('Y-m-d H:i:s')
        );

        foreach ($data['daily'] as $index => $day) {
            $dayStartLocal = (new \DateTimeImmutable($day['date'] . ' 00:00:00', $timezone));
            $dayEndLocal = $dayStartLocal->modify('+1 day');
            $dayStartUtc = $dayStartLocal->setTimezone(new \DateTimeZone('UTC'));
            $dayEndUtc = $dayEndLocal->setTimezone(new \DateTimeZone('UTC'));

            $data['daily'][$index]['counts'] = self::queryDigestCountsForWindow(
                $dayStartUtc->format('Y-m-d H:i:s'),
                $dayEndUtc->format('Y-m-d H:i:s')
            );
        }

        $data['top_paths'] = self::queryTopBlockedPaths(
            $periodStartUtc->format('Y-m-d H:i:s'),
            $periodEndUtc->format('Y-m-d H:i:s')
        );
        $data['top_rules'] = self::queryTopBlockedRules(
            $periodStartUtc->format('Y-m-d H:i:s'),
            $periodEndUtc->format('Y-m-d H:i:s')
        );

        return $data;
    }

    public static function detectClientIp(): string
    {
        $candidates = [];
        $server = $_SERVER;
        $remoteAddr = isset($server['REMOTE_ADDR']) ? trim((string) $server['REMOTE_ADDR']) : '';
        $settings = self::getSettings();
        $trustForwarded = self::shouldTrustForwardedHeaders($remoteAddr, $settings);

        if ($trustForwarded) {
            if (!empty($server['HTTP_CF_CONNECTING_IP'])) {
                $candidates[] = (string) $server['HTTP_CF_CONNECTING_IP'];
            }

            if (!empty($server['HTTP_X_FORWARDED_FOR'])) {
                $forwarded = explode(',', (string) $server['HTTP_X_FORWARDED_FOR']);
                foreach ($forwarded as $item) {
                    $candidates[] = trim($item);
                }
            }

            if (!empty($server['HTTP_X_REAL_IP'])) {
                $candidates[] = (string) $server['HTTP_X_REAL_IP'];
            }
        }

        if ($remoteAddr !== '') {
            $candidates[] = $remoteAddr;
        }

        foreach ($candidates as $candidate) {
            $candidate = trim($candidate);
            if ($candidate === '') {
                continue;
            }

            if (filter_var($candidate, FILTER_VALIDATE_IP)) {
                return self::sanitizeIp($candidate);
            }
        }

        return '';
    }

    public static function getProxyStatus(): array
    {
        $server = $_SERVER;
        $remoteAddr = isset($server['REMOTE_ADDR']) ? trim((string) $server['REMOTE_ADDR']) : '';
        $hasForwardedHeaders = !empty($server['HTTP_CF_CONNECTING_IP'])
            || !empty($server['HTTP_X_FORWARDED_FOR'])
            || !empty($server['HTTP_X_REAL_IP']);
        $settings = self::getSettings();
        $trustedProxies = self::getTrustedProxyIps($settings);
        $trustCloudflare = !empty($settings['trust_cloudflare']);
        $isCloudflare = self::isCloudflareIp($remoteAddr);
        $trustForwarded = self::shouldTrustForwardedHeaders($remoteAddr, $settings);

        return [
            'has_forwarded_headers' => $hasForwardedHeaders,
            'remote_addr' => $remoteAddr,
            'trust_forwarded' => $trustForwarded,
            'trust_cloudflare' => $trustCloudflare ? 1 : 0,
            'is_cloudflare' => $isCloudflare ? 1 : 0,
            'trusted_proxy_count' => count($trustedProxies),
        ];
    }

    protected static function getTrustedProxyIps(?array $settings = null): array
    {
        $trusted = [];
        if ($settings === null) {
            $settings = self::getSettings();
        }

        if (defined('VULNTITAN_TRUSTED_PROXIES')) {
            $raw = VULNTITAN_TRUSTED_PROXIES;
            if (is_array($raw)) {
                $trusted = $raw;
            } elseif (is_string($raw)) {
                $trusted = array_map('trim', explode(',', $raw));
            }
        }

        if (function_exists('apply_filters')) {
            $filtered = apply_filters('vulntitan_trusted_proxies', []);
            if (is_array($filtered)) {
                $trusted = array_merge($trusted, $filtered);
            }
        }

        if (!empty($settings['trusted_proxies'])) {
            if (is_array($settings['trusted_proxies'])) {
                $trusted = array_merge($trusted, $settings['trusted_proxies']);
            } else {
                $trusted = array_merge($trusted, preg_split('/[\r\n,]+/', (string) $settings['trusted_proxies']) ?: []);
            }
        }

        return self::sanitizeTrustedProxyList($trusted);
    }

    protected static function shouldTrustForwardedHeaders(string $remoteAddr, ?array $settings = null): bool
    {
        if ($settings === null) {
            $settings = self::getSettings();
        }

        $trustedProxies = self::getTrustedProxyIps($settings);
        if (self::isTrustedProxy($remoteAddr, $trustedProxies)) {
            return true;
        }

        return !empty($settings['trust_cloudflare']) && self::isCloudflareIp($remoteAddr);
    }

    protected static function isTrustedProxy(string $remoteAddr, array $trustedProxies): bool
    {
        if ($remoteAddr === '' || !filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
            return false;
        }

        foreach ($trustedProxies as $trustedProxy) {
            $rule = self::sanitizeTrustedProxyRule((string) $trustedProxy);
            if ($rule === '') {
                continue;
            }

            if (strpos($rule, '/') !== false) {
                if (self::ipInCidr($remoteAddr, $rule)) {
                    return true;
                }

                continue;
            }

            if ($remoteAddr === $rule) {
                return true;
            }
        }

        return false;
    }

    protected static function isCloudflareIp(string $ip): bool
    {
        $ip = self::sanitizeIp($ip);
        if ($ip === '') {
            return false;
        }

        foreach (self::getCloudflareCidrs() as $cidr) {
            if (self::ipInCidr($ip, $cidr)) {
                return true;
            }
        }

        return false;
    }

    protected static function getCloudflareCidrs(): array
    {
        $cidrs = [
            '173.245.48.0/20',
            '103.21.244.0/22',
            '103.22.200.0/22',
            '103.31.4.0/22',
            '141.101.64.0/18',
            '108.162.192.0/18',
            '190.93.240.0/20',
            '188.114.96.0/20',
            '197.234.240.0/22',
            '198.41.128.0/17',
            '162.158.0.0/15',
            '104.16.0.0/13',
            '104.24.0.0/14',
            '172.64.0.0/13',
            '131.0.72.0/22',
            '2400:cb00::/32',
            '2606:4700::/32',
            '2803:f800::/32',
            '2405:b500::/32',
            '2405:8100::/32',
            '2a06:98c0::/29',
            '2c0f:f248::/32',
        ];

        if (function_exists('apply_filters')) {
            $filtered = apply_filters('vulntitan_cloudflare_cidrs', []);
            if (is_array($filtered) && $filtered) {
                $cidrs = array_merge($cidrs, $filtered);
            }
        }

        return $cidrs;
    }

    protected static function isPrivateIp(string $ip): bool
    {
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }

        return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    }

    protected static function ipInCidr(string $ip, string $cidr): bool
    {
        if ($cidr === '') {
            return false;
        }

        if (strpos($cidr, '/') === false) {
            return $ip === $cidr;
        }

        [$subnet, $mask] = explode('/', $cidr, 2);
        $mask = (int) $mask;

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
            && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
        ) {
            if ($mask < 0 || $mask > 32) {
                return false;
            }

            $ipLong = ip2long($ip);
            $subnetLong = ip2long($subnet);
            if ($ipLong === false || $subnetLong === false) {
                return false;
            }

            if ($mask === 0) {
                return true;
            }

            $maskLong = -1 << (32 - $mask);
            return (($ipLong & $maskLong) === ($subnetLong & $maskLong));
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
            && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
        ) {
            if ($mask < 0 || $mask > 128) {
                return false;
            }

            $ipBin = inet_pton($ip);
            $subnetBin = inet_pton($subnet);
            if ($ipBin === false || $subnetBin === false) {
                return false;
            }

            $bytes = intdiv($mask, 8);
            $bits = $mask % 8;

            if ($bytes > 0 && substr($ipBin, 0, $bytes) !== substr($subnetBin, 0, $bytes)) {
                return false;
            }

            if ($bits === 0) {
                return true;
            }

            $ipByte = ord($ipBin[$bytes]);
            $subnetByte = ord($subnetBin[$bytes]);
            $maskByte = (~(0xff >> $bits)) & 0xff;

            return (($ipByte & $maskByte) === ($subnetByte & $maskByte));
        }

        return false;
    }

    public static function installMuLoader(): array
    {
        $result = [
            'success' => false,
            'path' => '',
            'error' => '',
        ];

        $loaderPath = self::getMuLoaderPath();
        if ($loaderPath === '') {
            $result['error'] = 'WPMU plugin directory is not available.';
            self::storeMuStatus($result);
            return $result;
        }

        $loaderDir = dirname($loaderPath);
        if (!is_dir($loaderDir) && !wp_mkdir_p($loaderDir)) {
            $result['error'] = 'Unable to create MU plugin directory.';
            $result['path'] = self::getDisplayPath($loaderPath);
            self::storeMuStatus($result);
            return $result;
        }

        if (!is_writable($loaderDir)) {
            $result['error'] = 'MU plugin directory is not writable.';
            $result['path'] = self::getDisplayPath($loaderPath);
            self::storeMuStatus($result);
            return $result;
        }

        $content = self::buildMuLoaderContent();
        $shouldWrite = true;

        if (file_exists($loaderPath)) {
            $existing = file_get_contents($loaderPath);
            if (is_string($existing) && hash_equals(sha1($existing), sha1($content))) {
                $shouldWrite = false;
            }
        }

        if ($shouldWrite) {
            $written = file_put_contents($loaderPath, $content, LOCK_EX);
            if ($written === false) {
                $result['error'] = 'Unable to write MU loader file.';
                $result['path'] = self::getDisplayPath($loaderPath);
                self::storeMuStatus($result);
                return $result;
            }
        }

        $result['success'] = true;
        $result['path'] = self::getDisplayPath($loaderPath);
        $result['error'] = '';
        self::storeMuStatus($result);

        return $result;
    }

    public static function removeMuLoader(): void
    {
        $loaderPath = self::getMuLoaderPath();
        if ($loaderPath !== '' && file_exists($loaderPath)) {
            @unlink($loaderPath);
        }

        update_option(self::OPTION_MU_STATUS, [
            'success' => false,
            'path' => self::getDisplayPath($loaderPath),
            'error' => '',
            'updated_at' => current_time('mysql'),
        ], false);
    }

    public static function getMuLoaderStatus(): array
    {
        $stored = get_option(self::OPTION_MU_STATUS, []);
        if (!is_array($stored)) {
            $stored = [];
        }

        $loaderPath = self::getMuLoaderPath();
        $runtimePath = defined('VULNTITAN_PLUGIN_DIR')
            ? VULNTITAN_PLUGIN_DIR . '/includes/MuFirewall/Runtime.php'
            : '';

        return [
            'installed' => $loaderPath !== '' && file_exists($loaderPath),
            'path' => self::getDisplayPath($loaderPath),
            'dir_writable' => $loaderPath !== '' && is_writable(dirname($loaderPath)),
            'runtime_exists' => $runtimePath !== '' && file_exists($runtimePath),
            'last_update' => (string)($stored['updated_at'] ?? ''),
            'error' => (string)($stored['error'] ?? ''),
        ];
    }

    protected static function tableExists(): bool
    {
        global $wpdb;

        $tableName = self::getTableName();
        $like = $wpdb->esc_like($tableName);
        $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $like));

        return is_string($exists) && $exists === $tableName;
    }

    protected static function tableHasColumn(string $columnName): bool
    {
        global $wpdb;

        $tableName = self::getTableName();
        $safeColumn = preg_replace('/[^a-z0-9_]/i', '', $columnName);

        if (!is_string($safeColumn) || $safeColumn === '') {
            return false;
        }

        $result = $wpdb->get_var(
            $wpdb->prepare("SHOW COLUMNS FROM `{$tableName}` LIKE %s", $safeColumn)
        );

        return is_string($result) && $result === $safeColumn;
    }

    protected static function hasRequiredSchemaColumns(): bool
    {
        return self::tableHasColumn('event_action')
            && self::tableHasColumn('blocked')
            && self::tableHasColumn('context_json');
    }

    protected static function sanitizeSettings(array $settings): array
    {
        $defaults = self::getDefaultSettings();
        $wafWhitelistPaths = self::sanitizeWhitelistPaths($settings['waf_whitelist_paths'] ?? $defaults['waf_whitelist_paths']);
        $xmlrpcAllowlist = self::sanitizeIpList($settings['xmlrpc_allowlist_ips'] ?? $defaults['xmlrpc_allowlist_ips']);
        $twoFactorRoles = self::sanitizeRoleList(
            $settings['two_factor_roles'] ?? $defaults['two_factor_roles'],
            $defaults['two_factor_roles']
        );
        $lockoutAllowlist = self::sanitizeIpList($settings['lockout_allowlist_ips'] ?? $defaults['lockout_allowlist_ips']);
        $trustedProxies = self::sanitizeTrustedProxyList($settings['trusted_proxies'] ?? $defaults['trusted_proxies']);
        $weeklySummaryRecipient = sanitize_email((string) ($settings['weekly_summary_email_recipient'] ?? $defaults['weekly_summary_email_recipient']));

        if ($weeklySummaryRecipient === '') {
            $weeklySummaryRecipient = sanitize_email((string) $defaults['weekly_summary_email_recipient']);
        }

        $submissionMinSubmitSeconds = max(1, min(30, (int)($settings['submission_min_submit_seconds'] ?? $settings['comment_min_submit_seconds'] ?? $defaults['submission_min_submit_seconds'])));
        $submissionMaxLinks = max(0, min(20, (int)($settings['submission_max_links'] ?? $settings['comment_max_links'] ?? $defaults['submission_max_links'])));
        $submissionRateLimitAttempts = max(1, min(20, (int)($settings['submission_rate_limit_attempts'] ?? $settings['comment_rate_limit_attempts'] ?? $defaults['submission_rate_limit_attempts'])));
        $submissionRateLimitWindowMinutes = max(1, min(120, (int)($settings['submission_rate_limit_window_minutes'] ?? $settings['comment_rate_limit_window_minutes'] ?? $defaults['submission_rate_limit_window_minutes'])));
        $formShieldEnabled = !empty($settings['form_shield_enabled']) ? 1 : 0;
        $formProviderCf7Enabled = !empty($settings['form_provider_cf7_enabled']) ? 1 : 0;
        $formProviderFluentFormsEnabled = !empty($settings['form_provider_fluentforms_enabled']) ? 1 : 0;

        return [
            'enabled' => !empty($settings['enabled']) ? 1 : 0,
            'login_protection_enabled' => !empty($settings['login_protection_enabled']) ? 1 : 0,
            'custom_login_slug' => self::sanitizeCustomLoginSlug($settings['custom_login_slug'] ?? $defaults['custom_login_slug']),
            'two_factor_enabled' => !empty($settings['two_factor_enabled']) ? 1 : 0,
            'two_factor_roles' => $twoFactorRoles,
            'trusted_device_days' => max(0, min(90, (int)($settings['trusted_device_days'] ?? $defaults['trusted_device_days']))),
            'captcha_provider' => self::sanitizeCaptchaProvider($settings['captcha_provider'] ?? $defaults['captcha_provider']),
            'captcha_site_key' => self::sanitizeShortText((string) ($settings['captcha_site_key'] ?? $defaults['captcha_site_key']), 255),
            'captcha_secret_key' => self::sanitizeShortText((string) ($settings['captcha_secret_key'] ?? $defaults['captcha_secret_key']), 255),
            'captcha_login_enabled' => !empty($settings['captcha_login_enabled']) ? 1 : 0,
            'captcha_register_enabled' => !empty($settings['captcha_register_enabled']) ? 1 : 0,
            'captcha_lostpassword_enabled' => !empty($settings['captcha_lostpassword_enabled']) ? 1 : 0,
            'captcha_comment_enabled' => !empty($settings['captcha_comment_enabled']) ? 1 : 0,
            'xmlrpc_policy' => self::sanitizeXmlrpcPolicy($settings['xmlrpc_policy'] ?? $defaults['xmlrpc_policy']),
            'xmlrpc_allowlist_ips' => $xmlrpcAllowlist,
            'lockout_allowlist_ips' => $lockoutAllowlist,
            'xmlrpc_rate_limit_attempts' => max(1, min(500, (int)($settings['xmlrpc_rate_limit_attempts'] ?? $defaults['xmlrpc_rate_limit_attempts']))),
            'xmlrpc_rate_limit_window_minutes' => max(1, min(240, (int)($settings['xmlrpc_rate_limit_window_minutes'] ?? $defaults['xmlrpc_rate_limit_window_minutes']))),
            'weak_password_blocking_enabled' => !empty($settings['weak_password_blocking_enabled']) ? 1 : 0,
            'waf_sqli_enabled' => !empty($settings['waf_sqli_enabled']) ? 1 : 0,
            'waf_command_injection_enabled' => !empty($settings['waf_command_injection_enabled']) ? 1 : 0,
            'waf_whitelist_paths' => $wafWhitelistPaths,
            'learning_mode_enabled' => !empty($settings['learning_mode_enabled']) ? 1 : 0,
            'learning_suggestion_threshold' => max(2, min(20, (int)($settings['learning_suggestion_threshold'] ?? $defaults['learning_suggestion_threshold']))),
            'learning_suggestion_window_days' => max(1, min(30, (int)($settings['learning_suggestion_window_days'] ?? $defaults['learning_suggestion_window_days']))),
            'trust_cloudflare' => !empty($settings['trust_cloudflare']) ? 1 : 0,
            'trusted_proxies' => $trustedProxies,
            'max_attempts' => max(2, min(20, (int)($settings['max_attempts'] ?? $defaults['max_attempts']))),
            'lockout_minutes' => max(5, min(240, (int)($settings['lockout_minutes'] ?? $defaults['lockout_minutes']))),
            'comment_shield_enabled' => !empty($settings['comment_shield_enabled']) ? 1 : 0,
            'form_shield_enabled' => $formShieldEnabled,
            'form_provider_cf7_enabled' => $formProviderCf7Enabled,
            'form_provider_fluentforms_enabled' => $formProviderFluentFormsEnabled,
            'cf7_shield_enabled' => $formProviderCf7Enabled,
            'submission_min_submit_seconds' => $submissionMinSubmitSeconds,
            'submission_max_links' => $submissionMaxLinks,
            'submission_rate_limit_attempts' => $submissionRateLimitAttempts,
            'submission_rate_limit_window_minutes' => $submissionRateLimitWindowMinutes,
            'comment_min_submit_seconds' => $submissionMinSubmitSeconds,
            'comment_max_links' => $submissionMaxLinks,
            'comment_rate_limit_attempts' => $submissionRateLimitAttempts,
            'comment_rate_limit_window_minutes' => $submissionRateLimitWindowMinutes,
            'comment_suspicious_action' => (($settings['comment_suspicious_action'] ?? $defaults['comment_suspicious_action']) === 'block') ? 'block' : 'hold',
            'log_retention_days' => max(1, min(365, (int)($settings['log_retention_days'] ?? $defaults['log_retention_days']))),
            'weekly_summary_email_enabled' => !empty($settings['weekly_summary_email_enabled']) ? 1 : 0,
            'weekly_summary_email_recipient' => $weeklySummaryRecipient,
        ];
    }

    protected static function sanitizeCaptchaProvider($value): string
    {
        $provider = sanitize_key((string) $value);

        if (!in_array($provider, ['none', 'turnstile', 'hcaptcha'], true)) {
            return 'none';
        }

        return $provider;
    }

    protected static function sanitizeXmlrpcPolicy($value): string
    {
        $policy = sanitize_key((string) $value);

        if (!in_array($policy, ['allow', 'disable', 'rate_limit'], true)) {
            return 'allow';
        }

        return $policy;
    }

    protected static function sanitizeIpList($value): array
    {
        if (is_array($value)) {
            $rawItems = $value;
        } else {
            $rawItems = preg_split('/[\r\n,]+/', (string) $value) ?: [];
        }

        $ips = [];

        foreach ($rawItems as $item) {
            $ip = self::sanitizeIp((string) $item);
            if ($ip === '') {
                continue;
            }

            $ips[$ip] = $ip;
        }

        return array_values($ips);
    }

    protected static function sanitizeTrustedProxyList($value): array
    {
        if (is_array($value)) {
            $rawItems = $value;
        } else {
            $rawItems = preg_split('/[\r\n,]+/', (string) $value) ?: [];
        }

        $rules = [];

        foreach ($rawItems as $item) {
            $rule = self::sanitizeTrustedProxyRule((string) $item);
            if ($rule === '') {
                continue;
            }

            $rules[$rule] = $rule;
        }

        return array_values($rules);
    }

    protected static function sanitizeTrustedProxyRule(string $rule): string
    {
        $rule = trim($rule);
        if ($rule === '') {
            return '';
        }

        if (strpos($rule, '/') === false) {
            return self::sanitizeIp($rule);
        }

        [$ip, $maskRaw] = explode('/', $rule, 2);
        $ip = self::sanitizeIp($ip);
        if ($ip === '' || !preg_match('/^\d{1,3}$/', $maskRaw)) {
            return '';
        }

        $mask = (int) $maskRaw;

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            if ($mask < 0 || $mask > 32) {
                return '';
            }
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            if ($mask < 0 || $mask > 128) {
                return '';
            }
        } else {
            return '';
        }

        return $ip . '/' . $mask;
    }

    protected static function sanitizeRoleList($value, array $fallback): array
    {
        $roles = self::normalizeRoleList($value);

        if (!$roles) {
            $roles = self::normalizeRoleList($fallback);
        }

        return $roles;
    }

    protected static function normalizeRoleList($value): array
    {
        if (is_array($value)) {
            $rawItems = $value;
        } else {
            $rawItems = preg_split('/[\r\n,]+/', (string) $value) ?: [];
        }

        $knownRoles = self::getKnownRoles();
        $roles = [];

        foreach ($rawItems as $item) {
            $role = sanitize_key((string) $item);
            if ($role === '') {
                continue;
            }

            if ($knownRoles && !in_array($role, $knownRoles, true)) {
                continue;
            }

            $roles[$role] = $role;
        }

        return array_values($roles);
    }

    protected static function getKnownRoles(): array
    {
        if (!function_exists('wp_roles')) {
            return [];
        }

        $wpRoles = wp_roles();
        $roles = $wpRoles ? $wpRoles->roles : [];

        return is_array($roles) ? array_keys($roles) : [];
    }

    protected static function emptyDigestCounts(): array
    {
        return [
            'total_events' => 0,
            'blocked' => 0,
            'forms_blocked' => 0,
            'login_failed' => 0,
            'login_blocked' => 0,
            'login_lockout' => 0,
            'login_recovered' => 0,
            'sqli' => 0,
            'command_injection' => 0,
            'path_traversal' => 0,
            'sensitive_file_probe' => 0,
            'comment_spam_blocked' => 0,
            'comment_spam_held' => 0,
            'comment_pending_review' => 0,
            'comment_rate_limited' => 0,
            'unique_attackers' => 0,
        ];
    }

    protected static function queryDigestCountsForWindow(string $startGmt, string $endGmt): array
    {
        global $wpdb;

        $tableName = self::getTableName();
        $blockedCondition = self::getBlockedSqlCondition();

        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT
                    COUNT(*) AS total_events,
                    SUM(CASE WHEN {$blockedCondition} THEN 1 ELSE 0 END) AS blocked,
                    SUM(CASE WHEN event_type = 'request_blocked' AND event_source IN ('contact_form_7', 'fluent_forms') THEN 1 ELSE 0 END) AS forms_blocked,
                    SUM(CASE WHEN event_type = 'login_failed' THEN 1 ELSE 0 END) AS login_failed,
                    SUM(CASE WHEN event_type = 'login_blocked' THEN 1 ELSE 0 END) AS login_blocked,
                    SUM(CASE WHEN event_type = 'login_lockout' THEN 1 ELSE 0 END) AS login_lockout,
                    SUM(CASE WHEN event_type = 'login_recovered' THEN 1 ELSE 0 END) AS login_recovered,
                    SUM(CASE WHEN event_type = 'request_blocked' AND rule_group = 'waf_sqli' THEN 1 ELSE 0 END) AS sqli,
                    SUM(CASE WHEN event_type = 'request_blocked' AND rule_group = 'waf_command_injection' THEN 1 ELSE 0 END) AS command_injection,
                    SUM(CASE WHEN event_type = 'request_blocked' AND rule_id = 'path_traversal' THEN 1 ELSE 0 END) AS path_traversal,
                    SUM(CASE WHEN event_type = 'request_blocked' AND rule_id = 'sensitive_file_probe' THEN 1 ELSE 0 END) AS sensitive_file_probe,
                    SUM(CASE WHEN event_type = 'comment_spam_blocked' THEN 1 ELSE 0 END) AS comment_spam_blocked,
                    SUM(CASE WHEN event_type = 'comment_spam_held' THEN 1 ELSE 0 END) AS comment_spam_held,
                    SUM(CASE WHEN event_type = 'comment_pending_review' THEN 1 ELSE 0 END) AS comment_pending_review,
                    SUM(CASE WHEN event_type = 'comment_rate_limited' THEN 1 ELSE 0 END) AS comment_rate_limited,
                    COUNT(DISTINCT CASE WHEN {$blockedCondition} AND ip_hash <> '' THEN ip_hash ELSE NULL END) AS unique_attackers
                 FROM {$tableName}
                 WHERE created_at >= %s AND created_at < %s",
                $startGmt,
                $endGmt
            ),
            ARRAY_A
        );

        $defaults = self::emptyDigestCounts();

        if (!is_array($row)) {
            return $defaults;
        }

        foreach ($defaults as $key => $value) {
            $defaults[$key] = (int) ($row[$key] ?? 0);
        }

        return $defaults;
    }

    protected static function queryTopBlockedPaths(string $startGmt, string $endGmt, int $limit = 5): array
    {
        global $wpdb;

        $tableName = self::getTableName();
        $safeLimit = max(1, min(10, $limit));
        $blockedCondition = self::getBlockedSqlCondition();

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT request_path, COUNT(*) AS hits
                 FROM {$tableName}
                 WHERE created_at >= %s
                   AND created_at < %s
                   AND {$blockedCondition}
                   AND request_path <> ''
                 GROUP BY request_path
                 ORDER BY hits DESC, request_path ASC
                 LIMIT {$safeLimit}",
                $startGmt,
                $endGmt
            ),
            ARRAY_A
        );

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

        return array_map(static function (array $row): array {
            return [
                'path' => (string) ($row['request_path'] ?? ''),
                'hits' => (int) ($row['hits'] ?? 0),
            ];
        }, $rows);
    }

    protected static function queryTopBlockedRules(string $startGmt, string $endGmt, int $limit = 5): array
    {
        global $wpdb;

        $tableName = self::getTableName();
        $safeLimit = max(1, min(10, $limit));
        $blockedCondition = self::getBlockedSqlCondition();

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT rule_group, rule_id, matched_pattern, COUNT(*) AS hits
                 FROM {$tableName}
                 WHERE created_at >= %s
                   AND created_at < %s
                   AND {$blockedCondition}
                   AND (rule_group <> '' OR rule_id <> '' OR matched_pattern <> '')
                 GROUP BY rule_group, rule_id, matched_pattern
                 ORDER BY hits DESC, rule_group ASC, rule_id ASC
                 LIMIT {$safeLimit}",
                $startGmt,
                $endGmt
            ),
            ARRAY_A
        );

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

        return array_map(static function (array $row): array {
            return [
                'rule_group' => (string) ($row['rule_group'] ?? ''),
                'rule_id' => (string) ($row['rule_id'] ?? ''),
                'matched_pattern' => (string) ($row['matched_pattern'] ?? ''),
                'hits' => (int) ($row['hits'] ?? 0),
            ];
        }, $rows);
    }

    protected static function getBlockedSqlCondition(): string
    {
        return "(blocked = 1 OR event_type IN ('request_blocked', 'login_blocked', 'login_lockout', 'comment_spam_blocked', 'comment_rate_limited'))";
    }

    protected static function getWpTimezone(): \DateTimeZone
    {
        if (function_exists('wp_timezone')) {
            return wp_timezone();
        }

        $timezoneString = (string) get_option('timezone_string', 'UTC');
        if ($timezoneString !== '') {
            try {
                return new \DateTimeZone($timezoneString);
            } catch (\Exception $exception) {
                // Fall through to UTC.
            }
        }

        return new \DateTimeZone('UTC');
    }

    protected static function formatLocalTimestamp(int $timestamp, string $format): string
    {
        if (function_exists('wp_date')) {
            return wp_date($format, $timestamp, self::getWpTimezone());
        }

        return date_i18n($format, $timestamp, false);
    }

    public static function sanitizeCustomLoginSlug($input): string
    {
        if (!is_scalar($input)) {
            return '';
        }

        $value = trim((string) $input);
        if ($value === '') {
            return '';
        }

        $value = preg_replace('/[\?#].*$/', '', $value);
        $value = str_replace('\\', '/', (string) $value);
        $value = preg_replace('#/+#', '/', (string) $value);
        $value = trim((string) $value, "/ \t\n\r\0\x0B.");

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

        $segments = explode('/', $value);
        $normalized = [];

        foreach ($segments as $segment) {
            $segment = sanitize_title_with_dashes((string) $segment, '', 'save');
            $segment = trim($segment, '-');

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

            $normalized[] = substr($segment, 0, 40);

            if (count($normalized) >= 4) {
                break;
            }
        }

        if (!$normalized) {
            return '';
        }

        $slug = implode('/', $normalized);
        $slug = substr($slug, 0, 90);
        $slug = trim($slug, '/');

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

        $reservedPaths = [
            'admin',
            'admin-ajax.php',
            'admin-post.php',
            'feed',
            'index.php',
            'login',
            'robots.txt',
            'sitemap.xml',
            'wp-admin',
            'wp-json',
            'wp-login',
            'wp-login.php',
            'xmlrpc.php',
        ];

        if (in_array($slug, $reservedPaths, true)) {
            return '';
        }

        if (preg_match('#(^|/)(?:wp-admin|wp-login(?:\.php)?|wp-json|xmlrpc(?:\.php)?|admin-ajax(?:\.php)?|admin-post(?:\.php)?)(/|$)#', $slug)) {
            return '';
        }

        return $slug;
    }

    protected static function sanitizeWhitelistPaths($input): array
    {
        $items = [];

        if (is_string($input)) {
            $items = preg_split('/\r\n|\r|\n|,/', $input) ?: [];
        } elseif (is_array($input)) {
            $items = $input;
        } elseif (is_scalar($input)) {
            $items = [(string) $input];
        }

        $normalized = [];

        foreach ($items as $item) {
            if (!is_scalar($item)) {
                continue;
            }

            $value = (string) $item;
            $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
            $value = trim((string) $value);

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

            $value = substr($value, 0, 190);
            $normalized[$value] = true;

            if (count($normalized) >= 80) {
                break;
            }
        }

        return array_keys($normalized);
    }

    protected static function customLoginSlugConflicts(string $slug): bool
    {
        if ($slug === '') {
            return false;
        }

        if (function_exists('url_to_postid')) {
            $postId = url_to_postid(home_url('/' . user_trailingslashit($slug)));
            if ($postId > 0) {
                return true;
            }
        }

        if (function_exists('get_page_by_path')) {
            $page = get_page_by_path($slug, OBJECT, ['page']);
            if ($page instanceof \WP_Post) {
                return true;
            }
        }

        return false;
    }

    protected static function sanitizeEventType(string $eventType): string
    {
        $normalized = strtolower(trim($eventType));
        $normalized = preg_replace('/[^a-z0-9_\-]/', '', $normalized);

        return $normalized !== '' ? substr($normalized, 0, 64) : 'unknown';
    }

    protected static function sanitizeEventAction(string $eventAction): string
    {
        $normalized = strtolower(trim($eventAction));
        $normalized = preg_replace('/[^a-z0-9_\-]/', '', $normalized);

        if ($normalized === '') {
            return 'log';
        }

        if (!in_array($normalized, ['allow', 'block', 'challenge', 'log'], true)) {
            return 'log';
        }

        return substr($normalized, 0, 16);
    }

    protected static function sanitizeRequestUri(string $uri): string
    {
        $uri = trim($uri);
        if ($uri === '') {
            return '';
        }

        return substr($uri, 0, 1900);
    }

    protected static function sanitizeRequestPath(string $path): string
    {
        $path = trim($path);
        if ($path === '') {
            return '';
        }

        if (strpos($path, '/') !== 0) {
            $path = '/' . ltrim($path, '/');
        }

        return substr($path, 0, 191);
    }

    protected static function sanitizeRequestScheme(string $scheme): string
    {
        $scheme = strtolower(trim($scheme));
        $scheme = preg_replace('/[^a-z0-9\+\.\-]/', '', $scheme);

        if ($scheme === '') {
            return 'http';
        }

        return substr($scheme, 0, 16);
    }

    protected static function sanitizeRequestHost(string $host): string
    {
        $host = strtolower(trim($host));
        if ($host === '') {
            return '';
        }

        $host = preg_replace('/:\d+$/', '', $host);
        $host = preg_replace('/[^a-z0-9\.\-\[\]:]/', '', $host);

        return substr((string) $host, 0, 191);
    }

    protected static function sanitizeForwardedFor(string $forwardedFor): string
    {
        $forwardedFor = trim($forwardedFor);
        if ($forwardedFor === '') {
            return '';
        }

        $parts = explode(',', $forwardedFor);
        $sanitized = [];

        foreach ($parts as $part) {
            $ip = self::sanitizeIp((string) $part);
            if ($ip === '') {
                continue;
            }
            $sanitized[] = $ip;
        }

        if (!$sanitized) {
            return '';
        }

        return substr(implode(',', $sanitized), 0, 255);
    }

    protected static function sanitizeCountryCode(string $countryCode): string
    {
        $countryCode = strtoupper(trim($countryCode));
        $countryCode = preg_replace('/[^A-Z]/', '', $countryCode);

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

        return substr($countryCode, 0, 2);
    }

    protected static function sanitizeRequestId(string $requestId): string
    {
        $requestId = trim($requestId);
        if ($requestId === '') {
            return '';
        }

        $requestId = preg_replace('/[^a-zA-Z0-9\:\._\-]/', '', $requestId);

        return substr((string) $requestId, 0, 64);
    }

    protected static function sanitizeShortText(string $value, int $maxLength = 255): string
    {
        $value = sanitize_text_field($value);

        return substr($value, 0, max(1, $maxLength));
    }

    protected static function sanitizeUuid(string $uuid): string
    {
        $uuid = strtolower(trim($uuid));
        $uuid = preg_replace('/[^a-f0-9\-]/', '', $uuid);

        if ($uuid === '') {
            return self::generateEventUuid();
        }

        return substr((string) $uuid, 0, 36);
    }

    protected static function sanitizeSeverity($severity): int
    {
        $level = (int) $severity;

        return max(1, min(5, $level));
    }

    protected static function sanitizeResponseCode($responseCode): int
    {
        $code = (int) $responseCode;
        if ($code < 0 || $code > 599) {
            return 0;
        }

        return $code;
    }

    protected static function sanitizeMysqlDatetime(string $dateTime): string
    {
        if ($dateTime === '') {
            return gmdate('Y-m-d H:i:s');
        }

        if (!preg_match('/^\d{4}\-\d{2}\-\d{2}\s\d{2}\:\d{2}\:\d{2}$/', $dateTime)) {
            return gmdate('Y-m-d H:i:s');
        }

        return $dateTime;
    }

    protected static function sanitizeIp(string $ip): string
    {
        $ip = trim($ip);
        if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
            return '';
        }

        return $ip;
    }

    protected static function extractRequestPath(string $requestUri): string
    {
        if ($requestUri === '') {
            return '';
        }

        $path = parse_url($requestUri, PHP_URL_PATH);
        if (!is_string($path)) {
            return '';
        }

        return $path;
    }

    protected static function extractQueryString(string $requestUri): string
    {
        if ($requestUri === '') {
            return '';
        }

        $query = parse_url($requestUri, PHP_URL_QUERY);
        if (!is_string($query)) {
            return '';
        }

        return $query;
    }

    protected static function inferEventAction(string $eventType): string
    {
        if (self::isBlockedEventType($eventType)) {
            return 'block';
        }

        if ($eventType === 'login_recovered') {
            return 'allow';
        }

        return 'log';
    }

    protected static function inferSeverity(string $eventType): int
    {
        switch ($eventType) {
            case 'request_blocked':
            case 'login_lockout':
                return 4;
            case 'login_blocked':
            case 'login_failed':
                return 3;
            case 'login_recovered':
                return 1;
            default:
                return 2;
        }
    }

    protected static function inferResponseCode(string $eventType, int $blocked): int
    {
        if ($eventType === 'request_blocked') {
            return 403;
        }

        if ($eventType === 'login_blocked' || $eventType === 'login_lockout') {
            return 429;
        }

        if ($blocked === 1) {
            return 403;
        }

        return 0;
    }

    protected static function isBlockedEventType(string $eventType): bool
    {
        return in_array($eventType, self::BLOCKED_EVENT_TYPES, true);
    }

    protected static function normalizeFlag($value): int
    {
        return !empty($value) ? 1 : 0;
    }

    protected static function isLoggedIn(): bool
    {
        return function_exists('is_user_logged_in') && is_user_logged_in();
    }

    protected static function detectCurrentUserId(): int
    {
        if (!function_exists('get_current_user_id')) {
            return 0;
        }

        return max(0, (int) get_current_user_id());
    }

    protected static function detectCurrentUsername(int $userId): string
    {
        if ($userId <= 0) {
            return '';
        }

        if (function_exists('get_userdata')) {
            $userData = get_userdata($userId);
            if (is_object($userData) && isset($userData->user_login)) {
                return (string) $userData->user_login;
            }
        }

        if (!function_exists('wp_get_current_user')) {
            return '';
        }

        $user = wp_get_current_user();
        if (!is_object($user) || !isset($user->user_login)) {
            return '';
        }

        return (string) $user->user_login;
    }

    protected static function detectRequestScheme(array $server): string
    {
        if (function_exists('is_ssl') && is_ssl()) {
            return 'https';
        }

        $https = strtolower((string)($server['HTTPS'] ?? ''));
        if ($https !== '' && $https !== 'off' && $https !== '0') {
            return 'https';
        }

        $port = (string)($server['SERVER_PORT'] ?? '');
        if ($port === '443') {
            return 'https';
        }

        return 'http';
    }

    protected static function detectRequestHost(array $server): string
    {
        if (!empty($server['HTTP_HOST'])) {
            return (string) $server['HTTP_HOST'];
        }

        if (!empty($server['SERVER_NAME'])) {
            return (string) $server['SERVER_NAME'];
        }

        return '';
    }

    protected static function detectRequestId(array $server): string
    {
        $keys = [
            'HTTP_X_REQUEST_ID',
            'HTTP_X_CORRELATION_ID',
            'HTTP_CF_RAY',
        ];

        foreach ($keys as $key) {
            if (!empty($server[$key])) {
                return (string) $server[$key];
            }
        }

        return '';
    }

    protected static function detectCountryCode(array $server): string
    {
        if (!empty($server['HTTP_CF_IPCOUNTRY'])) {
            return (string) $server['HTTP_CF_IPCOUNTRY'];
        }

        if (!empty($server['GEOIP_COUNTRY_CODE'])) {
            return (string) $server['GEOIP_COUNTRY_CODE'];
        }

        return '';
    }

    protected static function detectSessionId(): string
    {
        if (!function_exists('session_status') || !function_exists('session_id')) {
            return '';
        }

        if (session_status() !== PHP_SESSION_ACTIVE) {
            return '';
        }

        return (string) session_id();
    }

    protected static function detectIsAjaxRequest(array $server): bool
    {
        if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
            return true;
        }

        $header = strtolower((string)($server['HTTP_X_REQUESTED_WITH'] ?? ''));

        return $header === 'xmlhttprequest';
    }

    protected static function detectIsRestRequest(string $requestPath): bool
    {
        if (defined('REST_REQUEST') && REST_REQUEST) {
            return true;
        }

        return strpos($requestPath, '/wp-json') === 0;
    }

    protected static function detectIsXmlRpcRequest(string $requestPath): bool
    {
        return $requestPath === '/xmlrpc.php' || strpos($requestPath, '/xmlrpc.php') === 0;
    }

    protected static function detectIsWpLoginRequest(string $requestPath): bool
    {
        return $requestPath === '/wp-login.php' || strpos($requestPath, '/wp-login.php') === 0;
    }

    protected static function generateEventUuid(): string
    {
        if (function_exists('wp_generate_uuid4')) {
            return (string) wp_generate_uuid4();
        }

        $random = uniqid('evt_', true);

        return substr(md5($random), 0, 8)
            . '-' . substr(md5($random), 8, 4)
            . '-' . substr(md5($random), 12, 4)
            . '-' . substr(md5($random), 16, 4)
            . '-' . substr(md5($random), 20, 12);
    }

    protected static function buildIpHash(string $ipAddress): string
    {
        if ($ipAddress === '') {
            return '';
        }

        $salt = function_exists('wp_salt') ? (string) wp_salt('auth') : '';

        return hash('sha256', $ipAddress . '|' . $salt);
    }

    protected static function decodeDetails(string $details): array
    {
        if ($details === '') {
            return [];
        }

        $decoded = json_decode($details, true);
        if (!is_array($decoded)) {
            return [];
        }

        return $decoded;
    }

    protected static function toLocalDate(string $gmtDate): string
    {
        if ($gmtDate === '') {
            return '';
        }

        return get_date_from_gmt($gmtDate, 'Y-m-d H:i:s');
    }

    protected static function getMuLoaderPath(): string
    {
        if (!defined('WPMU_PLUGIN_DIR')) {
            return '';
        }

        return untrailingslashit(WPMU_PLUGIN_DIR) . '/' . self::MU_LOADER_FILENAME;
    }

    protected static function getDisplayPath(string $path): string
    {
        $normalizedPath = trim($path);
        if ($normalizedPath === '') {
            return '';
        }

        $normalizedPath = wp_normalize_path($normalizedPath);

        if (defined('ABSPATH')) {
            $rootPath = trailingslashit(wp_normalize_path(ABSPATH));
            if (strpos($normalizedPath, $rootPath) === 0) {
                return ltrim(substr($normalizedPath, strlen($rootPath)), '/');
            }
        }

        if (defined('WP_CONTENT_DIR')) {
            $contentPath = trailingslashit(wp_normalize_path(WP_CONTENT_DIR));
            if (strpos($normalizedPath, $contentPath) === 0) {
                return 'wp-content/' . ltrim(substr($normalizedPath, strlen($contentPath)), '/');
            }
        }

        if (defined('WPMU_PLUGIN_DIR')) {
            $muPath = trailingslashit(wp_normalize_path(WPMU_PLUGIN_DIR));
            if (strpos($normalizedPath, $muPath) === 0) {
                return 'wp-content/mu-plugins/' . ltrim(substr($normalizedPath, strlen($muPath)), '/');
            }
        }

        return basename($normalizedPath);
    }

    protected static function buildMuLoaderContent(): string
    {
        $runtimePath = defined('VULNTITAN_PLUGIN_DIR')
            ? str_replace('\\', '/', VULNTITAN_PLUGIN_DIR . '/includes/MuFirewall/Runtime.php')
            : '';
        $exportedRuntimePath = var_export($runtimePath, true);

        return "<?php\n"
            . "/**\n"
            . " * Plugin Name: VulnTitan Firewall (MU Loader)\n"
            . " * Description: Auto-generated loader for VulnTitan firewall runtime.\n"
            . " */\n\n"
            . "defined('ABSPATH') || exit;\n\n"
            . "\$runtimeFile = {$exportedRuntimePath};\n"
            . "if (is_string(\$runtimeFile) && \$runtimeFile !== '' && file_exists(\$runtimeFile)) {\n"
            . "    require_once \$runtimeFile;\n"
            . "    if (class_exists('\\\\VulnTitan\\\\MuFirewall\\\\Runtime')) {\n"
            . "        \\VulnTitan\\MuFirewall\\Runtime::boot();\n"
            . "    }\n"
            . "}\n";
    }

    protected static function storeMuStatus(array $status): void
    {
        update_option(self::OPTION_MU_STATUS, [
            'success' => !empty($status['success']),
            'path' => self::getDisplayPath((string)($status['path'] ?? '')),
            'error' => (string)($status['error'] ?? ''),
            'updated_at' => current_time('mysql'),
        ], false);
    }
}
