<?php
/**
 * Service Allowlist Controller
 *
 * Manages allowlisting for trusted services (monitoring tools, crawlers, etc.)
 * based on User-Agent verification and optional DNS verification.
 *
 * Note: Unlike IP-based whitelisting, service allowlisting uses User-Agent
 * matching and DNS verification to identify legitimate services without
 * hardcoding potentially outdated IP addresses.
 *
 * @package VMPFence
 * @since 2.0.0
 * @since 2.2.4 Added detection for Facebook user traffic (not just crawler)
 */

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

/**
 * Service Allowlist Controller Class
 *
 * @since 2.0.0
 */
class VMPFence_Service_Allowlist_Controller {

    /**
     * Cache for service verification results
     *
     * @var array
     */
    private static $cache = array();

    /**
     * Service definitions with IP ranges and verification methods
     *
     * @var array
     */
    private static $service_patterns = array(
        'google' => array(
            'patterns' => array(
                'Googlebot',
                'Googlebot-Image',
                'Googlebot-News',
                'Googlebot-Video',
                'Google-InspectionTool',
                'GoogleOther',
                'Google-Extended',
                'Google-Safety'
            ),
            'dns_verify' => true, // Use DNS verification instead of IP ranges
            'dns_suffix' => array('.googlebot.com', '.google.com'),
            'ip_ranges' => array() // Empty - use DNS verification
        ),
        'facebook' => array(
            'patterns' => array(
                'facebookexternalhit',
                'facebookcatalog',
                'FacebookBot',
                'meta-externalagent',
                'meta-externalfetcher',
                'meta-webindexer',
                'meta-externalads'
            ),
            'dns_verify' => false,
            'ip_ranges' => array(
                '31.13.24.0/21',
                '31.13.64.0/18',
                '66.220.144.0/20',
                '69.63.176.0/20',
                '69.171.224.0/19',
                '74.119.76.0/22',
                '103.4.96.0/22',
                '157.240.0.0/17',
                '173.252.64.0/18',
                '179.60.192.0/22',
                '185.60.216.0/22',
                '204.15.20.0/22',
                '2620:0:1c00::/40',
                '2a03:2880::/32'
            )
        ),
        'uptime' => array(
            'patterns' => array(
                'UptimeRobot'
            ),
            'dns_verify' => false,
            'ip_ranges' => array(
                '46.137.190.132',
                '122.248.234.23',
                '178.62.52.237',
                '46.101.250.135',
                '178.62.94.63',
                '159.203.30.41',
                '188.166.23.127',
                '138.197.181.60',
                '138.68.156.251',
                '104.131.107.63',
                '159.203.59.141',
                '159.203.63.62',
                '138.197.165.42',
                '146.185.157.14',
                '159.203.32.222',
                '54.79.28.129',
                '5.196.67.212',
                '188.165.253.77',
                '178.62.86.30',
                '94.130.165.177',
                '176.58.111.196',
                '54.94.157.33',
                '138.68.17.194',
                '68.183.133.240',
                '159.65.129.45'
            )
        ),
        'statuscake' => array(
            'patterns' => array(
                'StatusCake'
            ),
            'dns_verify' => false,
            'ip_ranges' => array(
                '185.39.140.0/22',
                '185.62.201.0/24',
                '91.216.254.0/24',
                '109.74.192.0/20',
                '46.165.240.0/20',
                '2a00:1768::/32',
                '2a00:1c98::/32'
            )
        ),
        'managewp' => array(
            'patterns' => array(
                'ManageWP'
            ),
            'dns_verify' => false,
            'ip_ranges' => array(
                '45.79.87.3',
                '50.116.33.29',
                '66.175.211.235',
                '69.164.220.35',
                '72.14.179.8',
                '139.162.122.157',
                '139.162.193.54',
                '178.79.151.69'
            )
        ),
        'seznam' => array(
            'patterns' => array(
                'SeznamBot'
            ),
            'dns_verify' => false,
            'ip_ranges' => array(
                '77.75.72.0/21',
                '77.75.76.0/22',
                '2a02:598::/32'
            )
        ),
        'sucuri' => array(
            'patterns' => array(
                'Sucuri'
            ),
            'dns_verify' => false,
            'ip_ranges' => array(
                '192.88.134.0/23',
                '185.93.228.0/22',
                '2a02:fe80::/29',
                '66.248.200.0/22',
                '208.109.0.0/22'
            )
        )
    );

