<?php
/**
 * VMPFence IP Allowlist Helper Class
 * 
 * Handles IP address matching and validation for Login Security features.
 * Supports multiple IP formats: single IPs, CIDR notation, dash ranges, and bracket ranges.
 * 
 * @package VMPFence
 * @subpackage Login_Security
 * @since Phase 7
 */

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

class VMPFence_IP_Allowlist {
    
    /**
     * Check if an IP address is in the allowlist
     * 
     * @param string $ip The IP address to check
     * @param string|array $allowlist Optional allowlist (defaults to database config)
     * @return bool True if IP is allowlisted, false otherwise
     */
    public static function is_ip_allowlisted($ip, $allowlist = null) {
        // Get allowlist from database if not provided
        if ($allowlist === null) {
            $allowlist = VMPFence_Config::get('vmpfence_ip_allowlist', '');
        }
        
        // Convert string to array if needed
        if (is_string($allowlist)) {
            $allowlist = array_filter(array_map('trim', explode("\n", $allowlist)));
        }
        
        // Empty allowlist = no IPs allowed
        if (empty($allowlist)) {
            return false;
        }
        
        // Normalize the IP
        $ip = trim($ip);
        if (empty($ip)) {
            return false;
        }
        
        // Check each pattern in the allowlist
        foreach ($allowlist as $pattern) {
            $pattern = trim($pattern);
            if (empty($pattern)) {
                continue;
            }
            
            if (self::ip_matches_pattern($ip, $pattern)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Validate IP list input from textarea
     * Returns WP_Error with line numbers on validation failure
     * 
     * @param string $ip_list Newline-separated IP list
     * @return true|WP_Error True on success, WP_Error on validation failure
     */
    public static function validate_ip_list($ip_list) {
        // Empty list is valid
        if (empty($ip_list)) {
            return true;
        }
        
        $lines = explode("\n", $ip_list);
        $errors = array();
        
        foreach ($lines as $line_num => $line) {
            $line = trim($line);
            
            // Skip empty lines
            if (empty($line)) {
                continue;
            }
            
            // Validate format
            if (!self::is_valid_ip_format($line)) {
                $errors[] = sprintf(
                    __('Line %d: Invalid IP format "%s"', 'vmpfence-security'),
                    $line_num + 1,
                    esc_html($line)
                );
            }
        }
        
        if (!empty($errors)) {
            return new WP_Error('invalid_ip_format', implode('<br>', $errors));
        }
        
        return true;
    }
    
    /**
     * Check if an IP matches a specific pattern
     * 
     * @param string $ip The IP address to check
     * @param string $pattern The pattern to match against (single IP, CIDR, dash range, bracket range)
     * @return bool True if IP matches pattern
     */
    public static function ip_matches_pattern($ip, $pattern) {
        // Exact match (single IP)
        if ($ip === $pattern) {
            return true;
        }
        
        // CIDR notation (e.g., 192.168.1.0/24)
        if (strpos($pattern, '/') !== false) {
            return self::match_cidr($ip, $pattern);
        }
        
        // Dash range (e.g., 192.168.1.1-192.168.1.100)
        if (strpos($pattern, '-') !== false && strpos($pattern, '[') === false) {
            return self::match_dash_range($ip, $pattern);
        }
        
        // Bracket range (e.g., 192.168.1.[10-50])
        if (strpos($pattern, '[') !== false) {
            return self::match_bracket_range($ip, $pattern);
        }
        
        return false;
    }
    
    /**
     * Validate if a string is a valid IP format
     * 
     * @param string $format The format to validate
     * @return bool True if valid format
     */
    public static function is_valid_ip_format($format) {
        // Single IP (IPv4 or IPv6)
        if (filter_var($format, FILTER_VALIDATE_IP)) {
            return true;
        }
        
        // CIDR notation
        if (strpos($format, '/') !== false) {
            list($ip, $mask) = explode('/', $format, 2);
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                return is_numeric($mask) && $mask >= 0 && $mask <= 32;
            }
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
                return is_numeric($mask) && $mask >= 0 && $mask <= 128;
            }
            return false;
        }
        
        // Dash range (e.g., 192.168.1.1-192.168.1.100)
        if (strpos($format, '-') !== false && strpos($format, '[') === false) {
            $parts = explode('-', $format, 2);
            return count($parts) === 2 
                && filter_var(trim($parts[0]), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) 
                && filter_var(trim($parts[1]), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
        }
        
        // Bracket range (e.g., 192.168.1.[10-50])
        if (preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.\[(\d{1,3})-(\d{1,3})\]$/', $format, $matches)) {
            $octets = array((int)$matches[1], (int)$matches[2], (int)$matches[3], 0);
            $start = (int)$matches[4];
            $end = (int)$matches[5];
            
            // Validate octets and range
            foreach (array_slice($octets, 0, 3) as $octet) {
                if ($octet < 0 || $octet > 255) {
                    return false;
                }
            }
            
            return $start >= 0 && $end <= 255 && $start <= $end;
        }
        
        return false;
    }
    
    /**
     * Match IP against CIDR notation
     * 
     * @param string $ip The IP to check
     * @param string $cidr CIDR notation (e.g., 192.168.1.0/24)
     * @return bool True if IP is in CIDR range
     */
    private static function match_cidr($ip, $cidr) {
        list($subnet, $mask) = explode('/', $cidr, 2);
        
        // IPv4
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && 
            filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            
            $ip_long = ip2long($ip);
            $subnet_long = ip2long($subnet);
            $mask_long = -1 << (32 - (int)$mask);
            
            return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
        }
        
        // IPv6
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && 
            filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            
            $ip_bin = inet_pton($ip);
            $subnet_bin = inet_pton($subnet);
            $mask_int = (int)$mask;
            
            // Create mask
            $mask_bin = str_repeat('f', $mask_int >> 2);
            switch ($mask_int % 4) {
                case 1: $mask_bin .= '8'; break;
                case 2: $mask_bin .= 'c'; break;
                case 3: $mask_bin .= 'e'; break;
            }
            $mask_bin = str_pad($mask_bin, 32, '0');
            $mask_bin = pack('H*', $mask_bin);
            
            return ($ip_bin & $mask_bin) === ($subnet_bin & $mask_bin);
        }
        
        return false;
    }
    
