<?php
/**
 * Global AI + Search Engine Refusal Layer
 * 
 * Handles meta tags, HTTP headers, robots.txt management,
 * and active enforcement for AI training and indexing refusal.
 */

namespace HumanGate;

class RefusalLayer {
    
    /**
     * Known AI crawler user agents to block
     */
    private $ai_crawler_agents = [
        'GPTBot',
        'ChatGPT-User',
        'CCBot',
        'anthropic-ai',
        'ClaudeBot',
        'Claude-Web',
        'PerplexityBot',
        'Perplexity',
        'Google-Extended',
        'GoogleOther',
        'Applebot-Extended',
        'Omgilibot',
        'FacebookBot',
        'Bytespider', // TikTok
        'Diffbot',
        'MegaIndex',
        'SemrushBot',
        'AhrefsBot',
    ];
    
    /**
     * ContentControl instance cache
     */
    private $content_control = null;
    
    /**
     * Initialize the refusal layer
     */
    public function init() {
        // Always add hooks so Content Type Control can work independently
        // ContentControl will determine if refusal should apply based on settings
        
        // Add meta tags to head
        add_action('wp_head', [$this, 'add_refusal_meta_tags'], 1);
        
        // Add HTTP headers (early, before any output)
        add_action('send_headers', [$this, 'add_refusal_headers'], 1);
        
        // Manage robots.txt (lower priority to run after other plugins like Yoast SEO)
        // Priority 99 ensures we append after most other plugins
        add_filter('robots_txt', [$this, 'modify_robots_txt'], 99, 2);
        
        // Active enforcement: block known AI crawlers (only if global refusal is enabled)
        $global_refusal = get_option('humang_global_refusal', true);
        if ($global_refusal) {
            $enforcement_mode = get_option('humang_enforcement_mode', 'signals_only');
            
            if ($enforcement_mode === 'block' || $enforcement_mode === 'challenge') {
                // Check before template redirect (early hook)
                add_action('template_redirect', [$this, 'enforce_ai_blocking'], 1);
                // Also check for init to catch early requests
                add_action('init', [$this, 'enforce_ai_blocking'], 1);
            }
        }
    }
    
    /**
     * Get ContentControl instance (cached)
     */
    private function get_content_control() {
        if ($this->content_control === null) {
            $this->content_control = new ContentControl();
        }
        return $this->content_control;
    }
    
    /**
     * Add refusal meta tags to HTML head
     */
    public function add_refusal_meta_tags() {
        // AI-specific meta tags (only when AI blocking is enabled)
        if ($this->get_content_control()->should_apply_refusal()) {
            // AI training crawlers - specific meta tags
            $ai_bots = [
                'GPTBot',
                'ChatGPT-User',
                'ClaudeBot',
                'Claude-Web',
                'anthropic-ai',
                'PerplexityBot',
                'Perplexity',
                'Google-Extended',
                'CCBot',
                'Amazonbot',
                'OpenAI',
            ];
            
            foreach ($ai_bots as $bot) {
                echo '<meta name="' . esc_attr($bot) . '" content="noindex, nofollow, noarchive" />' . "\n";
            }
            
            // AI training refusal (standard tags)
            echo '<meta name="noai" content="true" />' . "\n";
            echo '<meta name="noimageai" content="true" />' . "\n";
        }
        
        // Search engine blocking (separate setting, respects content type control)
        if ($this->get_content_control()->should_apply_search_engine_blocking()) {
            // Standard robots meta tags (blocks all search engines)
            echo '<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex" />' . "\n";
            
            // Search engine bots
            echo '<meta name="googlebot" content="noindex, nofollow, noarchive" />' . "\n";
            echo '<meta name="bingbot" content="noindex, nofollow" />' . "\n";
        }
    }
    