    /**
     * Check if request is from an allowlisted service
     *
     * @since 2.0.0
     * @since 2.2.4 Added detection for Facebook user traffic (not just crawler)
     * @param string $ip IP address
     * @param string $user_agent User-Agent string
     * @return bool True if allowlisted service, false otherwise
     */
    public static function is_allowlisted_service($ip, $user_agent) {
        // Get enabled services from config
        $services_json = VMPFence_Config::get('whitelisted_services', '{}');
        $services = json_decode($services_json, true);
        if (!is_array($services) || empty($services)) {
            return false;
        }

        // Check cache first
        $cache_key = hash('sha256', $ip . '|' . $user_agent);
        if (isset(self::$cache[$cache_key])) {
            return self::$cache[$cache_key];
        }

        $result = false;

        // Check each enabled service
        foreach ($services as $service_key => $enabled) {
            if (!$enabled) {
                continue; // Service is disabled
            }

            if (!isset(self::$service_patterns[$service_key])) {
                continue; // Unknown service
            }

            $service_config = self::$service_patterns[$service_key];

            // User-Agent is only a hint - MUST verify IP or DNS
            $user_agent_matches = self::matches_user_agent($user_agent, $service_config['patterns']);
            
            if (!$user_agent_matches) {
                continue; // User-Agent doesn't match, skip this service
            }

            // User-Agent matches - now verify IP is legitimate
            if ($service_config['dns_verify']) {
                // Use DNS verification (for Google)
                if (self::verify_dns($ip, $service_config['dns_suffix'])) {
                    $result = true;
                    break;
                } else {
                    continue;
                }
            } else {
                // Use IP range verification
                if (self::verify_ip_range($ip, $service_config['ip_ranges'])) {
                    $result = true;
                    break;
                } else {
                    continue;
                }
            }
        }

        // Cache result
        self::$cache[$cache_key] = $result;

        // If no crawler match, check for Facebook user traffic (regular users clicking Facebook links)
        if (!$result && is_array($services) && isset($services['facebook']) && $services['facebook']) {
            if (self::is_facebook_user_traffic($user_agent)) {
                $result = true;
                self::$cache[$cache_key] = $result;
            }
        }

        return $result;
    }

