<?php
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

function moesec_firewall_tab() {
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'moesec'));
    }

    if (isset($_POST['moesec_save_firewall']) && check_admin_referer('moesec_firewall', 'moesec_firewall_nonce')) {
        $enable_firewall = isset($_POST['enable_firewall']) ? 1 : 0;
        $blocked_ips = isset($_POST['blocked_ips']) ? sanitize_textarea_field($_POST['blocked_ips']) : '';
        $enable_geo_blocking = isset($_POST['enable_geo_blocking']) ? 1 : 0;
        $blocked_countries = isset($_POST['blocked_countries']) ? array_map('sanitize_text_field', $_POST['blocked_countries']) : array();

        update_option('moesec_enable_firewall', $enable_firewall);
        update_option('moesec_blocked_ips', $blocked_ips);
        update_option('moesec_enable_geo_blocking', $enable_geo_blocking);
        update_option('moesec_blocked_countries', $blocked_countries);
        
        echo '<div class="updated"><p>' . esc_html__('Firewall settings saved successfully.', 'moesec') . '</p></div>';
    }
    
    $enable_firewall = get_option('moesec_enable_firewall', false);
    $blocked_ips = get_option('moesec_blocked_ips', '');
    $enable_geo_blocking = get_option('moesec_enable_geo_blocking', false);
    $blocked_countries = get_option('moesec_blocked_countries', array());
    
    // Country list
    $countries = moesec_get_country_list();
    
    // Check if GeoLite2 database exists
    $geoip_db_path = plugin_dir_path(__FILE__) . '../assets/GeoLite2-Country.mmdb';
    $geoip_db_exists = file_exists($geoip_db_path);
    
    ?>
    <h3><?php esc_html_e('Firewall', 'moesec'); ?></h3>
    <form method="post" action="">
        <?php wp_nonce_field('moesec_firewall', 'moesec_firewall_nonce'); ?>
        <table class="form-table">
            <tr>
                <th><label for="enable_firewall"><?php esc_html_e('Enable Firewall', 'moesec'); ?></label></th>
                <td>
                    <input type="checkbox" id="enable_firewall" name="enable_firewall" <?php checked($enable_firewall); ?>>
                    <p class="description"><?php esc_html_e('Enable IP-based firewall protection.', 'moesec'); ?></p>
                </td>
            </tr>
            <tr>
                <th><label for="blocked_ips"><?php esc_html_e('Blocked IP Addresses', 'moesec'); ?></label></th>
                <td>
                    <textarea id="blocked_ips" name="blocked_ips" rows="10" cols="50" class="large-text code"><?php echo esc_textarea($blocked_ips); ?></textarea>
                    <p class="description"><?php esc_html_e('Enter one IP address per line. You can use CIDR notation for IP ranges (e.g., 192.168.1.0/24).', 'moesec'); ?></p>
                </td>
            </tr>
            <tr>
                <th><label for="enable_geo_blocking"><?php esc_html_e('Enable Geo-Blocking', 'moesec'); ?></label></th>
                <td>
                    <input type="checkbox" id="enable_geo_blocking" name="enable_geo_blocking" <?php checked($enable_geo_blocking); ?> <?php echo !$geoip_db_exists ? 'disabled' : ''; ?>>
                    <p class="description">
                        <?php if ($geoip_db_exists): ?>
                            <span style="color: #46b450;">✓ <?php esc_html_e('GeoLite2 database is installed and ready.', 'moesec'); ?></span>
                        <?php else: ?>
                            <span style="color: #d63638;">✗ <?php esc_html_e('GeoLite2 database not found. Please download GeoLite2-Country.mmdb and place it in the assets folder.', 'moesec'); ?></span><br>
                            <a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data" target="_blank"><?php esc_html_e('Download GeoLite2 Database', 'moesec'); ?></a>
                        <?php endif; ?>
                    </p>
                </td>
            </tr>
            <?php if ($geoip_db_exists): ?>
            <tr>
                <th><label for="blocked_countries"><?php esc_html_e('Blocked Countries', 'moesec'); ?></label></th>
                <td>
                    <div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: #fff;">
                        <div style="margin-bottom: 10px;">
                            <button type="button" class="button" onclick="selectAllCountries(true)"><?php esc_html_e('Select All', 'moesec'); ?></button>
                            <button type="button" class="button" onclick="selectAllCountries(false)"><?php esc_html_e('Deselect All', 'moesec'); ?></button>
                        </div>
                        <?php foreach ($countries as $code => $name): ?>
                        <label style="display: block; margin-bottom: 5px;">
                            <input type="checkbox" 
                                   class="country-checkbox"
                                   name="blocked_countries[]" 
                                   value="<?php echo esc_attr($code); ?>" 
                                   <?php checked(in_array($code, $blocked_countries)); ?>>
                            <?php echo esc_html($name . ' (' . $code . ')'); ?>
                        </label>
                        <?php endforeach; ?>
                    </div>
                    <p class="description">
                        <?php esc_html_e('Select countries to block. This uses MaxMind GeoLite2 database for accurate country detection.', 'moesec'); ?>
                    </p>
                </td>
            </tr>
            <?php endif; ?>
        </table>
        <p class="submit">
            <input type="submit" name="moesec_save_firewall" class="button button-primary" value="<?php esc_attr_e('Save Firewall Settings', 'moesec'); ?>">
        </p>
    </form>
    
    <script>
    function selectAllCountries(select) {
        var checkboxes = document.querySelectorAll('.country-checkbox');
        checkboxes.forEach(function(checkbox) {
            checkbox.checked = select;
        });
    }
    </script>
    
    <div class="moesec-info-box" style="background: #e7f3ff; border-left: 4px solid #0073aa; padding: 15px; margin: 20px 0;">
        <h4><?php esc_html_e('Geo-Blocking Information:', 'moesec'); ?></h4>
        <ul>
            <li><?php esc_html_e('Uses MaxMind Free GeoLite2 database for accurate country detection', 'moesec'); ?></li>
            <li><?php esc_html_e('Works on any hosting environment (shared, VPS, or dedicated)', 'moesec'); ?></li>
            <li><?php esc_html_e('No server modules or external libraries required', 'moesec'); ?></li>
            <li><?php esc_html_e('Compatible with all PHP versions 7.0+', 'moesec'); ?></li>
            <li><?php esc_html_e('For more Advanced Firewall & WAF check MoeSec.com Platform', 'moesec'); ?></li>
        </ul>
    </div>
    <?php
}

