<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }

class Vulnity_Suspicious_Query_Alert extends Vulnity_Alert_Base {

    const FLOOD_COOLDOWN_INDEX = 'vulnity_flood_cooldown_index';
    const ATTACK_COUNTER_INDEX = 'vulnity_attack_counter_index';
    const FLOOD_BLOCK_INDEX    = 'vulnity_flood_block_index';

    // Flood detection configuration
    private $flood_threshold = 5;           // Number of queries to trigger flood alert
    private $flood_time_window = 30;        // Time window in seconds
    private $flood_cooldown = 300;          // 5 minutes cooldown for alerts after flood detected
    
    private $sql_injection_patterns = array(
        // Basic SQL Injection patterns
        'union\s+select',
        'union\s+all\s+select',
        'select.*from.*information_schema',
        'select.*from.*mysql\.',
        'select.*from.*sys\.',
        'select.*from.*performance_schema',
        '\bor\s+1\s*=\s*1',
        '\band\s+1\s*=\s*1',
        '\bor\s+\'1\'\s*=\s*\'1\'',
        '\band\s+\'1\'\s*=\s*\'1\'',
        '\bor\s+1\s*--',
        '\bor\s+\'1\'\s*--',
        'waitfor\s+delay',
        'benchmark\s*\(',
        'sleep\s*\(',
        'pg_sleep',
        'dbms_pipe\.receive_message',
        'extractvalue\s*\(',
        'updatexml\s*\(',
        'xmltype\s*\(',
        'row_count\s*\(',
        'load_file\s*\(',
        'into\s+outfile',
        'into\s+dumpfile',
        'group\s+by\s+having',
        'group\s+by\s+.*\s+having\s+1\s*=\s*1',
        '@@version',
        '@@datadir',
        '@@hostname',
        'database\s*\(\s*\)',
        'schema\s*\(\s*\)',
        'user\s*\(\s*\)',
        '\bcurrent_user\b',
        '\bsystem_user\b',
        'concat\s*\(.*char\s*\(',
        'chr\s*\(.*\|\|.*chr\s*\(',
        'exp\s*\(.*~\s*\(',
        'gtid_subset\s*\(',
        'json_keys\s*\(',
        'uuid_to_bin\s*\('
    );
    
    private $code_execution_patterns = array(
        // PHP code execution
        'eval\s*\(',
        'assert\s*\(',
        'system\s*\(',
        'exec\s*\(',
        'shell_exec\s*\(',
        'passthru\s*\(',
        'proc_open\s*\(',
        'popen\s*\(',
        'pcntl_exec',
        'expect_popen',
        'ssh2_exec',
        // PHP dangerous functions
        'create_function\s*\(',
        'include\s*\(',
        'include_once\s*\(',
        'require\s*\(',
        'require_once\s*\(',
        'file_get_contents\s*\(',
        'file_put_contents\s*\(',
        'fopen\s*\(',
        'fwrite\s*\(',
        'fputs\s*\(',
        'fpassthru\s*\(',
        'readfile\s*\(',
        'show_source\s*\(',
        'highlight_file\s*\(',
        'parse_ini_file\s*\(',
        'base64_decode\s*\(',
        'gzinflate\s*\(',
        'gzuncompress\s*\(',
        'str_rot13\s*\(',
        'unserialize\s*\(',
        'register_shutdown_function\s*\(',
        'register_tick_function\s*\(',
        '\$\{.*\}',
        '\$\$\w+'
    );
    
    private $xss_patterns = array(
        // XSS patterns
        '<script[^>]*>.*?<\/script',
        'javascript:',
        'javascript\s*:',
        'vbscript:',
        'vbscript\s*:',
        'onload\s*=',
        'onerror\s*=',
        'onclick\s*=',
        'onmouseover\s*=',
        'onmouseout\s*=',
        'onmousemove\s*=',
        'onkeypress\s*=',
        'onkeydown\s*=',
        'onkeyup\s*=',
        'onfocus\s*=',
        'onblur\s*=',
        'onchange\s*=',
        'onsubmit\s*=',
        'ondblclick\s*=',
        'onreset\s*=',
        'onselect\s*=',
        'oncopy\s*=',
        'oncut\s*=',
        'onpaste\s*=',
        'onabort\s*=',
        'ondrag\s*=',
        'ondragend\s*=',
        'ondragenter\s*=',
        'ondragleave\s*=',
        'ondragover\s*=',
        'ondragstart\s*=',
        'ondrop\s*=',
        '<iframe[^>]*>',
        '<embed[^>]*>',
        '<object[^>]*>',
        '<applet[^>]*>',
        '<meta[^>]*http-equiv',
        '<img[^>]*src[^>]*=',
        '<svg[^>]*onload',
        'expression\s*\(',
        'import\s*\(',
        'document\.cookie',
        'document\.write',
        'window\.location',
        'document\.location',
        'alert\s*\(',
        'confirm\s*\(',
        'prompt\s*\(',
        'String\.fromCharCode'
    );
    