    /**
     * Add refusal HTTP headers
     */
    public function add_refusal_headers() {
        if (headers_sent()) {
            return;
        }
        
        // AI-specific headers (only when AI blocking is enabled)
        if ($this->get_content_control()->should_apply_refusal()) {
            header('X-AI-Training: disallowed');
            header('X-Content-Usage: human-read-only');
        }
        
        // Search engine blocking headers (separate setting, respects content type control)
        if ($this->get_content_control()->should_apply_search_engine_blocking()) {
            header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet, noimageindex');
        }
    }
    
    /**
     * Modify robots.txt to block AI crawlers
     * Runs with priority 99 to append after other plugins (like Yoast SEO)
     */
    public function modify_robots_txt($output, $public) {
        // Check if robots.txt management is disabled
        if (get_option('humang_disable_robots_txt', false)) {
            return $output; // Let other plugins manage it
        }
        
        if (!$public) {
            return $output;
        }
        
        // Check if HumanGate section already exists (prevent duplication)
        if (!empty($output) && strpos($output, '# HumanGate Plugin') !== false) {
            return $output; // Already added, don't duplicate
        }
        
        // Only block AI training crawlers, not search engines
        // Note: Googlebot and Bingbot are search engines, not AI crawlers
        $ai_crawlers = [
            'User-agent: GPTBot',
            'Disallow: /',
            '',
            'User-agent: ChatGPT-User',
            'Disallow: /',
            '',
            'User-agent: ClaudeBot',
            'Disallow: /',
            '',
            'User-agent: Claude-Web',
            'Disallow: /',
            '',
            'User-agent: anthropic-ai',
            'Disallow: /',
            '',
            'User-agent: PerplexityBot',
            'Disallow: /',
            '',
            'User-agent: Perplexity',
            'Disallow: /',
            '',
            'User-agent: CCBot',
            'Disallow: /',
            '',
            'User-agent: Amazonbot',
            'Disallow: /',
            '',
            'User-agent: Google-Extended',
            'Disallow: /',
            '',
            'User-agent: OpenAI',
            'Disallow: /',
        ];
        
        $ai_block = implode("\n", $ai_crawlers);
        
        // Append to existing robots.txt (compatible with Yoast SEO, RankMath, etc.)
        // We append with a clear section marker so users can see what was added
        if (!empty($output)) {
            // Remove trailing whitespace from existing output
            $output = rtrim($output);
            return $output . "\n\n# HumanGate Plugin - AI Crawler Blocks\n" . $ai_block;
        }
        
        // If no existing robots.txt, create a minimal one
        return "User-agent: *\nAllow: /\n\n# HumanGate Plugin - AI Crawler Blocks\n" . $ai_block;
    }
    
    /**
     * Detect if robots.txt managing plugins are active
     */
    public static function detect_robots_txt_plugins() {
        $detected = [];
        
        // Yoast SEO
        if (defined('WPSEO_VERSION') || class_exists('WPSEO_Options')) {
            $detected[] = 'Yoast SEO';
        }
        
        // Rank Math
        if (defined('RANK_MATH_VERSION') || class_exists('RankMath')) {
            $detected[] = 'Rank Math';
        }
        
        // All in One SEO
        if (defined('AIOSEO_VERSION') || class_exists('AIOSEO')) {
            $detected[] = 'All in One SEO';
        }
        
        // SEOPress
        if (defined('SEOPRESS_VERSION') || function_exists('seopress_get_service')) {
            $detected[] = 'SEOPress';
        }
        
        // The SEO Framework
        if (defined('THE_SEO_FRAMEWORK_VERSION') || class_exists('The_SEO_Framework\Load')) {
            $detected[] = 'The SEO Framework';
        }
        
        // Squirrly SEO
        if (defined('SQ_VERSION') || class_exists('SQ_Classes_FrontController')) {
            $detected[] = 'Squirrly SEO';
        }
        
        // SEO Ultimate
        if (defined('SU_VERSION') || class_exists('SEO_Ultimate')) {
            $detected[] = 'SEO Ultimate';
        }
        
        // WP Meta SEO
        if (defined('WPMS_VERSION') || class_exists('WP_Meta_SEO')) {
            $detected[] = 'WP Meta SEO';
        }
        
        return $detected;
    }
    