// Get country list
function moesec_get_country_list() {
    return array(
        'AF' => 'Afghanistan', 'AL' => 'Albania', 'DZ' => 'Algeria', 'AR' => 'Argentina',
        'AU' => 'Australia', 'AT' => 'Austria', 'BD' => 'Bangladesh', 'BY' => 'Belarus',
        'BE' => 'Belgium', 'BR' => 'Brazil', 'BG' => 'Bulgaria', 'CA' => 'Canada',
        'CL' => 'Chile', 'CN' => 'China', 'CO' => 'Colombia', 'CR' => 'Costa Rica',
        'HR' => 'Croatia', 'CU' => 'Cuba', 'CZ' => 'Czech Republic', 'DK' => 'Denmark',
        'EG' => 'Egypt', 'EE' => 'Estonia', 'FI' => 'Finland', 'FR' => 'France',
        'DE' => 'Germany', 'GR' => 'Greece', 'HK' => 'Hong Kong', 'HU' => 'Hungary',
        'IS' => 'Iceland', 'IN' => 'India', 'ID' => 'Indonesia', 'IR' => 'Iran',
        'IQ' => 'Iraq', 'IE' => 'Ireland', 'IL' => 'Israel', 'IT' => 'Italy',
        'JP' => 'Japan', 'KZ' => 'Kazakhstan', 'KE' => 'Kenya', 'KP' => 'North Korea',
        'KR' => 'South Korea', 'LV' => 'Latvia', 'LT' => 'Lithuania', 'MY' => 'Malaysia',
        'MX' => 'Mexico', 'NL' => 'Netherlands', 'NZ' => 'New Zealand', 'NG' => 'Nigeria',
        'NO' => 'Norway', 'PK' => 'Pakistan', 'PE' => 'Peru', 'PH' => 'Philippines',
        'PL' => 'Poland', 'PT' => 'Portugal', 'RO' => 'Romania', 'RU' => 'Russia',
        'SA' => 'Saudi Arabia', 'RS' => 'Serbia', 'SG' => 'Singapore', 'SK' => 'Slovakia',
        'SI' => 'Slovenia', 'ZA' => 'South Africa', 'ES' => 'Spain', 'SE' => 'Sweden',
        'CH' => 'Switzerland', 'SY' => 'Syria', 'TW' => 'Taiwan', 'TH' => 'Thailand',
        'TR' => 'Turkey', 'UA' => 'Ukraine', 'AE' => 'United Arab Emirates', 'GB' => 'United Kingdom',
        'US' => 'United States', 'VE' => 'Venezuela', 'VN' => 'Vietnam'
    );
}