    private $path_traversal_patterns = array(
        // Path traversal patterns
        '\.\.\/',
        '\.\.\\\\',
        '\.\.\%2f',
        '\.\.\%5c',
        '\%2e\%2e\%2f',
        '\%2e\%2e\%5c',
        '\.\.\/\.\.\/',
        '\.\.\\\\\.\.\\\\',
        '\/etc\/passwd',
        '\/etc\/shadow',
        '\/etc\/hosts',
        '\/etc\/group',
        '\/etc\/gshadow',
        '\/etc\/motd',
        '\/etc\/issue',
        '\/etc\/bashrc',
        '\/etc\/profile',
        '\/proc\/self\/environ',
        '\/proc\/version',
        '\/proc\/cmdline',
        '\/proc\/mounts',
        '\/proc\/config\.gz',
        'c:\\\\windows\\\\system32',
        'c:\\\\windows\\\\win\.ini',
        'c:\\\\windows\\\\system\.ini',
        'c:\\\\boot\.ini',
        'c:\\\\inetpub\\\\',
        'c:\\\\winnt\\\\',
        '\.htaccess',
        '\.htpasswd',
        'wp-config\.php',
        'configuration\.php',
        'config\.inc\.php',
        'settings\.php',
        'database\.php',
        'db\.php',
        'setup\.php',
        'phpinfo\.php',
        '\.env',
        '\.git',
        '\.svn',
        'web\.config',
        'applicationhost\.config'
    );
    
    private $ldap_injection_patterns = array(
        // LDAP injection patterns
        '\(\|\(',
        '\(\&\(',
        '\*\|',
        '\|\*',
        '\*\%00',
        '\(\!\(',
        'objectclass\=\*',
        'objectcategory\=\*',
        '\(\&\(objectclass\=',
        '\|\(uid\=',
        '\|\(cn\=',
        '\|\(sn\=',
        '\|\(givenname\=',
        '\|\(mail\=',
        '\)\(\|',
        '\)\(\&',
        'ldap:\/\/',
        'ldaps:\/\/',
        '\*\)\(\|\(password',
        '\*\)\(\|\(userpassword',
        '\)\(\|\(objectclass\=\*',
        '\x00',
        '\x0a',
        '\x0d',
        '\x1a',
        '\x20\)\(',
        'admincount\=',
        'useraccountcontrol\=',
        'primarygroupid\=',
        'objectsid\=',
        'samaccountname\='
    );
    
    private $xxe_patterns = array(
        // XXE injection patterns
        '<!DOCTYPE[^>]*>',
        '<!ENTITY[^>]*>',
        '<!ELEMENT[^>]*>',
        '<!ATTLIST[^>]*>',
        'SYSTEM\s+["\']file:',
        'SYSTEM\s+["\']php:',
        'SYSTEM\s+["\']expect:',
        'SYSTEM\s+["\']http:',
        'SYSTEM\s+["\']https:',
        'SYSTEM\s+["\']ftp:',
        'SYSTEM\s+["\']gopher:',
        'SYSTEM\s+["\']data:',
        'PUBLIC\s+["\'][^"\']*["\']',
        '\&xxe;', // Specific entity name for XXE. Broad entity matches were removed to avoid false positives on normal HTML entities.
        '<\?xml[^>]*>',
        'xmlns:xi\=',
        '<xi:include',
        'href\=["\']file:',
        'href\=["\']php:',
        'href\=["\']expect:',
        'href\=["\']http:',
        'parse\=["\']xml',
        'CDATA\[',
        '\]\]>',
        'file_get_contents\(["\']php:\/\/input',
        'simplexml_load_string',
        'DOMDocument',
        'loadXML',
        'XMLReader',
        'xml_parse'
    );
    
    public function __construct() {
        $this->alert_type = 'suspicious_query';
        parent::__construct();
    }
    
    protected function register_hooks() {
        add_action('init', array($this, 'check_request'), 1);
        
        // Clean up old data periodically
        add_action('vulnity_cleanup_flood_data', array($this, 'cleanup_flood_data'));
        if (!wp_next_scheduled('vulnity_cleanup_flood_data')) {
            wp_schedule_event(time(), 'hourly', 'vulnity_cleanup_flood_data');
        }
    }
    
    /**
     * Check if IP is in alert cooldown (not used for blocking, kept for future use)
     */
    public function check_ip_block() {
        // This method is kept for compatibility but not used for blocking
        // We only use cooldowns for alerts, not for blocking requests
    }
    
    public function check_request() {
        $nonce_valid = $this->is_request_nonce_valid('vulnity_suspicious_query');
        $allow_public_scan = $this->is_public_scan_allowed();
        $public_nonce_valid = $allow_public_scan ? $this->is_public_scan_nonce_valid() : false;
        $trusted_context = $nonce_valid || $public_nonce_valid;

        // Always inspect GET/POST/server data to catch live probing. Cookies are only
        // read in trusted contexts (nonce-validated admin or approved public scans)
        // to avoid collecting authentication data from untrusted traffic.
        // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
        $request_keys = $this->get_monitored_request_keys();
        $cookie_names = $this->get_cookie_names_to_monitor();

        $this->evaluate(array(
            'get' => $this->get_monitored_input_values(INPUT_GET, $request_keys),
            'post' => $this->get_monitored_input_values(INPUT_POST, $request_keys),
            'cookie' => $trusted_context ? $this->get_monitored_cookie_values($cookie_names) : array(),
            'server' => $this->get_monitored_server_values()
        ));
        // phpcs:enable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing
    }

    /**
     * Validate request nonce when present to ensure authenticated contexts are protected.
     */
    private function is_request_nonce_valid($action) {
        $nonce = '';

        if (isset($_REQUEST['_wpnonce'])) {
            $nonce = sanitize_text_field(wp_unslash($_REQUEST['_wpnonce']));
        }

        return !empty($nonce) && function_exists('wp_verify_nonce') && wp_verify_nonce($nonce, $action);
    }

