<?php
/**
 * Secure Logger - Zero-Trust Logging System
 * 
 * Automatically sanitizes sensitive data (API keys, IPs, emails, passwords) 
 * before writing to logs. Prevents PII and credential leakage.
 *
 * @package    VMPFence
 * @subpackage Lib
 * @since      2.0.0
 * @since 2.2.2 Removed actual error_log calls to prevent accidental logging in production.
 */

if (!defined('ABSPATH')) {
    exit;
}

class VMPFence_Secure_Logger {
    
    /**
     * Log levels
     */
    const LEVEL_ERROR = 'error';
    const LEVEL_WARNING = 'warning';
    const LEVEL_INFO = 'info';
    const LEVEL_DEBUG = 'debug';
    
    /**
     * Sensitive keys to always remove from logged data
     */
    private static $sensitive_keys = [
        'api_key',
        'apikey',
        'api-key',
        'password',
        'passwd',
        'pwd',
        'secret',
        'token',
        'auth',
        'authorization',
        'x-api-key',
        'access_token',
        'refresh_token',
        'client_secret',
        'private_key',
        'google_safebrowsing_api_key'
    ];
    
    /**
     * Check if we're in production mode
     * 
     * @return bool True if production, false if development
     */
    public static function is_production() {
        // Check WordPress debug mode
        if (defined('WP_DEBUG') && WP_DEBUG === true) {
            return false;
        }
        
        // Check environment setting
        $env = get_option('vmpfence_environment', 'production');
        return $env === 'production';
    }
    
    /**
     * Get current log level setting
     * 
     * @return string Current log level (error, warning, info, debug)
     */
    public static function get_log_level() {
        $level = get_option('vmpfence_log_level', 'error');
        
        // In production, always limit to error level
        if (self::is_production() && $level !== self::LEVEL_ERROR) {
            return self::LEVEL_ERROR;
        }
        
        return $level;
    }
    
    /**
     * Check if a log level should be logged based on current settings
     * 
     * @param string $level Log level to check
     * @return bool True if should log
     */
    private static function should_log($level) {
        $current_level = self::get_log_level();
        
        $levels = [
            self::LEVEL_ERROR => 1,
            self::LEVEL_WARNING => 2,
            self::LEVEL_INFO => 3,
            self::LEVEL_DEBUG => 4
        ];
        
        $current_priority = isset($levels[$current_level]) ? $levels[$current_level] : 1;
        $message_priority = isset($levels[$level]) ? $levels[$level] : 4;
        
        return $message_priority <= $current_priority;
    }
    
    /**
     * Main logging method - automatically sanitizes all data
     * 
     * @param string $level Log level (error, warning, info, debug)
     * @param string $message Log message
     * @param array $context Additional context data (will be sanitized)
     */
    public static function log($level, $message, $context = []) {
        // Check if we should log this level
        if (!self::should_log($level)) {
            return;
        }
        
        // Sanitize message
        $sanitized_message = self::sanitize_string($message);
        
        // Sanitize context
        $sanitized_context = self::sanitize_for_logging($context);
        
        // Build log entry
        $log_entry = strtoupper($level) . ': ' . $sanitized_message;
        
        // Add context if present
        if (!empty($sanitized_context)) {
            $log_entry .= ' | Context: ' . json_encode($sanitized_context);
        }
        
        // Write to WordPress error log (only if logging is enabled)
        // In production with error level, this is the only place error_log should be used
        if (function_exists('error_log') && (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG || !self::is_production())) {
            // error_log('[VMPFence] ' . $log_entry);
        }
    }
    
    /**
     * Convenience methods for different log levels
     */
    public static function error($message, $context = []) {
        self::log(self::LEVEL_ERROR, $message, $context);
    }
    
    public static function warning($message, $context = []) {
        self::log(self::LEVEL_WARNING, $message, $context);
    }
    
    public static function info($message, $context = []) {
        self::log(self::LEVEL_INFO, $message, $context);
    }
    
    public static function debug($message, $context = []) {
        self::log(self::LEVEL_DEBUG, $message, $context);
    }
    
    /**
     * Recursively sanitize data for logging
     * 
     * @param mixed $data Data to sanitize
     * @return mixed Sanitized data
     */
    public static function sanitize_for_logging($data) {
        if (is_array($data)) {
            return self::sanitize_array($data);
        } elseif (is_object($data)) {
            return self::sanitize_object($data);
        } elseif (is_string($data)) {
            return self::sanitize_string($data);
        }
        
        return $data;
    }
    