function moesec_get_country_from_ip($ip) {
    $geoip_db_path = plugin_dir_path(__FILE__) . '../assets/GeoLite2-Country.mmdb';
    
    if (!file_exists($geoip_db_path)) {
        error_log('[MoeSec] GeoIP database file not found at: ' . $geoip_db_path);
        return false;
    }
    
    try {
        $reader = new MoeSec_GeoIP2_Reader($geoip_db_path);
        $country = $reader->get_country($ip);
        error_log('[MoeSec] Country lookup for ' . $ip . ': ' . ($country ?: 'NOT FOUND'));
        return $country;
    } catch (Exception $e) {
        error_log('[MoeSec] GeoIP Error: ' . $e->getMessage());
        return false;
    }
}

class MoeSec_GeoIP2_Reader {
    private $fileHandle;
    private $fileSize;
    private $nodeCount;
    private $recordSize;
    private $searchTreeSize;
    private $metadataStart;
    
    public function __construct($database_file) {
        if (!file_exists($database_file)) {
            throw new Exception('Database file not found');
        }
        
        $this->fileHandle = fopen($database_file, 'rb');
        if (!$this->fileHandle) {
            throw new Exception('Cannot open database file');
        }
        
        $this->fileSize = filesize($database_file);
        $this->parse_metadata();
        
        error_log('[MoeSec] MMDB initialized - Nodes: ' . $this->nodeCount . ', RecordSize: ' . $this->recordSize . ', TreeSize: ' . $this->searchTreeSize);
    }
    
    public function __destruct() {
        if ($this->fileHandle) {
            fclose($this->fileHandle);
        }
    }
    
    private function parse_metadata() {
        $searchSize = min(131072, $this->fileSize);
        fseek($this->fileHandle, -$searchSize, SEEK_END);
        $buffer = fread($this->fileHandle, $searchSize);
        
        $marker = "\xab\xcd\xefMaxMind.com";
        $markerPos = strrpos($buffer, $marker);
        
        if ($markerPos === false) {
            throw new Exception('Metadata marker not found');
        }
        
        $this->metadataStart = $this->fileSize - $searchSize + $markerPos;
        
        fseek($this->fileHandle, $this->metadataStart + strlen($marker));
        $metadataBytes = fread($this->fileHandle, 10000);
        
        $metadata = $this->decode_metadata_map($metadataBytes);
        
        $this->nodeCount = isset($metadata['node_count']) ? $metadata['node_count'] : 0;
        $this->recordSize = isset($metadata['record_size']) ? $metadata['record_size'] : 28;
        
        if ($this->nodeCount == 0) {
            $estimatedTreeSize = $this->metadataStart - 100;
            $bytesPerNode = $this->recordSize / 4; // 28 bits = 7 bytes per node
            $this->nodeCount = intval($estimatedTreeSize / $bytesPerNode);
            error_log('[MoeSec] Using estimated node count: ' . $this->nodeCount);
        }
        
        $this->searchTreeSize = $this->nodeCount * ($this->recordSize / 4);
        
        error_log('[MoeSec] Metadata parsed - NodeCount: ' . $this->nodeCount . ', RecordSize: ' . $this->recordSize);
    }
    