    /**
     * Check if User-Agent matches any pattern
     *
     * @since 2.0.0
     * @param string $user_agent User-Agent string
     * @param array $patterns Array of patterns to match
     * @return bool True if matches
     */
    private static function matches_user_agent($user_agent, $patterns) {
        if (empty($user_agent)) {
            return false;
        }

        foreach ($patterns as $pattern) {
            if (stripos($user_agent, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Verify IP is in allowed IP ranges
     *
     * @since 2.0.0
     * @param string $ip IP address to verify
     * @param array $ip_ranges Array of IP ranges (CIDR or single IPs)
     * @return bool True if IP is in range, false otherwise
     */
    private static function verify_ip_range($ip, $ip_ranges) {
        if (empty($ip_ranges)) {
            return false;
        }

        foreach ($ip_ranges as $range) {
            $ip_range_obj = new VMPFence_IP_Range($range);
            if ($ip_range_obj->is_ip_in_range($ip)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Verify IP via reverse DNS lookup
     *
     *  DNS verification:
     * 1. Reverse DNS lookup: IP -> hostname
     * 2. Verify hostname ends with allowed suffix
     * 3. Forward DNS lookup: hostname -> IP
     * 4. Confirm IP matches original
     *
     * @since 2.0.0
     * @param string $ip IP address to verify
     * @param array $allowed_suffixes Array of allowed DNS suffixes (e.g., ['.googlebot.com'])
     * @return bool True if verified, false otherwise
     */
    private static function verify_dns($ip, $allowed_suffixes) {
        // Check reverse DNS cache first (7-day expiry)
        global $wpdb;
        $cache_table = VMPFence_Constants::get_table_name(VMPFence_Constants::TABLE_REVERSE_CACHE);
        $ip_binary = @inet_pton($ip);
        $hostname = null;
        if ($ip_binary !== false) {
            $cache_expiry = time() - (7 * 86400);
            $hostname = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT host FROM {$cache_table} WHERE IP = %s AND lastUpdate > %d",
                    $ip_binary,
                    $cache_expiry
                )
            );
        }

        // Original logic below - Step 1: Reverse DNS lookup (use cache if available)
        if ($hostname === null) {
            $hostname = gethostbyaddr($ip);
            // Cache the lookup result
            if ($ip_binary !== false) {
                $cache_host = ($hostname === $ip) ? '' : $hostname;
                $wpdb->query(
                    $wpdb->prepare(
                        "INSERT INTO {$cache_table} (IP, host, lastUpdate) 
                         VALUES (%s, %s, %d)
                         ON DUPLICATE KEY UPDATE host = %s, lastUpdate = %d",
                        $ip_binary, $cache_host, time(),
                        $cache_host, time()
                    )
                );
            }
        }
        
        if ($hostname === $ip || $hostname === '' || $hostname === null) {
            // No reverse DNS record found
            return false;
        }

        // Step 2: Verify hostname ends with allowed suffix
        $hostname_verified = false;
        foreach ($allowed_suffixes as $suffix) {
            if (substr($hostname, -strlen($suffix)) === $suffix) {
                $hostname_verified = true;
                break;
            }
        }

        if (!$hostname_verified) {
            return false;
        }

        // Step 3: Forward DNS lookup
        $forward_ip = gethostbyname($hostname);
        
        if ($forward_ip === $hostname) {
            // Forward lookup failed
            return false;
        }

        // Step 4: Verify IPs match
        if ($forward_ip !== $ip) {
            return false;
        }
        return true;
    }

    /**
     * Clear verification cache
     *
     * @since 2.0.0
     */
    public static function clear_cache() {
        self::$cache = array();
    }

    /**
     * Check if request is from Facebook user traffic (not crawler)
     * Detects users clicking links from Facebook posts, ads, messenger, etc.
     *
     * @since 2.2.4
     * @param string $user_agent User-Agent string
     * @return bool True if Facebook user traffic detected
     */
    public static function is_facebook_user_traffic($user_agent) {
        $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
        $query_string = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
                
        // METHOD 1: Check for fbclid parameter (Facebook Click ID - most reliable)
        // Added by Facebook ads, sponsored posts, and organic post clicks
        if (isset($_GET['fbclid']) || strpos($query_string, 'fbclid=') !== false) {
            return true;
        }
        
        // METHOD 2: Check for Facebook UTM parameters
        if ((isset($_GET['utm_source']) && $_GET['utm_source'] === 'facebook') ||
            (isset($_GET['utm_source']) && $_GET['utm_source'] === 'fb') ||
            strpos($query_string, 'utm_source=facebook') !== false ||
            strpos($query_string, 'utm_source=fb') !== false) {
            return true;
        }
        
        // METHOD 3: Check User-Agent for Facebook app indicators
        // FBAN, FBAV, FB_IAB, FB4A (Android), FBIOS (iPhone)
        if (stripos($user_agent, 'FBAN') !== false || 
            stripos($user_agent, 'FBAV') !== false ||
            stripos($user_agent, 'FB_IAB') !== false ||
            stripos($user_agent, 'FB4A') !== false ||
            stripos($user_agent, 'FBIOS') !== false) {
            return true;
        }
        
        // METHOD 4: Check HTTP referrer for Facebook domains
        if (!empty($referrer)) {
            $facebook_domains = array(
                'facebook.com',
                'fb.com',
                'l.facebook.com',      // Main redirect domain
                'm.facebook.com',      // Mobile web
                'lm.facebook.com',     // Mobile redirect
                'www.facebook.com',
                'web.facebook.com',
                'messenger.com',       // Messenger clicks
                'business.facebook.com'
            );
            
            $referrer_host = parse_url($referrer, PHP_URL_HOST);
            if ($referrer_host) {
                foreach ($facebook_domains as $fb_domain) {
                    // Exact match OR subdomain match
                    if ($referrer_host === $fb_domain || 
                        substr($referrer_host, -(strlen($fb_domain) + 1)) === '.' . $fb_domain) {
                        return true;
                    }
                }
            }
        }
        
        return false;
    }

    /**
     * Get list of available services
     *
     * @since 2.0.0
     * @return array Service definitions with keys: name, default_enabled
     */
    public static function get_available_services() {
        return array(
            'sucuri' => array(
                'name' => 'Sucuri',
                'default_enabled' => true
            ),
            'facebook' => array(
                'name' => 'Facebook',
                'default_enabled' => true
            ),
            'uptime' => array(
                'name' => 'Uptime Robot',
                'default_enabled' => true
            ),
            'statuscake' => array(
                'name' => 'StatusCake',
                'default_enabled' => true
            ),
            'managewp' => array(
                'name' => 'ManageWP',
                'default_enabled' => false
            ),
            'seznam' => array(
                'name' => 'Seznam Search Engine',
                'default_enabled' => true
            ),
            'google' => array(
                'name' => 'Google Search Engine',
                'default_enabled' => true
            )
        );
    }
}