    /**
     * Sanitize array - remove sensitive keys and sanitize values
     * 
     * @param array $array Array to sanitize
     * @return array Sanitized array
     */
    private static function sanitize_array($array) {
        $sanitized = [];
        
        foreach ($array as $key => $value) {
            $key_lower = strtolower($key);
            
            // Skip sensitive keys
            if (self::is_sensitive_key($key_lower)) {
                $sanitized[$key] = '***REDACTED***';
                continue;
            }
            
            // Recursively sanitize value
            $sanitized[$key] = self::sanitize_for_logging($value);
        }
        
        return $sanitized;
    }
    
    /**
     * Sanitize object - convert to array and sanitize
     * 
     * @param object $object Object to sanitize
     * @return array Sanitized data
     */
    private static function sanitize_object($object) {
        // Convert to array
        $array = json_decode(json_encode($object), true);
        return self::sanitize_array($array);
    }
    
    /**
     * Sanitize string - remove sensitive patterns
     * 
     * @param string $string String to sanitize
     * @return string Sanitized string
     */
    public static function sanitize_string($string) {
        // Mask IP addresses
        $string = self::mask_ip_addresses($string);
        
        // Mask API keys (40+ char alphanumeric strings)
        $string = self::mask_api_keys($string);
        
        // Mask email addresses
        $string = self::mask_emails($string);
        
        // Sanitize URLs (remove query params with sensitive data)
        $string = self::sanitize_urls($string);
        
        // Mask file paths (replace site root)
        $string = self::mask_file_paths($string);
        
        return $string;
    }
    