    private function decode_metadata_map($data) {
        $metadata = array();
        $pos = 0;
        $len = strlen($data);
        
        
        if (preg_match('/node_count/', $data, $matches, PREG_OFFSET_CAPTURE)) {
            $keyPos = $matches[0][1];
            $valueSection = substr($data, $keyPos + 10, 20);
            
            for ($i = 0; $i < 15; $i++) {
                if (!isset($valueSection[$i])) break;
                $byte = ord($valueSection[$i]);
                
                if ($byte >= 0xc1 && $byte <= 0xc3) {
                    $intSize = $byte - 0xc0;
                    $intBytes = substr($valueSection, $i + 1, $intSize);
                    if (strlen($intBytes) == $intSize) {
                        $value = 0;
                        for ($j = 0; $j < $intSize; $j++) {
                            $value = ($value << 8) | ord($intBytes[$j]);
                        }
                        $metadata['node_count'] = $value;
                        error_log('[MoeSec] Found node_count in metadata: ' . $value);
                        break;
                    }
                }
                
                if ($i + 4 <= strlen($valueSection)) {
                    $testValue = unpack('N', substr($valueSection, $i, 4))[1];
                    if ($testValue > 100000 && $testValue < 10000000) {
                        $metadata['node_count'] = $testValue;
                        error_log('[MoeSec] Estimated node_count: ' . $testValue);
                        break;
                    }
                }
            }
        }
        
        if (preg_match('/record_size/', $data, $matches, PREG_OFFSET_CAPTURE)) {
            $keyPos = $matches[0][1];
            $valueSection = substr($data, $keyPos + 11, 10);
            
            for ($i = 0; $i < 5; $i++) {
                if (!isset($valueSection[$i])) break;
                $byte = ord($valueSection[$i]);
                if ($byte >= 24 && $byte <= 32) {
                    $metadata['record_size'] = $byte;
                    error_log('[MoeSec] Found record_size in metadata: ' . $byte);
                    break;
                }
            }
        }
        
        return $metadata;
    }
    
    public function get_country($ip) {
        $originalIp = $ip;
        $isIPv4 = false;
        
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $ip = '::ffff:' . $ip;
            $isIPv4 = true;
            error_log('[MoeSec] Converted IPv4 to IPv6-mapped: ' . $ip);
        }
        
        $ipBinary = @inet_pton($ip);
        if ($ipBinary === false) {
            error_log('[MoeSec] Invalid IP format: ' . $originalIp);
            return false;
        }
        
        error_log('[MoeSec] IP binary length: ' . strlen($ipBinary) . ' bytes');
        
        $dataPointer = $this->search_tree($ipBinary, $isIPv4);
        
        if ($dataPointer === false || $dataPointer == 0) {
            error_log('[MoeSec] No data pointer found for IP: ' . $originalIp);
            return false;
        }
        
        error_log('[MoeSec] Data pointer: ' . $dataPointer);
        
        $countryCode = $this->read_data_section($dataPointer);
        
        error_log('[MoeSec] Country code result: ' . ($countryCode ?: 'NULL'));
        