    /**
     * Actively enforce blocking of known AI crawlers
     */
    public function enforce_ai_blocking() {
        // Skip for admin, ajax, cron, and logged-in users
        if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
            return;
        }
        
        // Allow logged-in users to pass through
        if (is_user_logged_in()) {
            return;
        }
        
        // Check IP whitelist first (performance optimization)
        $ip = $this->get_client_ip();
        if ($ip && $this->is_ip_whitelisted($ip)) {
            return; // Whitelisted IP, allow access
        }
        
        // Sanitize user agent (prevent injection)
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
        
        // Validate user agent length (prevent DoS via huge user agent)
        if (empty($user_agent) || strlen($user_agent) > 512) {
            return;
        }
        
        // Check user agent whitelist (performance optimization)
        if ($this->is_user_agent_whitelisted($user_agent)) {
            return; // Whitelisted user agent, allow access
        }
        
        // Check if user agent matches known AI crawlers (with caching)
        $is_ai_crawler = false;
        $matched_agent = '';
        
        // Use cached pattern matching for performance
        $cache_key = 'humang_ua_check_' . md5($user_agent);
        $cached_result = get_transient($cache_key);
        
        if ($cached_result !== false) {
            // Cache hit - use cached result
            if ($cached_result['is_ai']) {
                $is_ai_crawler = true;
                $matched_agent = $cached_result['matched'];
            }
        } else {
            // Cache miss - perform pattern matching
        foreach ($this->ai_crawler_agents as $agent) {
            if (stripos($user_agent, $agent) !== false) {
                $is_ai_crawler = true;
                $matched_agent = $agent;
                break;
            }
            }
            
            // Cache result for 1 hour (user agents don't change frequently)
            set_transient($cache_key, [
                'is_ai' => $is_ai_crawler,
                'matched' => $matched_agent
            ], HOUR_IN_SECONDS);
        }
        
        if (!$is_ai_crawler) {
            return;
        }
        
        // Verify search engine indexing bots (Googlebot, Bingbot) if verification is enabled
        // This allows search engine indexing while blocking AI training crawlers
        if ($this->should_verify_search_engine_bot($user_agent)) {
            if ($this->verify_search_engine_bot($user_agent)) {
                return; // Verified search engine bot, allow for indexing
            }
        }
        
        $enforcement_mode = get_option('humang_enforcement_mode', 'signals_only');
        
        // Log the block attempt
        $telemetry = new Telemetry();
        $telemetry->increment_blocked('ai_crawler_blocked');
        
