<?php
/**
 * Live Traffic Filter
 *
 * @package VMPFence
 * @subpackage Tools
 * @since 2.1.0
 */

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Class VMPFence_Live_Traffic_Filter
 *
 * Represents a single filter condition for live traffic queries
 *
 * @since 2.1.0
 */
class VMPFence_Live_Traffic_Filter {

    /**
     * Filter parameter (ip, url, action, etc.)
     * @var string
     */
    private $param;

    /**
     * Filter operator (=, !=, contains, regexp, etc.)
     * @var string
     */
    private $operator;

    /**
     * Filter value
     * @var mixed
     */
    private $value;

    /**
     * Parent query reference
     * @var VMPFence_Live_Traffic_Query
     */
    private $query;

    /**
     * Valid operators
     * @var array
     */
    private $validOperators = array(
        '=',
        '!=',
        'contains',
        'notcontains',
        'regexp',
        'hregexp',      // Hex regexp for IP
        'hnotregexp',   // Hex not regexp for IP
        '>',
        '<',
        '>=',
        '<=',
        'isnullorempty', // Special operator for checking NULL or empty string
    );

    /**
     * Constructor
     *
     * @since 2.1.0
     * @param VMPFence_Live_Traffic_Query $query Parent query
     * @param string $param Filter parameter
     * @param string $operator Filter operator
     * @param mixed $value Filter value
     */
    public function __construct($query, $param, $operator, $value) {
        $this->query = $query;
        $this->param = strtolower($param);
        $this->operator = strtolower($operator);
        $this->value = $value;
    }

    /**
     * Validate filter
     *
     * @since 2.1.0
     * @return bool
     */
    public function validate() {
        // Check if parameter is valid
        if (!$this->query->isValidParam($this->param)) {
            return false;
        }

        // Check if operator is valid
        if (!in_array($this->operator, $this->validOperators)) {
            return false;
        }

        return true;
    }

    /**
     * Convert filter to SQL WHERE clause fragment
     *
     * @since 2.1.0
     * @return string SQL WHERE clause fragment
     */
    public function toSQL() {
        global $wpdb;

        if (!$this->validate()) {
            return '';
        }

        // Get column name
        $column = $this->query->getColumnName($this->param);
        
        // Add table alias for main hits table columns
        if (!in_array($column, array('user_login', 'display_name', 'fail', 'username'))) {
            $column = 'h.' . $column;
        } else {
            // User table columns
            if ($column === 'user_login' || $column === 'display_name') {
                $column = 'u.' . $column;
            }
            // Login table columns
            if ($column === 'fail' || ($column === 'username' && $this->param === 'loginusername')) {
                $column = 'l.' . $column;
            }
        }

        $value = $this->value;

        // Handle special cases based on parameter type
        switch ($this->param) {
            case 'ip':
                return $this->buildIPFilter($column, $value);
                
            case 'type':
                // Type filter: human=1, bot=0
                if ($value === 'human') {
                    return "(h.jsRun = 1)";
                } elseif ($value === 'bot' || $value === 'crawler') {
                    return "(h.jsRun = 0)";
                }
                // Invalid type value - return empty to skip filter
                return '';

            case 'isgoogle':
            case 'jsrun':
                // Boolean fields - only support = and != operators
                $boolValue = ($value === 'yes' || $value === '1' || $value === 1 || $value === true) ? 1 : 0;
                if ($this->operator === '=') {
                    return $wpdb->prepare("({$column} = %d)", $boolValue);
                } elseif ($this->operator === '!=') {
                    return $wpdb->prepare("({$column} != %d)", $boolValue);
                }
                // Invalid operator for boolean field - return empty to skip filter
                return '';

            case 'statuscode':
            case 'userid':
                // Numeric fields
                return $this->buildNumericFilter($column, $value);

            case 'url':
            case 'referer':
            case 'ua':
            case 'action':
            case 'actiondescription':
            case 'username':
                // Text fields
                return $this->buildTextFilter($column, $value);
        }

        return '';
    }