        return $countryCode;
    }
    
    private function search_tree($ipBinary, $isIPv4 = false) {
        $bitCount = strlen($ipBinary) * 8;
        $node = 0;
        
        $startBit = $isIPv4 ? 96 : 0;
        
        error_log('[MoeSec] Starting tree search, bits: ' . $bitCount . ', startBit: ' . $startBit);
        
        for ($i = $startBit; $i < $bitCount; $i++) {
            if ($node >= $this->nodeCount) {
                error_log('[MoeSec] Reached data section at bit ' . $i . ', node: ' . $node);
                return $node;
            }
            
            // Get the bit value
            $byteIndex = intval($i / 8);
            $bitIndex = $i % 8;
            $byte = ord($ipBinary[$byteIndex]);
            $bit = ($byte >> (7 - $bitIndex)) & 1;
            
            // Read the node record
            $nextNode = $this->read_node_record($node, $bit);
            
            if ($nextNode === false) {
                error_log('[MoeSec] Failed to read node at position ' . $node);
                return false;
            }
            
            if ($i < $startBit + 10 || $i % 20 == 0) {
                error_log('[MoeSec] Bit ' . $i . ': ' . $bit . ', Node: ' . $node . ' -> ' . $nextNode);
            }
            
            $node = $nextNode;
        }
        
        return $node >= $this->nodeCount ? $node : false;
    }
    
    private function read_node_record($nodeNumber, $index) {
        
        if ($this->recordSize == 24) {
            $nodeBytes = 6; // 2 records × 3 bytes
            $offset = $nodeNumber * $nodeBytes;
            
            if ($offset + $nodeBytes > $this->searchTreeSize) {
                return false;
            }
            
            fseek($this->fileHandle, $offset);
            $nodeData = fread($this->fileHandle, $nodeBytes);
            
            if (strlen($nodeData) < $nodeBytes) {
                return false;
            }
            
            if ($index == 0) {
                $bytes = substr($nodeData, 0, 3);
                $value = (ord($bytes[0]) << 16) | (ord($bytes[1]) << 8) | ord($bytes[2]);
            } else {
                $bytes = substr($nodeData, 3, 3);
                $value = (ord($bytes[0]) << 16) | (ord($bytes[1]) << 8) | ord($bytes[2]);
            }
            
            return $value;
            
        } else if ($this->recordSize == 28) {
            $nodeBytes = 7;
            $offset = $nodeNumber * $nodeBytes;
            
            if ($offset + $nodeBytes > $this->searchTreeSize) {
                return false;
            }
            
            fseek($this->fileHandle, $offset);
            $nodeData = fread($this->fileHandle, $nodeBytes);
            
            if (strlen($nodeData) < $nodeBytes) {
                return false;
            }
            
            if ($index == 0) {
                $value = (ord($nodeData[0]) << 20) | (ord($nodeData[1]) << 12) | 
                         (ord($nodeData[2]) << 4) | (ord($nodeData[3]) >> 4);
            } else {
                $value = ((ord($nodeData[3]) & 0x0F) << 24) | (ord($nodeData[4]) << 16) | 
                         (ord($nodeData[5]) << 8) | ord($nodeData[6]);
            }
            
            return $value;
            
        } else {
            $nodeBytes = ($this->recordSize * 2) / 8;
            $offset = $nodeNumber * $nodeBytes;
            
            if ($offset + $nodeBytes > $this->searchTreeSize) {
                return false;
            }
            
            fseek($this->fileHandle, $offset);
            $nodeData = fread($this->fileHandle, intval($nodeBytes) + 1);
            
            if (strlen($nodeData) < $nodeBytes) {
                return false;
            }
            
            $bytesPerRecord = $this->recordSize / 8;
            $recordOffset = $index * $bytesPerRecord;
            $bytes = substr($nodeData, intval($recordOffset), intval($bytesPerRecord) + 1);
            $value = 0;
            for ($i = 0; $i < strlen($bytes) && $i < 4; $i++) {
                $value = ($value << 8) | ord($bytes[$i]);
            }
            
            return $value;
        }
    }
    
    private function read_data_section($pointer) {
        $dataOffset = $this->searchTreeSize + 16 + ($pointer - $this->nodeCount);
        
        error_log('[MoeSec] Reading data at offset: ' . $dataOffset . ' (pointer: ' . $pointer . ', tree size: ' . $this->searchTreeSize . ', node count: ' . $this->nodeCount . ')');
        
        if ($dataOffset < 0 || $dataOffset > $this->metadataStart) {
            error_log('[MoeSec] Invalid data offset (metadata starts at: ' . $this->metadataStart . ')');
            return false;
        }
        
        fseek($this->fileHandle, $dataOffset);
        $data = fread($this->fileHandle, 1000);
        
        if (strlen($data) < 5) {
            error_log('[MoeSec] Insufficient data read');
            return false;
        }
        
        error_log('[MoeSec] Data section (first 100 bytes hex): ' . bin2hex(substr($data, 0, 100)));
        
        $countryCode = $this->decode_mmdb_data($data);
        
        return $countryCode;
    }
    
    private function decode_mmdb_data($data) {
        $pos = 0;
        $result = $this->decode_value($data, $pos);
        
        if (!is_array($result)) {
            error_log('[MoeSec] Root data is not a map');
            return false;
        }
        
        
        if (isset($result['country']['iso_code'])) {
            $code = $result['country']['iso_code'];
            error_log('[MoeSec] Found country code in country.iso_code: ' . $code);
            return $code;
        }
        
        if (isset($result['registered_country']['iso_code'])) {
            $code = $result['registered_country']['iso_code'];
            error_log('[MoeSec] Found country code in registered_country.iso_code: ' . $code);
            return $code;
        }
        
        if (isset($result['represented_country']['iso_code'])) {
            $code = $result['represented_country']['iso_code'];
            error_log('[MoeSec] Found country code in represented_country.iso_code: ' . $code);
            return $code;
        }
        
        error_log('[MoeSec] No country code found in decoded data structure');
        return false;
    }
    
    private function decode_value($data, &$pos) {
        if ($pos >= strlen($data)) {
            return null;
        }
        
        $ctrl = ord($data[$pos++]);
        $type = $ctrl >> 5; // Upper 3 bits = type
        
        if ($type == 0) {
            if ($pos >= strlen($data)) return null;
            $type = ord($data[$pos++]) + 7;
        }
        
        $size = $ctrl & 0x1f;
        
        if ($size >= 29) {
            $bytesToRead = $size - 28;
            $size = 0;
            for ($i = 0; $i < $bytesToRead && $pos < strlen($data); $i++) {
                $size = ($size << 8) | ord($data[$pos++]);
            }
            if ($size >= 29) {
                $size += 29 + (($bytesToRead - 1) * 256);
            }
        }
        
        switch ($type) {
            case 1: // Pointer
                return $this->decode_pointer($data, $pos, $ctrl);
                
            case 2: // UTF-8 String
                if ($pos + $size > strlen($data)) return '';
                $value = substr($data, $pos, $size);
                $pos += $size;
                return $value;
                
            case 3: // Double
                if ($pos + 8 > strlen($data)) return 0.0;
                $value = unpack('d', substr($data, $pos, 8))[1];
                $pos += 8;
                return $value;
                
            case 4: // Bytes
                if ($pos + $size > strlen($data)) return '';
                $value = substr($data, $pos, $size);
                $pos += $size;
                return $value;
                
            case 5: // Unsigned 16-bit int
                if ($pos + 2 > strlen($data)) return 0;
                $value = unpack('n', substr($data, $pos, 2))[1];
                $pos += 2;
                return $value;
                
            case 6: // Unsigned 32-bit int
                if ($pos + 4 > strlen($data)) return 0;
                $value = unpack('N', substr($data, $pos, 4))[1];
                $pos += 4;
                return $value;
                
            case 7: // Map
                $map = array();
                for ($i = 0; $i < $size; $i++) {
                    $key = $this->decode_value($data, $pos);
                    $value = $this->decode_value($data, $pos);
                    if ($key !== null) {
                        $map[$key] = $value;
                    }
                }
                return $map;
                
            case 8: // Signed 32-bit int
                if ($pos + 4 > strlen($data)) return 0;
                $bytes = substr($data, $pos, 4);
                $pos += 4;
                $value = unpack('N', $bytes)[1];
                if ($value & 0x80000000) {
                    $value = -((~$value & 0xffffffff) + 1);
                }
                return $value;
                
            case 9: // Unsigned 64-bit int
                if ($pos + 8 > strlen($data)) return 0;
                $bytes = substr($data, $pos, 8);
                $pos += 8;
                $high = unpack('N', substr($bytes, 0, 4))[1];
                $low = unpack('N', substr($bytes, 4, 4))[1];
                return ($high << 32) | $low;
                
            case 10: // Unsigned 128-bit int
                if ($pos + 16 > strlen($data)) return 0;
                $pos += 16;
                return 0; // Not fully supported
                
            case 11: // Array
                $array = array();
                for ($i = 0; $i < $size; $i++) {
                    $array[] = $this->decode_value($data, $pos);
                }
                return $array;
                
            case 14: // Boolean
                return $size == 1;
                
            case 15: // Float
                if ($pos + 4 > strlen($data)) return 0.0;
                $value = unpack('f', substr($data, $pos, 4))[1];
                $pos += 4;
                return $value;
                
            default:
                // Unknown type or integer with size bytes
                if ($size > 0 && $pos + $size <= strlen($data)) {
                    $value = 0;
                    for ($i = 0; $i < $size; $i++) {
                        $value = ($value << 8) | ord($data[$pos++]);
                    }
                    return $value;
                }
                return null;
        }
    }
    
    private function decode_pointer($data, &$pos, $ctrl) {
        $size = (($ctrl >> 3) & 0x3);
        $bytes = $size + 1;
        
        if ($pos + $bytes > strlen($data)) {
            return null;
        }
        
        $pointer = 0;
        $offset = 0;
        
        switch ($size) {
            case 0: // 11 bit pointer
                $pointer = (($ctrl & 0x7) << 8) | ord($data[$pos++]);
                $offset = 0;
                break;
            case 1: // 19 bit pointer
                $pointer = (($ctrl & 0x7) << 16) | (ord($data[$pos++]) << 8) | ord($data[$pos++]);
                $offset = 2048;
                break;
            case 2: // 27 bit pointer
                $pointer = (($ctrl & 0x7) << 24) | (ord($data[$pos++]) << 16) | 
                           (ord($data[$pos++]) << 8) | ord($data[$pos++]);
                $offset = 526336;
                break;
            case 3: // 35 bit pointer
                $pointer = (ord($data[$pos++]) << 24) | (ord($data[$pos++]) << 16) | 
                           (ord($data[$pos++]) << 8) | ord($data[$pos++]);
                $offset = 0;
                break;
        }
        
        $pointer += $offset;
        
        $savedPos = $pos;
        $pos = $pointer;
        $value = $this->decode_value($data, $pos);
        $pos = $savedPos;
        
        return $value;
    }
}