    /**
     * Determine if public scanning is enabled via plugin settings or filters.
     *
     * @return bool
     */
    private function is_public_scan_allowed() {
        $settings = get_option('vulnity_alert_settings', array());
        $allow_public_scan = isset($settings['allow_public_scan']) ? (bool) $settings['allow_public_scan'] : false;

        return (bool) apply_filters('vulnity_allow_public_scan', $allow_public_scan);
    }

    /**
     * Validate public scan nonce when public scanning is enabled.
     *
     * @return bool
     */
    private function is_public_scan_nonce_valid() {
        $nonce = vulnity_get_server_var('HTTP_X_VULNITY_PUBLIC_SCAN_NONCE');

        if (empty($nonce) && isset($_REQUEST['vulnity_public_scan_nonce'])) {
            $nonce = sanitize_text_field(wp_unslash($_REQUEST['vulnity_public_scan_nonce']));
        }

        return !empty($nonce) && function_exists('wp_verify_nonce') && wp_verify_nonce($nonce, 'vulnity_public_scan');
    }

    /**
     * Ensure the current user can trigger admin-only scans.
     *
     * @return bool
     */
    private function current_user_can_scan() {
        return function_exists('current_user_can') && current_user_can('manage_options');
    }

    /**
     * Keys of request data that should be inspected for suspicious patterns.
     *
     * @return array
     */
    private function get_monitored_request_keys() {
        return array(
            's', 'search', 'q', 'query', 'cmd', 'command', 'exec', 'execute', 'id', 'file', 'path', 'url', 'redirect', 'action', 'page', 'p'
        );
    }

    /**
     * Retrieve sanitized values for a limited set of request parameters.
     *
     * @param int   $input_type Input type constant (e.g. INPUT_GET).
     * @param array $keys       Keys to inspect.
     *
     * @return array Sanitized array data for monitored keys only.
     */
    private function get_monitored_input_values($input_type, $keys) {
        $captured = array();

        foreach ($keys as $key) {
            $sanitized_key = sanitize_key($key);

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

            $filtered_value = filter_input($input_type, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);

            if (is_array($filtered_value)) {
                $sanitized_array = $this->sanitize_input_array($filtered_value);

                if (!empty($sanitized_array)) {
                    $captured[$sanitized_key] = $sanitized_array;
                }
            } else {
                $filtered_scalar = filter_input($input_type, $key, FILTER_UNSAFE_RAW);

                if (is_string($filtered_scalar) && '' !== $filtered_scalar) {
                    $captured[$sanitized_key] = $this->prepare_value_for_analysis($filtered_scalar);
                }
            }
        }

        return $captured;
    }

    /**
     * Retrieve sanitized values for authentication-related cookies only.
     *
     * @return array Sanitized cookie subset.
     */
    private function get_monitored_cookie_values($cookie_names) {
        $captured = array();
        $max_length = 4096;

        foreach ($cookie_names as $name) {
            $sanitized_name = sanitize_key($name);

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

            $filtered_value = filter_input(INPUT_COOKIE, $name, FILTER_UNSAFE_RAW);

            if (null === $filtered_value) {
                $cookie_raw = filter_input(INPUT_COOKIE, $name, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);

                if (null !== $cookie_raw) {
                    $raw = wp_unslash($cookie_raw);
                    $filtered_value = $raw;
                }
            }

            if (is_array($filtered_value)) {
                $captured[$sanitized_name] = $this->sanitize_input_array($filtered_value, true, $max_length);
            } elseif (is_string($filtered_value) && '' !== $filtered_value) {
                $captured[$sanitized_name] = $this->prepare_value_for_analysis($filtered_value, $max_length);
            }
        }

        return $captured;
    }

    /**
     * Retrieve sanitized server data for fields that influence pattern analysis.
     *
     * @return array Sanitized server subset.
     */
    private function get_monitored_server_values() {
        $server_keys = array('REQUEST_URI', 'REQUEST_METHOD', 'HTTP_USER_AGENT', 'HTTP_REFERER');
        $captured = array();
        $max_length = 4096;

        foreach ($server_keys as $key) {
            $sanitized_key = sanitize_key($key);

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

            $filtered_value = filter_input(INPUT_SERVER, $key, FILTER_UNSAFE_RAW);

            if (null === $filtered_value) {
                $server_raw = filter_input(INPUT_SERVER, $key, FILTER_UNSAFE_RAW, FILTER_REQUIRE_ARRAY);

                if (null !== $server_raw) {
                    $raw = wp_unslash($server_raw);
                    $filtered_value = is_array($raw)
                        ? array_map('sanitize_text_field', $raw)
                        : sanitize_text_field($raw);
                }
            }

            if (is_array($filtered_value)) {
                $captured[$key] = $this->sanitize_input_array($filtered_value, true, $max_length);
            } elseif (is_string($filtered_value) && '' !== $filtered_value) {
                $captured[$key] = $this->prepare_value_for_analysis($filtered_value, $max_length);
            }
        }

        return $captured;
    }

    /**
     * Determine the cookie names that are relevant for request monitoring.
     *
     * @return array
     */
    private function get_cookie_names_to_monitor() {
        $cookie_names = array();

        if (defined('LOGGED_IN_COOKIE')) {
            $cookie_names[] = LOGGED_IN_COOKIE;
        }

        if (defined('SECURE_AUTH_COOKIE')) {
            $cookie_names[] = SECURE_AUTH_COOKIE;
        }

        if (defined('AUTH_COOKIE')) {
            $cookie_names[] = AUTH_COOKIE;
        }

        if (defined('COOKIEHASH')) {
            $cookie_names[] = 'wordpress_logged_in_' . COOKIEHASH;
            $cookie_names[] = 'wordpress_sec_' . COOKIEHASH;
        }

        return array_unique(array_filter($cookie_names));
    }
    