    /**
     * Build IP filter
     *
     * Handles IP addresses, wildcards, and ranges
     *
     * @since 2.1.0
     * @param string $column Column name
     * @param string $value Filter value
     * @return string SQL fragment
     */
    private function buildIPFilter($column, $value) {
        global $wpdb;
        
        // Check MySQL version for INET6_NTOA support (MySQL 5.6.3+)
        static $ip_convert = null;
        if ($ip_convert === null) {
            $mysql_version = $wpdb->get_var('SELECT VERSION()');
            $has_inet6 = version_compare($mysql_version, '5.6.3', '>=');
            $ip_convert = $has_inet6 ? 'INET6_NTOA' : 'INET_NTOA';
        }

        // Handle wildcards (e.g., 192.168.*.*) - IPv4 only
        if (strpos($value, '*') !== false && strpos($value, ':') === false) {
            // Convert wildcard to REGEXP pattern
            $pattern = str_replace('.', '\\.', $value);
            $pattern = str_replace('*', '[0-9]{1,3}', $pattern);
            $pattern = '^' . $pattern . '$';
            
            // Convert IP column to string for REGEXP comparison
            return $wpdb->prepare("({$ip_convert}(CAST(CONV(HEX({$column}), 16, 10) AS UNSIGNED)) REGEXP %s)", $pattern);
        }

        // Handle CIDR notation (e.g., 192.168.1.0/24 or 2001:db8::/32)
        if (strpos($value, '/') !== false) {
            $cidr_parts = explode('/', $value);
            if (count($cidr_parts) === 2 && !empty($cidr_parts[0]) && !empty($cidr_parts[1])) {
                list($network, $prefix) = $cidr_parts;
                $binaryNetwork = @inet_pton($network);
                
                if ($binaryNetwork) {
                    $prefix = intval($prefix);
                    $isIPv6 = (strlen($binaryNetwork) === 16);
                    
                    // For IPv4, use simple LIKE pattern
                    if (!$isIPv6 && strpos($network, '.') !== false) {
                        $parts = explode('.', $network);
                        $octets = intval($prefix) / 8;
                        $likePattern = implode('.', array_slice($parts, 0, $octets)) . '%';
                        
                        return $wpdb->prepare("({$ip_convert}(CAST(CONV(HEX({$column}), 16, 10) AS UNSIGNED)) LIKE %s)", $likePattern);
                    }
                    
                    // For IPv6, this requires binary comparison which is complex in SQL
                    // Fall back to no filtering for now (would need stored procedure or app-level filtering)
                    return "1=1";
                }
            }
        }

        // Regular operators
        switch ($this->operator) {
            case '=':
                $binaryIP = @inet_pton($value);
                if ($binaryIP) {
                    return $wpdb->prepare("({$column} = %s)", $binaryIP);
                }
                break;

            case '!=':
                $binaryIP = @inet_pton($value);
                if ($binaryIP) {
                    return $wpdb->prepare("({$column} != %s)", $binaryIP);
                }
                break;

            case 'contains':
                // Get MySQL version-aware IP converter
                static $ip_convert_contains = null;
                if ($ip_convert_contains === null) {
                    $mysql_version = $wpdb->get_var('SELECT VERSION()');
                    $has_inet6 = version_compare($mysql_version, '5.6.3', '>=');
                    $ip_convert_contains = $has_inet6 ? 'INET6_NTOA' : 'INET_NTOA';
                }
                // Convert IP to string and use LIKE
                return $wpdb->prepare("({$ip_convert_contains}(CAST(CONV(HEX({$column}), 16, 10) AS UNSIGNED)) LIKE %s)", '%' . $wpdb->esc_like($value) . '%');

            case 'notcontains':
                static $ip_convert_notcontains = null;
                if ($ip_convert_notcontains === null) {
                    $mysql_version = $wpdb->get_var('SELECT VERSION()');
                    $has_inet6 = version_compare($mysql_version, '5.6.3', '>=');
                    $ip_convert_notcontains = $has_inet6 ? 'INET6_NTOA' : 'INET_NTOA';
                }
                return $wpdb->prepare("({$ip_convert_notcontains}(CAST(CONV(HEX({$column}), 16, 10) AS UNSIGNED)) NOT LIKE %s)", '%' . $wpdb->esc_like($value) . '%');

            case 'regexp':
            case 'hregexp':
                static $ip_convert_regexp = null;
                if ($ip_convert_regexp === null) {
                    $mysql_version = $wpdb->get_var('SELECT VERSION()');
                    $has_inet6 = version_compare($mysql_version, '5.6.3', '>=');
                    $ip_convert_regexp = $has_inet6 ? 'INET6_NTOA' : 'INET_NTOA';
                }
                // REGEXP on string representation
                return $wpdb->prepare("({$ip_convert_regexp}(CAST(CONV(HEX({$column}), 16, 10) AS UNSIGNED)) REGEXP %s)", $value);

            case 'hnotregexp':
                static $ip_convert_notregexp = null;
                if ($ip_convert_notregexp === null) {
                    $mysql_version = $wpdb->get_var('SELECT VERSION()');
                    $has_inet6 = version_compare($mysql_version, '5.6.3', '>=');
                    $ip_convert_notregexp = $has_inet6 ? 'INET6_NTOA' : 'INET_NTOA';
                }
                return $wpdb->prepare("({$ip_convert_notregexp}(CAST(CONV(HEX({$column}), 16, 10) AS UNSIGNED)) NOT REGEXP %s)", $value);
        }

        return '';
    }

    /**
     * Build text filter
     *
     * @since 2.1.0
     * @param string $column Column name
     * @param string $value Filter value
     * @return string SQL fragment
     */
    private function buildTextFilter($column, $value) {
        global $wpdb;

        switch ($this->operator) {
            case '=':
                return $wpdb->prepare("({$column} = %s)", $value);

            case '!=':
                return $wpdb->prepare("({$column} != %s OR {$column} IS NULL)", $value);

            case 'contains':
                return $wpdb->prepare("({$column} LIKE %s)", '%' . $wpdb->esc_like($value) . '%');

            case 'notcontains':
                return $wpdb->prepare("({$column} NOT LIKE %s OR {$column} IS NULL)", '%' . $wpdb->esc_like($value) . '%');

            case 'regexp':
                return $wpdb->prepare("({$column} REGEXP %s)", $value);

            case 'isnullorempty':
                return "({$column} IS NULL OR {$column} = '')";
        }

        return '';
    }

    /**
     * Build numeric filter
     *
     * @since 2.1.0
     * @param string $column Column name
     * @param mixed $value Filter value
     * @return string SQL fragment
     */
    private function buildNumericFilter($column, $value) {
        global $wpdb;

        $numValue = is_numeric($value) ? floatval($value) : 0;

        switch ($this->operator) {
            case '=':
                return $wpdb->prepare("({$column} = %f)", $numValue);

            case '!=':
                return $wpdb->prepare("({$column} != %f)", $numValue);

            case '>':
                return $wpdb->prepare("({$column} > %f)", $numValue);

            case '<':
                return $wpdb->prepare("({$column} < %f)", $numValue);

            case '>=':
                return $wpdb->prepare("({$column} >= %f)", $numValue);

            case '<=':
                return $wpdb->prepare("({$column} <= %f)", $numValue);

            case 'contains':
                // For numeric fields, contains works like equals
                return $wpdb->prepare("({$column} = %f)", $numValue);
        }

        return '';
    }
}