        if ($enforcement_mode === 'block') {
            // Hard block: Return 403 Forbidden
            $this->send_block_response($matched_agent);
        } elseif ($enforcement_mode === 'challenge') {
            // Soft block: Redirect to challenge (if friction system is available)
            $this->redirect_to_challenge('ai_crawler');
        }
    }
    
    /**
     * Check if we should verify search engine indexing bots
     * These are bots for search engine indexing (Google, Bing, etc.), NOT AI training crawlers
     */
    private function should_verify_search_engine_bot($user_agent) {
        // Only verify if option is enabled and it's a known search engine bot
        if (!get_option('humang_verify_search_engine_bots', false)) {
            return false;
        }
        
        // Search engine indexing bots (for search results, NOT AI training)
        $search_engine_patterns = [
            'Googlebot',      // Google Search
            'Bingbot',        // Bing Search
            'Slurp',          // Yahoo Search
            'DuckDuckBot',    // DuckDuckGo
            'Baiduspider',    // Baidu
            'YandexBot',      // Yandex
            'Sogou',          // Sogou
        ];
        
        foreach ($search_engine_patterns as $pattern) {
            if (stripos($user_agent, $pattern) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Verify search engine bot by reverse DNS lookup
     * Verifies the bot is actually from the claimed search engine (prevents spoofing)
     * Caches results for 24 hours to reduce expensive DNS lookup latency
     * 
     * @param string $user_agent The user agent string to verify
     * @return bool True if verified as legitimate search engine bot, false otherwise
     */
    private function verify_search_engine_bot($user_agent) {
        $ip = $this->get_client_ip();
        
        if (!$ip) {
            return false;
        }
        
        // Additional validation: ensure IP is public
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return false;
        }
        
        // Create cache key based on IP and user agent
        // Normalize user agent to extract just the bot name for consistent caching
        $bot_name = '';
        if (stripos($user_agent, 'Googlebot') !== false) {
            $bot_name = 'googlebot';
        } elseif (stripos($user_agent, 'Bingbot') !== false) {
            $bot_name = 'bingbot';
        } elseif (stripos($user_agent, 'Slurp') !== false) {
            $bot_name = 'slurp';
        } elseif (stripos($user_agent, 'DuckDuckBot') !== false) {
            $bot_name = 'duckduckbot';
        } else {
            // For other search engines, use a hash of the user agent
            $bot_name = md5($user_agent);
        }
        
        $cache_key = 'humang_dns_verify_' . md5($ip . '_' . $bot_name);
        
        // Check cache first (24 hour expiration)
        $cached_result = get_transient($cache_key);
        if ($cached_result !== false) {
            return (bool) $cached_result;
        }
        
        // Cache miss: perform expensive reverse DNS lookup
        // This verifies the IP actually belongs to the claimed search engine
        $hostname = @gethostbyaddr($ip);
        
        if ($hostname === false || $hostname === $ip) {
            // Cache negative result for 24 hours
            set_transient($cache_key, 0, DAY_IN_SECONDS);
            return false;
        }
        
        // Verify hostname matches expected pattern for each search engine
        $verified = false;
        
        if (stripos($user_agent, 'Googlebot') !== false) {
            // Googlebot should reverse resolve to googlebot.com or google.com
            $verified = (bool) preg_match('/\.google(bot)?\.com$/i', $hostname);
        } elseif (stripos($user_agent, 'Bingbot') !== false) {
            // Bingbot should reverse resolve to search.msn.com
            $verified = (bool) preg_match('/\.search\.msn\.com$/i', $hostname);
        } elseif (stripos($user_agent, 'Slurp') !== false) {
            // Yahoo Slurp should reverse resolve to yahoo.net or yahoo.com
            $verified = (bool) preg_match('/\.yahoo\.(net|com)$/i', $hostname);
        } elseif (stripos($user_agent, 'DuckDuckBot') !== false) {
            // DuckDuckBot should reverse resolve to duckduckgo.com
            $verified = (bool) preg_match('/\.duckduckgo\.com$/i', $hostname);
        } else {
            // For other search engines, allow them if they pass the basic DNS check (hostname != IP)
            $verified = true;
        }
        
        // Cache result for 24 hours (both positive and negative results)
        set_transient($cache_key, $verified ? 1 : 0, DAY_IN_SECONDS);
        
        return $verified;
    }
    
    /**
     * Send 403 Forbidden response to blocked crawler
     */
    private function send_block_response($matched_agent) {
        http_response_code(403);
        header('X-Robots-Tag: noindex, nofollow');
        header('X-AI-Training: disallowed');
        header('Retry-After: 3600');
        
        // Send minimal HTML response
        // Note: Inline styles are used here because this is an error page that exits immediately,
        // so wp_enqueue_style() cannot be used. This is acceptable for error pages.
        echo '<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="robots" content="noindex, nofollow">
    <title>Access Denied</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: #f5f5f5;
            color: #333;
        }
        .container {
            text-align: center;
            padding: 2rem;
        }
        h1 { color: #d63638; }
    </style>
</head>
<body>
    <div class="container">
        <h1>403 - Access Denied</h1>
        <p>AI training crawlers are not permitted on this site.</p>
        <p><small>Detected: ' . esc_html($matched_agent) . '</small></p>
    </div>
</body>
</html>';
        
        exit;
    }
    
    /**
     * Get and validate client IP address
     */
    private function get_client_ip() {
        $ip_keys = [
            'HTTP_CF_CONNECTING_IP',     // Cloudflare
            'HTTP_X_REAL_IP',            // Nginx proxy
            'HTTP_X_FORWARDED_FOR',      // Standard proxy header
            'REMOTE_ADDR',                // Standard
        ];
        
        foreach ($ip_keys as $key) {
            if (!isset($_SERVER[$key])) {
                continue;
            }
            
            $ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
            
            // Handle X-Forwarded-For (can contain multiple IPs)
            if ($key === 'HTTP_X_FORWARDED_FOR') {
                $ips = explode(',', $ip);
                $ip = trim($ips[0]); // Take first IP (original client)
            }
            
            // Validate IP format
            if (filter_var($ip, FILTER_VALIDATE_IP)) {
                return $ip;
            }
        }
        
        return false;
    }
    
    /**
     * Redirect to challenge system (requires SelectiveFriction to be enabled)
     */
    private function redirect_to_challenge($reason) {
        // For challenge mode, we'll use the same challenge page mechanism
        // but we need to check if friction is enabled first
        if (!get_option('humang_friction_enabled', true)) {
            // Fallback to hard block if friction not enabled
            $this->send_block_response('AI Crawler');
            return;
        }
        
        // Get client IP securely
        $ip = $this->get_client_ip();
        if (!$ip) {
            // Can't verify IP, deny access
            $this->send_block_response('AI Crawler');
            return;
        }
        
        // Generate challenge token and nonce
        $challenge_token = wp_create_nonce('humang_challenge');
        $challenge_nonce = wp_create_nonce('humangate_challenge');
        set_transient('humang_challenge_' . $challenge_token, $ip, 300);
        
        // Serve challenge page (same mechanism as friction system)
        http_response_code(200);
        header('Content-Type: text/html; charset=UTF-8');
        
        // Escape URL and nonce for JavaScript context
        $verify_url = esc_js(admin_url('admin-ajax.php?action=humang_verify_challenge&token=' . esc_attr($challenge_token)));
        
        // Prepare translated strings
        $title_text = esc_html__('Verifying...', 'humangate');
        $verifying_text = esc_html__('Verifying browser...', 'humangate');
        $failed_text = esc_html__('Verification failed. Please try again.', 'humangate');
        $error_text = esc_html__('Verification error. Please refresh the page.', 'humangate');
        
        // Reuse the same challenge page HTML from friction system
        // Note: Inline styles and scripts are used here because this is a challenge page that exits immediately,
        // so wp_enqueue_style() and wp_enqueue_script() cannot be used. This is acceptable for error/challenge pages.
        echo '<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="noindex, nofollow">
    <title>' . esc_html($title_text) . '</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: #f5f5f5;
            color: #333;
        }
        .container {
            text-align: center;
            padding: 2rem;
        }
        .spinner {
            border: 3px solid #f3f3f3;
            border-top: 3px solid #333;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 1rem;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="spinner"></div>
        <p>' . esc_html($verifying_text) . '</p>
    </div>
    <script>
        (function() {
            function generateToken() {
                const data = {
                    timestamp: Date.now(),
                    random: Math.random().toString(36).substring(2),
                    performance: performance.now(),
                    screen: window.screen.width + "x" + window.screen.height,
                    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
                    languages: navigator.languages.join(",")
                };
                const str = JSON.stringify(data);
                let hash = 0;
                for (let i = 0; i < str.length; i++) {
                    const char = str.charCodeAt(i);
                    hash = ((hash << 5) - hash) + char;
                    hash = hash & hash;
                }
                return btoa(str).substring(0, 32) + Math.abs(hash).toString(36);
            }
            
            const token = generateToken();
            const verifyUrl = ' . wp_json_encode($verify_url) . ';
            const challengeNonce = ' . wp_json_encode($challenge_nonce) . ';
            
            fetch(verifyUrl, {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                body: "browser_token=" + encodeURIComponent(token) + "&humangate_challenge_nonce=" + encodeURIComponent(challengeNonce)
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    window.location.reload();
                } else {
                    document.querySelector(".container p").textContent = ' . wp_json_encode($failed_text) . ';
                }
            })
            .catch(error => {
                document.querySelector(".container p").textContent = ' . wp_json_encode($error_text) . ';
            });
        })();
    </script>
</body>
</html>';
        
        exit;
    }

    /**
     * Check if IP is whitelisted
     * 
     * @param string $ip IP address to check
     * @return bool True if IP is whitelisted
     */
    public function is_ip_whitelisted($ip) {
        if (empty($ip)) {
            return false;
        }
        
        // Get whitelist from options (cached per request)
        static $whitelist_cache = null;
        if ($whitelist_cache === null) {
            $whitelist_raw = get_option('humang_whitelist_ips', '');
            $whitelist_cache = array_filter(array_map('trim', explode("\n", $whitelist_raw)));
        }
        
        if (empty($whitelist_cache)) {
            return false;
        }
        
        // Check exact match and CIDR ranges
        foreach ($whitelist_cache as $whitelist_entry) {
            $whitelist_entry = trim($whitelist_entry);
            if (empty($whitelist_entry)) {
                continue;
            }
            
            // Exact match
            if ($ip === $whitelist_entry) {
                return true;
            }
            
            // CIDR range match
            if (strpos($whitelist_entry, '/') !== false) {
                if ($this->ip_in_cidr($ip, $whitelist_entry)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    /**
     * Check if user agent is whitelisted
     * 
     * @param string $user_agent User agent string to check
     * @return bool True if user agent is whitelisted
     */
    public function is_user_agent_whitelisted($user_agent) {
        if (empty($user_agent)) {
            return false;
        }
        
        // Get whitelist from options (cached per request)
        static $whitelist_cache = null;
        if ($whitelist_cache === null) {
            $whitelist_raw = get_option('humang_whitelist_user_agents', '');
            $whitelist_cache = array_filter(array_map('trim', explode("\n", $whitelist_raw)));
        }
        
        if (empty($whitelist_cache)) {
            return false;
        }
        
        // Check if user agent contains any whitelisted pattern
        foreach ($whitelist_cache as $pattern) {
            $pattern = trim($pattern);
            if (empty($pattern)) {
                continue;
            }
            
            if (stripos($user_agent, $pattern) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Check if IP is within CIDR range
     * 
     * @param string $ip IP address to check
     * @param string $cidr CIDR range (e.g., "192.168.1.0/24")
     * @return bool True if IP is in range
     */
    private function ip_in_cidr($ip, $cidr) {
        list($subnet, $mask) = explode('/', $cidr);
        
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            // IPv4
            $ip_long = ip2long($ip);
            $subnet_long = ip2long($subnet);
            
            // Validate that both IPs converted successfully
            if ($ip_long === false || $subnet_long === false) {
                return false;
            }
            
            $mask_long = -1 << (32 - (int)$mask);
            return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
        } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            // IPv6 (simplified check)
            $ip_bin = inet_pton($ip);
            $subnet_bin = inet_pton($subnet);
            
            // Validate that both IPs converted successfully
            if ($ip_bin === false || $subnet_bin === false) {
                return false;
            }
            
            $mask_bits = (int)$mask;
            $bytes = (int)($mask_bits / 8);
            $bits = $mask_bits % 8;
            
            if (strncmp($ip_bin, $subnet_bin, $bytes) !== 0) {
                return false;
            }
            
            if ($bits > 0) {
                $mask_byte = 0xFF << (8 - $bits);
                return (ord($ip_bin[$bytes]) & $mask_byte) === (ord($subnet_bin[$bytes]) & $mask_byte);
            }
            
            return true;
        }
        
        return false;
    }
}