    protected function evaluate($data) {
        $ip = $this->get_client_ip();
        
        // Check if we're in flood alert cooldown for this IP (not blocking, just cooldown for alerts)
        $flood_cooldown_key = 'vulnity_flood_cooldown_' . md5($ip);
        $in_flood_cooldown = get_transient($flood_cooldown_key);
        
        // If in cooldown, don't process anything - exit early
        if ($in_flood_cooldown) {
            vulnity_log('[Vulnity] IP ' . $ip . ' is in flood cooldown - skipping all processing'); // Log suspicious traffic for operators.
            return;
        }
        
        $detections = array(
            'sql_injection' => array(),
            'code_execution' => array(),
            'xss' => array(),
            'path_traversal' => array(),
            'ldap_injection' => array(),
            'xxe' => array()
        );
        
        // Check GET parameters
        foreach ($data['get'] as $key => $value) {
            $this->check_value_for_patterns($value, 'GET', $key, $detections);
        }
        
        // Check POST parameters
        foreach ($data['post'] as $key => $value) {
            $this->check_value_for_patterns($value, 'POST', $key, $detections);
        }
        
        // Check cookies
        foreach ($data['cookie'] as $key => $value) {
            $this->check_value_for_patterns($value, 'COOKIE', $key, $detections);
        }
        
        // Check URI
        if (isset($data['server']['REQUEST_URI'])) {
            $uri = $data['server']['REQUEST_URI'];
            $this->check_value_for_patterns($uri, 'URI', 'REQUEST_URI', $detections);
        }
        
        // Check User-Agent
        if (isset($data['server']['HTTP_USER_AGENT'])) {
            $ua = $data['server']['HTTP_USER_AGENT'];
            $this->check_value_for_patterns($ua, 'USER_AGENT', 'HTTP_USER_AGENT', $detections);
        }
        
        // Check Referer
        if (isset($data['server']['HTTP_REFERER'])) {
            $referer = $data['server']['HTTP_REFERER'];
            $this->check_value_for_patterns($referer, 'REFERER', 'HTTP_REFERER', $detections);
        }
        
        // Process detections and check for flooding
        $this->process_detections($detections, $data);
    }
    
    private function check_value_for_patterns($value, $type, $parameter, &$detections) {
        if (is_array($value)) {
            foreach ($value as $nested_key => $nested_value) {
                $nested_key_string = is_string($nested_key) ? $nested_key : (string) $nested_key;
                $this->check_value_for_patterns($nested_value, $type, $parameter . '[' . $nested_key_string . ']', $detections);
            }
            
            return;
        }
        
        if (is_string($value)) {
            if ($this->should_skip_action_parameter($type, $parameter, $value)) {
                return;
            }
            $this->check_patterns($value, $type, $parameter, $detections);
        }
    }

    private function should_skip_action_parameter($type, $parameter, $value) {
        if ($parameter !== 'action' || !is_string($value) || $value === '') {
            return false;
        }

        if ($type !== 'GET' && $type !== 'POST') {
            return false;
        }

        $request_uri = vulnity_get_server_var('REQUEST_URI');
        if ($request_uri === '' || stripos($request_uri, 'admin-ajax.php') === false) {
            return false;
        }

        $allowlist = apply_filters('vulnity_suspicious_query_action_allowlist', array(
            'dbg_lv_get_current_user_email',
        ));

        if (in_array($value, $allowlist, true)) {
            return true;
        }

        $prefixes = apply_filters('vulnity_suspicious_query_action_prefixes', array(
            'dbg_lv_',
        ));

        foreach ($prefixes as $prefix) {
            if ($prefix !== '' && strpos($value, $prefix) === 0) {
                return true;
            }
        }

        $logged_in = function_exists('is_user_logged_in') && is_user_logged_in();
        if ($logged_in && preg_match('/^[a-z0-9_-]+$/i', $value) === 1) {
            return true;
        }

        return false;
    }
    
    private function check_patterns($value, $type, $parameter, &$detections) {
        // Check SQL injection patterns
        foreach ($this->sql_injection_patterns as $pattern) {
            if (@preg_match('/' . $pattern . '/i', $value)) {
                $detections['sql_injection'][] = array(
                    'type' => $type,
                    'parameter' => $parameter,
                    'pattern' => $pattern,
                    'value' => substr($value, 0, 200)
                );
                break; // One detection per category is enough
            }
        }
        
        // Check code execution patterns
        foreach ($this->code_execution_patterns as $pattern) {
            if (@preg_match('/' . $pattern . '/i', $value)) {
                $detections['code_execution'][] = array(
                    'type' => $type,
                    'parameter' => $parameter,
                    'pattern' => $pattern,
                    'value' => substr($value, 0, 200)
                );
                break;
            }
        }
        
        // Check XSS patterns
        foreach ($this->xss_patterns as $pattern) {
            if (@preg_match('/' . $pattern . '/i', $value)) {
                $detections['xss'][] = array(
                    'type' => $type,
                    'parameter' => $parameter,
                    'pattern' => $pattern,
                    'value' => substr($value, 0, 200)
                );
                break;
            }
        }
        
        // Check path traversal patterns
        foreach ($this->path_traversal_patterns as $pattern) {
            if (@preg_match('/' . $pattern . '/i', $value)) {
                $detections['path_traversal'][] = array(
                    'type' => $type,
                    'parameter' => $parameter,
                    'pattern' => $pattern,
                    'value' => substr($value, 0, 200)
                );
                break;
            }
        }
        
        // Check LDAP injection patterns
        foreach ($this->ldap_injection_patterns as $pattern) {
            if (@preg_match('/' . $pattern . '/i', $value)) {
                $detections['ldap_injection'][] = array(
                    'type' => $type,
                    'parameter' => $parameter,
                    'pattern' => $pattern,
                    'value' => substr($value, 0, 200)
                );
                break;
            }
        }
        
        // Check XXE patterns, but only when the payload actually looks like XML
        if ($this->looks_like_xml_payload($value)) {
            foreach ($this->xxe_patterns as $pattern) {
                if (@preg_match('/' . $pattern . '/i', $value)) {
                    $detections['xxe'][] = array(
                        'type' => $type,
                        'parameter' => $parameter,
                        'pattern' => $pattern,
                        'value' => substr($value, 0, 200)
                    );
                    break;
                }
            }
        }
    }