if (get_option('moesec_enable_firewall', false)) {
    add_action('init', 'moesec_firewall', 1);
}

function moesec_firewall() {
    // Allow admin access
    if (is_admin()) {
        return;
    }
    
    $ip_address = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
    
    if (empty($ip_address)) {
        return;
    }
    
    $blocked_ips = get_option('moesec_blocked_ips', '');
    
    // Check if IP is blocked
    $blocked_ip_list = array_map('trim', explode("\n", $blocked_ips));
    foreach ($blocked_ip_list as $blocked_ip) {
        if (empty($blocked_ip)) {
            continue;
        }
        if (moesec_ip_in_range($ip_address, $blocked_ip)) {
            moesec_log(sprintf('Blocked IP access attempt: %s', $ip_address));
            wp_die(esc_html__('Access denied by MoeSec.com Website Firewall.', 'moesec'), esc_html__('Firewall Block', 'moesec'), array('response' => 403));
        }
    }
    
    // Check geo-blocking
    if (get_option('moesec_enable_geo_blocking', false)) {
        $blocked_countries = get_option('moesec_blocked_countries', array());
        if (!empty($blocked_countries)) {
            $country = moesec_get_country_from_ip($ip_address);
            if ($country && in_array($country, $blocked_countries)) {
                moesec_log(sprintf('Blocked country access attempt: %s from %s', $country, $ip_address));
                wp_die(
                    esc_html__('Access from your country is not permitted. MoeSec.com Website Security', 'moesec'),
                    esc_html__('Geo-Block', 'moesec'),
                    array('response' => 403)
                );
            }
        }
    }
}

function moesec_ip_in_range($ip, $range) {
    if (strpos($range, '/') !== false) {
        // CIDR
        list($range, $netmask) = explode('/', $range, 2);
        $range_decimal = ip2long($range);
        $ip_decimal = ip2long($ip);
        
        if ($range_decimal === false || $ip_decimal === false) {
            return false;
        }
        
        $wildcard_decimal = pow(2, (32 - $netmask)) - 1;
        $netmask_decimal = ~ $wildcard_decimal;
        return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal));
    } else {
        // Single IP
        return $ip === $range;
    }
}