    /**
     * Match IP against dash range notation
     * 
     * @param string $ip The IP to check
     * @param string $range Dash range (e.g., 192.168.1.1-192.168.1.100)
     * @return bool True if IP is in range
     */
    private static function match_dash_range($ip, $range) {
        $parts = explode('-', $range, 2);
        if (count($parts) !== 2) {
            return false;
        }
        
        $start = trim($parts[0]);
        $end = trim($parts[1]);
        
        // Only support IPv4 for dash ranges
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
            !filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ||
            !filter_var($end, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }
        
        $ip_long = ip2long($ip);
        $start_long = ip2long($start);
        $end_long = ip2long($end);
        
        return $ip_long >= $start_long && $ip_long <= $end_long;
    }
    
    /**
     * Match IP against bracket range notation
     * 
     * @param string $ip The IP to check
     * @param string $pattern Bracket range (e.g., 192.168.1.[10-50])
     * @return bool True if IP matches pattern
     */
    private static function match_bracket_range($ip, $pattern) {
        // Only support IPv4 for bracket ranges
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return false;
        }
        
        // Extract pattern parts: 192.168.1.[10-50]
        if (!preg_match('/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.\[(\d{1,3})-(\d{1,3})\]$/', $pattern, $pattern_matches)) {
            return false;
        }
        
        // Extract IP parts: 192.168.1.25
        $ip_parts = explode('.', $ip);
        if (count($ip_parts) !== 4) {
            return false;
        }
        
        // Check first 3 octets match
        if ($ip_parts[0] !== $pattern_matches[1] ||
            $ip_parts[1] !== $pattern_matches[2] ||
            $ip_parts[2] !== $pattern_matches[3]) {
            return false;
        }
        
        // Check 4th octet is in range
        $fourth_octet = (int)$ip_parts[3];
        $range_start = (int)$pattern_matches[4];
        $range_end = (int)$pattern_matches[5];
        
        return $fourth_octet >= $range_start && $fourth_octet <= $range_end;
    }
    
    /**
     * Convert IP to comparable format for sorting/comparing
     * 
     * @param string $ip The IP address
     * @return string|false Comparable format or false on failure
     */
    private static function ip_to_comparable($ip) {
        // IPv4
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return sprintf('%03d.%03d.%03d.%03d', ...explode('.', $ip));
        }
        
        // IPv6
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            return inet_pton($ip);
        }
        
        return false;
    }
}