    /**
     * Determine if a value appears to contain XML markers before running XXE signatures.
     *
     * @param string $value
     *
     * @return bool
     */
    private function looks_like_xml_payload($value) {
        if (!is_string($value) || $value === '') {
            return false;
        }

        $markers = array('<?xml', '<!DOCTYPE', '<!ENTITY', 'xmlns', '<xi:include');

        foreach ($markers as $marker) {
            if (stripos($value, $marker) !== false) {
                return true;
            }
        }

        return (bool) preg_match('/<[^>]+:\w+/i', $value);
    }

    /**
     * Suppress duplicate alerts caused by internal follow-up requests that only
     * carry the payload in the Referer header.
     *
     * Example: a search request with a malicious query is logged once on the
     * page load, and then a second time when an internal REST/AJAX endpoint
     * receives that page URL as HTTP_REFERER.
     *
     * @param array $detections
     * @param array $data
     *
     * @return array
     */
    private function filter_internal_referer_only_detections($detections, $data) {
        $request_uri = isset($data['server']['REQUEST_URI']) ? (string) $data['server']['REQUEST_URI'] : '';
        $referer = isset($data['server']['HTTP_REFERER']) ? (string) $data['server']['HTTP_REFERER'] : '';

        if (!$this->is_internal_follow_up_request($request_uri)) {
            return $detections;
        }

        if (!$this->is_same_site_referer($referer)) {
            return $detections;
        }

        if (!$this->has_referer_only_detections($detections)) {
            return $detections;
        }

        vulnity_log(
            '[Vulnity] Suppressed referer-only suspicious query duplicate for internal request: ' . $request_uri
        );

        return array(
            'sql_injection' => array(),
            'code_execution' => array(),
            'xss' => array(),
            'path_traversal' => array(),
            'ldap_injection' => array(),
            'xxe' => array()
        );
    }