    /**
     * Check if a key is sensitive
     * 
     * @param string $key Key to check
     * @return bool True if sensitive
     */
    private static function is_sensitive_key($key) {
        $key_lower = strtolower($key);
        
        foreach (self::$sensitive_keys as $sensitive) {
            if (strpos($key_lower, $sensitive) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Mask IP addresses in string
     * 
     * @param string $string String containing IPs
     * @return string String with masked IPs
     */
    public static function mask_ip_addresses($string) {
        // IPv4 pattern
        $pattern_ipv4 = '/\b(?:\d{1,3}\.){3}\d{1,3}\b/';
        $string = preg_replace_callback($pattern_ipv4, function($matches) {
            return self::mask_ip_address($matches[0]);
        }, $string);
        
        // IPv6 pattern (simplified)
        $pattern_ipv6 = '/\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b/';
        $string = preg_replace($pattern_ipv6, 'xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx', $string);
        
        return $string;
    }
    
    /**
     * Mask a single IP address
     * 
     * @param string $ip IP address to mask
     * @return string Masked IP (shows first 2 octets)
     */
    public static function mask_ip_address($ip) {
        // Skip localhost
        if ($ip === '127.0.0.1' || $ip === '::1') {
            return $ip;
        }
        
        // Skip private IPs if in development (supports both IPv4 and IPv6)
        if (!self::is_production()) {
            // FILTER_FLAG_NO_PRIV_RANGE returns FALSE for private IPs (both IPv4 and IPv6)
            if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
                return $ip;
            }
        }

        
        // Mask public IPs
        // Check if IPv4 (contains dots)
        if (strpos($ip, '.') !== false) {
            $parts = explode('.', $ip);
            if (count($parts) === 4) {
                return $parts[0] . '.' . $parts[1] . '.xxx.xxx';
            }
            return 'xxx.xxx.xxx.xxx';
        }
        
        // IPv6 masking (contains colons)
        if (strpos($ip, ':') !== false) {
            // Expand compressed IPv6 notation first
            $expanded = inet_pton($ip);
            if ($expanded && strlen($expanded) === 16) {
                // Convert back to full notation
                $hex = bin2hex($expanded);
                $groups = str_split($hex, 4);
                $full_ipv6 = implode(':', $groups);
                
                // Show first 2 groups and mask the rest
                $parts = explode(':', $full_ipv6);
                return $parts[0] . ':' . $parts[1] . ':xxxx:xxxx:xxxx:xxxx:xxxx:xxxx';
            }
        }
        
        return 'xxx.xxx.xxx.xxx';
    }
    
    /**
     * Mask API keys in string (40+ character alphanumeric strings)
     * 
     * @param string $string String containing API keys
     * @return string String with masked keys
     */
    public static function mask_api_keys($string) {
        // Pattern: 40+ alphanumeric characters (typical API key)
        $pattern = '/\b[A-Za-z0-9_-]{40,}\b/';
        
        $string = preg_replace_callback($pattern, function($matches) {
            return self::mask_api_key($matches[0]);
        }, $string);
        
        return $string;
    }
    
    /**
     * Mask a single API key
     * 
     * @param string $key API key to mask
     * @return string Masked key (shows only last 6 chars)
     */
    public static function mask_api_key($key) {
        if (strlen($key) <= 6) {
            return '***';
        }
        
        return '***' . substr($key, -6);
    }
    
    /**
     * Mask email addresses
     * 
     * @param string $string String containing emails
     * @return string String with masked emails
     */
    public static function mask_emails($string) {
        $pattern = '/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/';
        
        $string = preg_replace_callback($pattern, function($matches) {
            $email = $matches[0];
            $parts = explode('@', $email);
            
            if (count($parts) === 2) {
                $user = $parts[0];
                $domain = $parts[1];
                
                // Mask username (show first char only)
                $masked_user = substr($user, 0, 1) . '***';
                
                // Mask domain (show TLD only)
                $domain_parts = explode('.', $domain);
                if (count($domain_parts) >= 2) {
                    $tld = array_pop($domain_parts);
                    return $masked_user . '@***.'. $tld;
                }
                
                return $masked_user . '@***';
            }
            
            return '***@***';
        }, $string);
        
        return $string;
    }
    
    /**
     * Sanitize URLs - remove sensitive query parameters
     * 
     * @param string $string String containing URLs
     * @return string String with sanitized URLs
     */
    public static function sanitize_urls($string) {
        // First, sanitize full URLs (http:// or https://)
        $pattern_full_url = '/(https?:\/\/[^\s]+)/i';
        
        $string = preg_replace_callback($pattern_full_url, function($matches) {
            return self::sanitize_single_url($matches[0]);
        }, $string);
        
        // Second, sanitize relative paths with query strings (e.g., "api/endpoint?api_key=xxx")
        // Pattern: word characters, slashes, dashes, then ? followed by query params
        $pattern_relative = '/([a-zA-Z0-9\/_-]+\?[^\s]+)/';
        
        $string = preg_replace_callback($pattern_relative, function($matches) {
            return self::sanitize_single_url($matches[0]);
        }, $string);
        
        return $string;
    }
    
    /**
     * Sanitize a single URL or path with query string
     * 
     * @param string $url URL or path to sanitize
     * @return string Sanitized URL
     */
    private static function sanitize_single_url($url) {
        // Parse URL (works for both full URLs and relative paths)
        $parsed = parse_url($url);
        
        if (!isset($parsed['query'])) {
            return $url; // No query string, return as-is
        }
        
        parse_str($parsed['query'], $params);
        
        // Sanitize sensitive parameters
        $sanitized_params = [];
        foreach ($params as $key => $value) {
            if (self::is_sensitive_key($key)) {
                $sanitized_params[$key] = '[REDACTED]';
            } else {
                $sanitized_params[$key] = $value;
            }
        }
        
        // Rebuild URL
        $base_url = '';
        
        // Add scheme and host if present (full URL)
        if (isset($parsed['scheme'])) {
            $base_url = $parsed['scheme'] . '://';
        }
        if (isset($parsed['host'])) {
            $base_url .= $parsed['host'];
        }
        
        // Add path
        if (isset($parsed['path'])) {
            $base_url .= $parsed['path'];
        }
        
        // Add sanitized query string
        if (!empty($sanitized_params)) {
            $base_url .= '?' . http_build_query($sanitized_params);
        }
        
        return $base_url;
    }
    
    /**
     * Mask file paths - replace absolute paths with relative
     * 
     * @param string $string String containing file paths
     * @return string String with masked paths
     */
    public static function mask_file_paths($string) {
        // Get site root paths
        $abspath = defined('ABSPATH') ? rtrim(ABSPATH, '/\\') : '';
        $wp_content = defined('WP_CONTENT_DIR') ? rtrim(WP_CONTENT_DIR, '/\\') : '';
        
        if ($abspath) {
            // Replace Windows paths
            $string = str_replace(str_replace('/', '\\', $abspath), '{SITE_ROOT}', $string);
            // Replace Unix paths
            $string = str_replace($abspath, '{SITE_ROOT}', $string);
        }
        
        if ($wp_content) {
            // Replace Windows paths
            $string = str_replace(str_replace('/', '\\', $wp_content), '{WP_CONTENT}', $string);
            // Replace Unix paths
            $string = str_replace($wp_content, '{WP_CONTENT}', $string);
        }
        
        return $string;
    }
    
}