    /**
     * Determine whether the current request is an internal API/background call
     * that commonly replays the original page URL in HTTP_REFERER.
     *
     * @param string $request_uri
     *
     * @return bool
     */
    private function is_internal_follow_up_request($request_uri) {
        if (!is_string($request_uri) || $request_uri === '') {
            return false;
        }

        $internal_markers = array(
            '/wp-json/',
            'rest_route=',
            '/wp-admin/admin-ajax.php',
            '/wp-cron.php',
        );

        foreach ($internal_markers as $marker) {
            if (strpos($request_uri, $marker) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine whether the referer belongs to the current site.
     *
     * @param string $referer
     *
     * @return bool
     */
    private function is_same_site_referer($referer) {
        if (!is_string($referer) || $referer === '') {
            return false;
        }

        $site_host = wp_parse_url(get_site_url(), PHP_URL_HOST);
        $referer_host = wp_parse_url($referer, PHP_URL_HOST);

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

        return strtolower($site_host) === strtolower($referer_host);
    }

    /**
     * Check whether every detection came only from the Referer header.
     *
     * @param array $detections
     *
     * @return bool
     */
    private function has_referer_only_detections($detections) {
        $has_detections = false;

        foreach ((array) $detections as $group) {
            if (!is_array($group) || empty($group)) {
                continue;
            }

            foreach ($group as $detection) {
                if (!is_array($detection)) {
                    continue;
                }

                $has_detections = true;

                if (!isset($detection['type']) || $detection['type'] !== 'REFERER') {
                    return false;
                }
            }
        }

        return $has_detections;
    }
    
    private function process_detections($detections, $data) {
        $ip = $this->get_client_ip();

        $detections = $this->filter_internal_referer_only_detections($detections, $data);
        
        // Double check cooldown here too
        $flood_cooldown_key = 'vulnity_flood_cooldown_' . md5($ip);
        if (get_transient($flood_cooldown_key)) {
            vulnity_log('[Vulnity] IP ' . $ip . ' is in cooldown - no alerts will be created'); // Log suspicious traffic for operators.
            return;
        }
        
        // Count total detections
        $total_detections = 0;
        $detected_types = array();
        
        $attack_types = array(
            'sql_injection' => 'SQL Injection',
            'code_execution' => 'Code Execution',
            'xss' => 'Cross-Site Scripting (XSS)',
            'path_traversal' => 'Path Traversal',
            'ldap_injection' => 'LDAP Injection',
            'xxe' => 'XXE Injection'
        );
        
        foreach ($attack_types as $key => $name) {
            if (!empty($detections[$key])) {
                $total_detections++;
                $detected_types[] = $key;
            }
        }
        
        // If no detections, return
        if ($total_detections === 0) {
            return;
        }
        
        // Track attack counter for flood detection
        $counter_key = 'vulnity_attack_counter_' . md5($ip);
        $counter = get_transient($counter_key);
        
        $current_time = time();
        
        if (!$counter) {
            $counter = array(
                'count' => 1,
                'first_seen' => $current_time,
                'last_seen' => $current_time,
                'attack_types' => $detected_types,
                'uris' => array(isset($data['server']['REQUEST_URI']) ? $data['server']['REQUEST_URI'] : 'unknown')
            );
        } else {
            $counter['count']++;
            $counter['last_seen'] = $current_time;
            
            // Add new attack types
            foreach ($detected_types as $type) {
                if (!in_array($type, $counter['attack_types'])) {
                    $counter['attack_types'][] = $type;
                }
            }
            
            // Track URIs (limit to 10)
            $uri = isset($data['server']['REQUEST_URI']) ? $data['server']['REQUEST_URI'] : 'unknown';
            if (count($counter['uris']) < 10 && !in_array($uri, $counter['uris'])) {
                $counter['uris'][] = $uri;
            }
        }
        
        // Check if we've hit flood threshold
        $time_elapsed = $current_time - $counter['first_seen'];
        
        if ($counter['count'] >= $this->flood_threshold && $time_elapsed <= $this->flood_time_window) {
            // FLOOD DETECTED! Create critical alert and set cooldown
            $this->create_flood_alert($ip, $counter, $data);
            
            // Set flood cooldown IMMEDIATELY to prevent any more alerts
            set_transient($flood_cooldown_key, true, $this->flood_cooldown);
            self::remember_indexed_transient(self::FLOOD_COOLDOWN_INDEX, $flood_cooldown_key, $current_time + $this->flood_cooldown);

            // Clear the counter
            delete_transient($counter_key);
            self::forget_indexed_transient(self::ATTACK_COUNTER_INDEX, $counter_key);
            
            // Log the cooldown activation
            vulnity_log('[Vulnity] Flood detected for IP ' . $ip . ' - Cooldown activated for ' . $this->flood_cooldown . ' seconds'); // Log suspicious traffic for operators.
            
            // Don't create individual alerts when flood is detected
            return;
        }
        
        // Update counter (reset if time window exceeded)
        if ($time_elapsed > $this->flood_time_window) {
            // Reset counter
            $counter = array(
                'count' => 1,
                'first_seen' => $current_time,
                'last_seen' => $current_time,
                'attack_types' => $detected_types,
                'uris' => array(isset($data['server']['REQUEST_URI']) ? $data['server']['REQUEST_URI'] : 'unknown')
            );
        }
        
        set_transient($counter_key, $counter, $this->flood_time_window + 60);
        self::remember_indexed_transient(self::ATTACK_COUNTER_INDEX, $counter_key, $current_time + $this->flood_time_window + 60);
        
        // Create individual alerts (we already checked cooldown at the beginning)
        foreach ($attack_types as $key => $name) {
            if (!empty($detections[$key])) {
                $count = count($detections[$key]);
                $severity = $this->calculate_severity($key, $count);
                
                $this->create_alert(array(
                    'severity' => $severity,
                    'title' => sprintf('Suspicious Query Detected: %s', $name),
                    'message' => sprintf(
                        '%s attack attempt detected from IP %s with %d suspicious pattern(s)',
                        $name,
                        $ip,
                        $count
                    ),
                    'details' => array(
                        'attack_type' => $name,
                        'suspicious_patterns' => $detections[$key],
                        'request_uri' => isset($data['server']['REQUEST_URI']) ? $data['server']['REQUEST_URI'] : '',
                        'request_method' => isset($data['server']['REQUEST_METHOD']) ? $data['server']['REQUEST_METHOD'] : '',
                        'user_agent' => isset($data['server']['HTTP_USER_AGENT']) ? $data['server']['HTTP_USER_AGENT'] : 'Unknown',
                        'referrer' => isset($data['server']['HTTP_REFERER']) ? $data['server']['HTTP_REFERER'] : 'None',
                        'ip_address' => $ip,
                        'timestamp' => current_time('mysql')
                    )
                ));
            }
        }
    }
    
    /**
     * Create flood alert when threshold is exceeded
     */
    private function create_flood_alert($ip, $counter, $data) {
        $duration = $counter['last_seen'] - $counter['first_seen'];
        $rate = $duration > 0 ? round($counter['count'] / $duration, 2) : $counter['count'];
        
        // Analyze attack profile
        $attack_profile = $this->analyze_attack_profile($counter);
        
        // Check if it's likely an automated tool
        $user_agent = isset($data['server']['HTTP_USER_AGENT']) ? $data['server']['HTTP_USER_AGENT'] : '';
        $is_automated = $this->is_automated_scanner($user_agent, $rate);
        
        $this->create_alert(array(
            'severity' => 'critical',
            'title' => 'Attack Flood Detected - Multiple Suspicious Queries',
            'message' => sprintf(
                'FLOOD ATTACK: %d suspicious queries from IP %s in %d seconds. Attack types: %s. Alert cooldown applied for %d minutes.',
                $counter['count'],
                $ip,
                $duration,
                implode(', ', array_map(function($type) {
                    $map = array(
                        'sql_injection' => 'SQL Injection',
                        'code_execution' => 'Code Execution',
                        'xss' => 'XSS',
                        'path_traversal' => 'Path Traversal',
                        'ldap_injection' => 'LDAP',
                        'xxe' => 'XXE'
                    );
                    return isset($map[$type]) ? $map[$type] : $type;
                }, $counter['attack_types'])),
                $this->flood_cooldown / 60
            ),
            'details' => array(
                'flood_attack' => true,
                'ip_address' => $ip,
                'total_queries' => $counter['count'],
                'duration_seconds' => $duration,
                'queries_per_second' => $rate,
                'attack_types' => $counter['attack_types'],
                'targeted_uris' => $counter['uris'],
                'first_seen' => wp_date('Y-m-d H:i:s', $counter['first_seen']),
                'last_seen' => wp_date('Y-m-d H:i:s', $counter['last_seen']),
                'user_agent' => substr($user_agent, 0, 500),
                'attack_profile' => $attack_profile,
                'is_automated_tool' => $is_automated,
                'action_taken' => 'alert_cooldown_applied',
                'cooldown_duration_seconds' => $this->flood_cooldown,
                'timestamp' => current_time('mysql')
            )
        ));
        
        // Log suspicious traffic for operators.
        vulnity_log(sprintf(
            '[Vulnity] CRITICAL - Attack flood: IP %s - %d queries in %ds - Alert cooldown for %d minutes',
            $ip,
            $counter['count'],
            $duration,
            $this->flood_cooldown / 60
        ));
    }
    
    /**
     * Block an IP address
     */
    private function block_ip($ip) {
        $block_key = 'vulnity_flood_block_' . md5($ip);
        $block_until = time() + $this->ip_block_duration;

        set_transient($block_key, $block_until, $this->ip_block_duration);
        self::remember_indexed_transient(self::FLOOD_BLOCK_INDEX, $block_key, $block_until);

        // Store in blocked IPs list
        $blocked_ips = get_option('vulnity_blocked_ips', array());
        $blocked_ips[$ip] = array(
            'blocked_at' => current_time('mysql'),
            'blocked_until' => wp_date('Y-m-d H:i:s', $block_until),
            'reason' => 'suspicious_query_flood',
            'block_duration' => $this->ip_block_duration
        );
        
        // Keep only last 100 blocked IPs
        if (count($blocked_ips) > 100) {
            $blocked_ips = array_slice($blocked_ips, -100, 100, true);
        }
        
        update_option('vulnity_blocked_ips', $blocked_ips);

        $this->notify_siem_of_blocked_ip($ip, $block_until);

        // Log critical security actions.
        vulnity_log('[Vulnity] IP blocked for flooding: ' . $ip . ' until ' . wp_date('Y-m-d H:i:s', $block_until));
    }

    private function notify_siem_of_blocked_ip($ip, $block_until) {
        if (!class_exists('Vulnity_Mitigation_Manager')) {
            return;
        }

        $manager = Vulnity_Mitigation_Manager::get_instance();
        if (!method_exists($manager, 'dispatch_generic_alert')) {
            return;
        }

        $duration_seconds = property_exists($this, 'ip_block_duration') ? (int) $this->ip_block_duration : 0;
        if ($duration_seconds <= 0 && $block_until > time()) {
            $duration_seconds = (int) max($block_until - time(), 0);
        }

        $duration_info = $this->format_block_duration_for_alert($duration_seconds);

        if ($duration_info['permanent']) {
            $message = sprintf('IP %s bloqueada de forma permanente', $ip);
            $duration_value = 'permanent';
            $duration_label = 'permanente';
        } else {
            $message = sprintf('IP %s bloqueada por %s', $ip, $duration_info['label']);
            $duration_value = $duration_info['code'];
            $duration_label = $duration_info['label'];
        }

        $payload = array(
            'alert_type' => 'system_notification',
            'severity' => 'medium',
            'title' => 'IP Bloqueada',
            'message' => $message,
            'details' => array(
                'action' => 'ip_blocked',
                'ip_address' => $ip,
                'duration' => $duration_value,
                'duration_readable' => $duration_label,
                'reason' => 'suspicious_query_flood',
                'blocked_by' => 'automatic'
            ),
            'system_event' => true
        );

        $manager->dispatch_generic_alert($payload);
    }

    private function format_block_duration_for_alert($seconds) {
        if ($seconds <= 0) {
            return array(
                'code' => 'permanent',
                'label' => 'permanente',
                'permanent' => true
            );
        }

        if ($seconds % 3600 === 0) {
            $hours = (int) ($seconds / 3600);
            $label = $hours === 1 ? '1 hora' : $hours . ' horas';
            return array(
                'code' => $hours . 'h',
                'label' => $label,
                'permanent' => false
            );
        }

        if ($seconds % 60 === 0) {
            $minutes = (int) ($seconds / 60);
            $label = $minutes === 1 ? '1 minuto' : $minutes . ' minutos';
            return array(
                'code' => $minutes . 'm',
                'label' => $label,
                'permanent' => false
            );
        }

        $label = $seconds === 1 ? '1 segundo' : $seconds . ' segundos';
        return array(
            'code' => $seconds . 's',
            'label' => $label,
            'permanent' => false
        );
    }
    
    /**
     * Analyze attack profile based on patterns
     */
    private function analyze_attack_profile($counter) {
        $types = $counter['attack_types'];
        $count = $counter['count'];
        
        if (in_array('sql_injection', $types) && in_array('xss', $types)) {
            return 'Multi-vector vulnerability scan';
        } elseif (in_array('sql_injection', $types) && $count > 10) {
            return 'SQL injection attack campaign';
        } elseif (in_array('path_traversal', $types)) {
            return 'Directory enumeration attempt';
        } elseif (count($types) >= 3) {
            return 'Comprehensive security scan';
        } elseif ($count > 20) {
            return 'Automated attack tool';
        } else {
            return 'Targeted attack attempt';
        }
    }
    
    /**
     * Check if the request is from an automated scanner
     */
    private function is_automated_scanner($user_agent, $rate) {
        $ua_lower = strtolower($user_agent);
        
        // Known scanner patterns
        $scanners = array(
            'nikto', 'sqlmap', 'nmap', 'burp', 'zap', 'wpscan',
            'acunetix', 'nessus', 'metasploit', 'curl', 'wget',
            'python', 'ruby', 'perl', 'scanner', 'bot', 'spider',
            'crawl', 'scan', 'audit', 'dirbuster', 'gobuster'
        );
        
        foreach ($scanners as $scanner) {
            if (strpos($ua_lower, $scanner) !== false) {
                return true;
            }
        }
        
        // High request rate indicates automation
        if ($rate > 2) {
            return true;
        }
        
        // Empty or generic user agent
        if (empty($user_agent) || strlen($user_agent) < 10) {
            return true;
        }
        
        return false;
    }
    
    /**
     * Clean up old flood data
     */
    public function cleanup_flood_data() {
        self::cleanup_indexed_transients(self::ATTACK_COUNTER_INDEX);
        self::cleanup_indexed_transients(self::FLOOD_COOLDOWN_INDEX);
        self::cleanup_indexed_transients(self::FLOOD_BLOCK_INDEX);

        vulnity_log('[Vulnity] Cleaned up old flood data');
    }
    
    private function calculate_severity($attack_type, $count) {
        // Code execution and SQL injection are always critical
        if (in_array($attack_type, array('code_execution', 'sql_injection'))) {
            return 'critical';
        }
        
        // XXE and LDAP injection are high to critical based on count
        if (in_array($attack_type, array('xxe', 'ldap_injection'))) {
            return $count > 1 ? 'critical' : 'high';
        }
        
        // Path traversal is high
        if ($attack_type === 'path_traversal') {
            return 'high';
        }
        
        // XSS is medium to high based on count
        if ($attack_type === 'xss') {
            return $count > 2 ? 'high' : 'medium';
        }
        
        return 'high'; // Default
    }

    private function sanitize_input_array($input, $allow_newlines = false, $max_length = null) {
        $sanitized = array();

        foreach ((array) $input as $key => $value) {
            $clean_key = is_string($key) ? sanitize_key(wp_unslash($key)) : $key;

            if (is_array($value)) {
                $sanitized[$clean_key] = $this->sanitize_input_array($value, $allow_newlines, $max_length);
            } elseif (is_string($value)) {
                $sanitized[$clean_key] = $this->sanitize_string_value($value, $allow_newlines, $max_length);
            } else {
                $sanitized[$clean_key] = $value;
            }
        }

        return $sanitized;
    }

    private function sanitize_string_value($value, $allow_newlines = false, $max_length = null) {
        $value = wp_unslash($value);
        $value = wp_check_invalid_utf8($value, true);

        if ($allow_newlines) {
            $value = sanitize_textarea_field($value);
        } else {
            $value = sanitize_text_field($value);
        }

        if (is_int($max_length) && $max_length > 0) {
            $value = mb_substr($value, 0, $max_length);
        }

        return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
    }

    /**
     * Prepare a raw input value for security pattern analysis.
     *
     * Unlike sanitize_string_value(), this method does NOT strip HTML tags
     * or remove percent-encoded sequences — both of which would prevent the
     * XSS/injection patterns from matching. It URL-decodes the value once so
     * encoded payloads (e.g. %3Cscript%3E) are also caught.
     *
     * @param string   $value
     * @param int|null $max_length
     * @return string
     */
    private function prepare_value_for_analysis($value, $max_length = null) {
        if (!is_string($value)) {
            return '';
        }

        $value = wp_unslash($value);
        $value = wp_check_invalid_utf8($value, true);

        // URL-decode once to expose encoded payloads (%3Cscript%3E → <script>)
        $decoded = rawurldecode($value);
        if (is_string($decoded) && $decoded !== $value) {
            $value = $decoded;
        }

        // Strip only null bytes and non-printable control characters; keep HTML intact
        $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);

        if (is_int($max_length) && $max_length > 0) {
            $value = mb_substr($value, 0, $max_length);
        }

        return $value;
    }

    private static function get_indexed_transients($option_name) {
        $stored = get_option($option_name, array());

        if (!is_array($stored)) {
            $stored = array();
        }

        return $stored;
    }

    private static function remember_indexed_transient($option_name, $key, $expires_at) {
        $key = sanitize_key($key);
        $stored = self::get_indexed_transients($option_name);
        $stored[$key] = (int) $expires_at;
        update_option($option_name, $stored, false);
    }

    private static function forget_indexed_transient($option_name, $key) {
        $key = sanitize_key($key);
        $stored = self::get_indexed_transients($option_name);

        if (isset($stored[$key])) {
            unset($stored[$key]);
            update_option($option_name, $stored, false);
        }
    }

    private static function cleanup_indexed_transients($option_name) {
        $stored = self::get_indexed_transients($option_name);

        if (empty($stored)) {
            return;
        }

        $now = time();

        foreach ($stored as $key => $expires_at) {
            if ($expires_at <= $now || false === get_transient($key)) {
                delete_transient($key);
                unset($stored[$key]);
            }
        }

        update_option($option_name, $stored, false);
    }
}
