<?php
/**
 * Public Files Scanner
 *
 * @package VMPFence
 * @since 1.0.0
 * @since 2.2.4 SQL Query fix
 */
//Prevent Direct Access
if(!defined('ABSPATH')){
    exit;
}

// Include centralized exclusion manager
require_once plugin_dir_path(__FILE__) . 'class-scan-exclusion-manager.php';

class VMPFence_Public_Files_Scanner {
    
    private $debug_log = array();
    private $scan_options = null;
    
    /**
     * Centralized exclusion manager for consistent exclusion logic
     */
    private $exclusion_manager;
    
    public function __construct() {
        $this->debug_log[] = 'Public Files Scanner initialized';
        
        // Initialize centralized exclusion manager
        $this->exclusion_manager = VMPFence_Scan_Exclusion_Manager::getInstance();
    }
    
    public function scan($scan_options = null) {
        // Initialize performance controls if available
        $use_performance_controls = false;
        if (class_exists('VMPFence_Performance_Manager')) {
            VMPFence_Performance_Manager::init();
            $use_performance_controls = true;
            $this->debug_log[] = 'Public Files Scanner: Performance controls initialized';
        }
        
        // If scan options provided, respect them
        if ($scan_options) {
            $this->scan_options = $scan_options;
            
            // Initialize exclusion manager with scan options
            $this->exclusion_manager->initialize($scan_options);
            
            $this->debug_log[] = 'Public Files Scanner: Scan options loaded';
        }
        
        $this->debug_log[] = 'Starting Public Files scan...';
        
        $issues = array();
        $scanned_count = 0;
        
        try {
            // Clean up old public files issues before scanning
            $this->cleanup_old_issues();
            
            // Scan for publicly accessible configuration, backup, and log files
            if (!$this->scan_options || !isset($this->scan_options['scansEnabled_config_files']) || $this->scan_options['scansEnabled_config_files']) {
                $config_issues = $this->scan_configuration_files();
                $issues = array_merge($issues, $config_issues);
                $scanned_count += count($this->get_configuration_files_list());
            } else {
                $this->debug_log[] = 'Public Files: Configuration files scan disabled by configuration';
            }
            
            // Scan for publicly accessible quarantined files (if any exist)
            if (!$this->scan_options || !isset($this->scan_options['scansEnabled_quarantined']) || $this->scan_options['scansEnabled_quarantined']) {
                $quarantined_issues = $this->scan_quarantined_files();
                $issues = array_merge($issues, $quarantined_issues);
            } else {
                $this->debug_log[] = 'Public Files: Quarantined files scan disabled by configuration';
            }
            
            // Always store scan results in database (even if no issues found)
            $this->store_scan_results($issues, $scanned_count);
            
            $this->debug_log[] = "Public Files scan completed. Found " . count($issues) . " issues from {$scanned_count} files checked.";
            
        } catch (Exception $e) {
            $this->debug_log[] = 'Error during Public Files scan: ' . $e->getMessage();
        }
        
        // Apply performance controls to limit issues if enabled
        if (class_exists('VMPFence_Performance_Manager')) {
            $issues = VMPFence_Performance_Manager::limit_issues($issues);
            $this->debug_log[] = 'Performance Manager: Limited issues to ' . count($issues);
        }
        
        return array(
            'issues' => $issues,
            'debug_log' => $this->debug_log,
            'scanned_count' => $scanned_count
        );
    }
    
    private function scan_configuration_files() {
        $this->debug_log[] = 'Scanning configuration and backup files...';
        
        $issues = array();
        $config_files = $this->get_configuration_files_list();
        
        $processed_files = 0;
        foreach ($config_files as $file_info) {
            $file_path = $file_info['path'];
            
            // Check exclusion manager first
            if ($this->exclusion_manager->should_exclude_file($file_path)) {
                continue; // Skip excluded files
            }
            
            // Performance throttling for file processing
            if (class_exists('VMPFence_Performance_Manager')) {
                VMPFence_Performance_Manager::throttle_processing($processed_files);
            }
            $processed_files++;
            
            $file_url = $file_info['url'];
            $file_name = basename($file_path);
            
            $this->debug_log[] = "Testing accessibility of: {$file_name}";
            
            // Check if file exists on server
            if (file_exists($file_path)) {
                // Determine detection mode (exact or regex)
                $mode = isset($file_info['mode']) ? $file_info['mode'] : 'exact';
                $pattern = isset($file_info['pattern']) ? $file_info['pattern'] : null;
                
                // Test if file is publicly accessible
                if ($this->is_publicly_accessible($file_url, $file_path, $mode, $pattern)) {
                    $issues[] = array(
                        'type' => 'public_config_file',
                        'severity' => 'critical',
                        'title' => 'Publicly Accessible Configuration File',
                        'description' => "Configuration/backup file '{$file_name}' is publicly accessible",
                        'file_path' => $file_path,
                        'file_url' => $file_url,
                        'recommendation' => 'Remove the file or block public access via .htaccess or server configuration',
                        'details' => "The file {$file_name} can be accessed directly from the web and may contain sensitive information like database credentials or configuration data."
                    );
                    
                    $this->debug_log[] = "VULNERABILITY: {$file_name} is publicly accessible!";
                } else {
                    $this->debug_log[] = "OK: {$file_name} is not publicly accessible";
                }
            }
        }
        
        return $issues;
    }
    
    private function scan_quarantined_files() {
        $this->debug_log[] = 'Scanning quarantined files...';
        
        $issues = array();
        
        // NEW: Get live quarantined files
        $live_quarantined_files = $this->get_live_quarantined_files();
        
        foreach ($live_quarantined_files as $file_path) {
            $file_name = basename($file_path);
            
            if (file_exists($file_path)) {
                // Convert file path to URL
                $file_url = $this->file_path_to_url($file_path);
                
                if ($file_url && $this->is_publicly_accessible($file_url, $file_path, 'exact')) {
                    $issues[] = array(
                        'type' => 'public_quarantined_file',
                        'severity' => 'high',
                        'title' => 'Publicly Accessible Quarantined File',
                        'description' => "Quarantined file '{$file_name}' is publicly accessible",
                        'file_path' => $file_path,
                        'file_url' => $file_url,
                        'recommendation' => 'Remove the file immediately or block public access',
                        'details' => "This file was previously identified as suspicious/malicious but is still accessible from the web."
                    );
                    
                    $this->debug_log[] = "VULNERABILITY: Quarantined file {$file_name} is publicly accessible!";
                }
            }
        }
        
        // LEGACY: Also check old database records (for backward compatibility)
        $legacy_issues = $this->scan_legacy_quarantined_files();
        $issues = array_merge($issues, $legacy_issues);
        
        return $issues;
    }
    
    /**
     * Legacy quarantined file scanning (original method)
     */
    private function scan_legacy_quarantined_files() {
        $issues = array();
        
        // Only check vmpfence_scan_issues table for quarantined files
        global $wpdb;
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $scan_issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        // Check vmpfence_scan_issues table for file-related issues
        $scan_quarantined_files = $wpdb->get_results(
            "SELECT file_path FROM {$scan_issues_table} WHERE issue_type LIKE '%malware%' OR issue_type LIKE '%suspicious%'",
            ARRAY_A
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        
        $total_quarantined = count($scan_quarantined_files);
        
        if ($total_quarantined > 0) {
            $this->debug_log[] = 'Found ' . $total_quarantined . ' quarantined file records to check';
            
            // Process vmpfence_scan_issues table records
            foreach ($scan_quarantined_files as $file_data) {
                $file_path = $file_data['file_path'];
                $file_name = basename($file_path);
                
                if (file_exists($file_path)) {
                    // Convert file path to URL
                    $file_url = $this->file_path_to_url($file_path);
                    
                    if ($file_url && $this->is_publicly_accessible($file_url, $file_path, 'exact')) {
                        $issues[] = array(
                            'type' => 'public_quarantined_file',
                            'severity' => 'high',
                            'title' => 'Publicly Accessible Quarantined File',
                            'description' => "Quarantined file '{$file_name}' is publicly accessible",
                            'file_path' => $file_path,
                            'file_url' => $file_url,
                            'recommendation' => 'Remove the file immediately or block public access',
                            'details' => "This file was previously identified as suspicious/malicious but is still accessible from the web."
                        );
                        
                        $this->debug_log[] = "VULNERABILITY: Quarantined file {$file_name} is publicly accessible!";
                    }
                }
            }
        } else {
            $this->debug_log[] = 'No quarantined files found to check';
        }
        
        return $issues;
    }
    
    private function get_configuration_files_list() {
        $wordpress_root = ABSPATH;
        $site_url = site_url();
        
        $config_files = array(
            array('path' => $wordpress_root . '.env', 'url' => $site_url . '/.env'),
            array('path' => $wordpress_root . '.user.ini', 'url' => $site_url . '/.user.ini'),
            array('path' => $wordpress_root . 'wp-config.php.bak', 'url' => $site_url . '/wp-config.php.bak'),
            array('path' => $wordpress_root . 'wp-config.php.bak.a2', 'url' => $site_url . '/wp-config.php.bak.a2'),
            array('path' => $wordpress_root . 'wp-config.php.swo', 'url' => $site_url . '/wp-config.php.swo'),
            array('path' => $wordpress_root . 'wp-config.php.save', 'url' => $site_url . '/wp-config.php.save'),
            array('path' => $wordpress_root . '#wp-config.php#', 'url' => $site_url . '/%23wp-config.php%23'),
            array('path' => $wordpress_root . 'wp-config.php~', 'url' => $site_url . '/wp-config.php~'),
            array('path' => $wordpress_root . 'wp-config.old', 'url' => $site_url . '/wp-config.old'),
            array('path' => $wordpress_root . '.wp-config.php.swp', 'url' => $site_url . '/.wp-config.php.swp'),
            array('path' => $wordpress_root . 'wp-config.bak', 'url' => $site_url . '/wp-config.bak'),
            array('path' => $wordpress_root . 'wp-config.save', 'url' => $site_url . '/wp-config.save'),
            array('path' => $wordpress_root . 'wp-config.php_bak', 'url' => $site_url . '/wp-config.php_bak'),
            array('path' => $wordpress_root . 'wp-config.php.swp', 'url' => $site_url . '/wp-config.php.swp'),
            array('path' => $wordpress_root . 'wp-config.php.old', 'url' => $site_url . '/wp-config.php.old'),
            array('path' => $wordpress_root . 'wp-config.php.original', 'url' => $site_url . '/wp-config.php.original'),
            array('path' => $wordpress_root . 'wp-config.php.orig', 'url' => $site_url . '/wp-config.php.orig'),
            array('path' => $wordpress_root . 'wp-config.txt', 'url' => $site_url . '/wp-config.txt'),
            array('path' => $wordpress_root . 'wp-config.original', 'url' => $site_url . '/wp-config.original'),
            array('path' => $wordpress_root . 'wp-config.orig', 'url' => $site_url . '/wp-config.orig'),
            array('path' => WP_CONTENT_DIR . '/debug.log', 'url' => content_url('/debug.log')),
            array('path' => $wordpress_root . 'searchreplacedb2.php', 'url' => $site_url . '/searchreplacedb2.php'),
            array('path' => $wordpress_root . 'searchreplacedb.php', 'url' => $site_url . '/searchreplacedb.php'),
            array('path' => $wordpress_root . 'adminer.php', 'url' => $site_url . '/adminer.php'),
            array('path' => $wordpress_root . 'phpmyadmin.php', 'url' => $site_url . '/phpmyadmin.php'),
            array('path' => $wordpress_root . 'sql.php', 'url' => $site_url . '/sql.php'),
            array('path' => $wordpress_root . 'test-public-file.txt', 'url' => $site_url . '/test-public-file.txt'),
        );
        
        // Dynamic user.ini detection
        $userIniFilename = ini_get('user_ini.filename');
        if ($userIniFilename && $userIniFilename !== '.user.ini') {
            $config_files[] = array('path' => $wordpress_root . $userIniFilename, 'url' => $site_url . '/' . $userIniFilename);
        }
        
        // Dynamic pattern search for searchreplacedb files
        $searchreplacedb_files = $this->find_files_by_pattern('searchreplacedb*.php');
        $config_files = array_merge($config_files, $searchreplacedb_files);
        
        return $config_files;
    }
    
    /**
     * Find files matching a pattern
     * @param string $pattern Glob pattern to search for
     * @return array Array of file info arrays
     */
    private function find_files_by_pattern($pattern) {
        $wordpress_root = ABSPATH;
        $site_url = site_url();
        $found_files = array();
        
        // Search in WordPress root directory
        $files = glob($wordpress_root . $pattern);
        
        foreach ($files as $file_path) {
            if (is_file($file_path)) {
                $relative_path = substr($file_path, strlen($wordpress_root));
                $found_files[] = array(
                    'path' => $file_path,
                    'url' => $site_url . '/' . $relative_path,
                    'mode' => 'regex', // Enable regex mode for these files
                    'pattern' => '/<title>Search and replace DB/i' 
                );
            }
        }
        
        return $found_files;
    }
    
    /**
     * Get live quarantined files
     * This integrates with your malware scanner to find recently quarantined files
     * @return array Array of file paths
     */
    private function get_live_quarantined_files() {
        $quarantined_files = array();
        
        // Method 1: Search for .suspected files
        $wordpress_root = ABSPATH;
        $suspected_files = $this->find_suspected_files($wordpress_root);
        $quarantined_files = array_merge($quarantined_files, $suspected_files);
        
        // Method 2: Search wp-content for .suspected files
        $wpcontent_suspected = $this->find_suspected_files(WP_CONTENT_DIR);
        $quarantined_files = array_merge($quarantined_files, $wpcontent_suspected);
        
        // Method 3: Get from your malware scanner's recent results
        // TODO: Connect this to your actual malware scanner
        $recent_malware = $this->get_recent_malware_detections();
        $quarantined_files = array_merge($quarantined_files, $recent_malware);
        
        $this->debug_log[] = 'Found ' . count($quarantined_files) . ' live quarantined files';
        
        return array_unique($quarantined_files);
    }
    
    /**
     * Find .suspected files recursively
     * @param string $directory Directory to search
     * @return array Array of suspected file paths
     */
    private function find_suspected_files($directory) {
        $suspected_files = array();
        
        if (!is_dir($directory)) {
            return $suspected_files;
        }
        
        try {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
                RecursiveIteratorIterator::LEAVES_ONLY
            );
            
            foreach ($iterator as $file) {
                if ($file->isFile() && preg_match('/\.suspected$/i', $file->getFilename())) {
                    $suspected_files[] = $file->getPathname();
                    $this->debug_log[] = 'Found .suspected file: ' . $file->getPathname();
                }
            }
        } catch (Exception $e) {
            $this->debug_log[] = 'Error scanning directory ' . $directory . ': ' . $e->getMessage();
            // Fallback to simple glob search
            $glob_pattern = $directory . '/*.suspected';
            $glob_files = glob($glob_pattern);
            if ($glob_files) {
                foreach ($glob_files as $file) {
                    $suspected_files[] = $file;
                    $this->debug_log[] = 'Found .suspected file via glob: ' . $file;
                }
            }
        }
        
        return $suspected_files;
    }
    
    /**
     * Get recent malware detections from your scanner
     * @return array Array of file paths from recent scans
     */
    private function get_recent_malware_detections() {
        global $wpdb;
        $recent_files = array();
        
        // Get files flagged as malware in last 30 days
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $scan_issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        $results = $wpdb->get_results(
            "SELECT DISTINCT file_path FROM {$scan_issues_table} 
             WHERE (issue_type LIKE '%malware%' OR issue_type LIKE '%suspicious%') 
             AND detected_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
             AND status != 'resolved'",
            ARRAY_A
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        
        if ($results) {
            foreach ($results as $result) {
                if (!empty($result['file_path']) && file_exists($result['file_path'])) {
                    $recent_files[] = $result['file_path'];
                }
            }
        }
        
        return $recent_files;
    }
    
    private function is_publicly_accessible($file_url, $file_path, $mode = 'exact', $pattern = null) {
        // Make HTTP request to the file URL
        $response = wp_remote_get($file_url, array(
            'timeout' => 10,
            'headers' => array(
                'Range' => 'bytes=0-699' // Only get first 700 bytes
            )
        ));
        
        // Check if HTTP response is successful (200-299)
        $response_code = wp_remote_retrieve_response_code($response);
        $this->debug_log[] = "  HTTP response code for {$file_url}: {$response_code}";
        
        // Accept both 200 (OK) and 206 (Partial Content) as successful responses
        if (!($response_code == 200 || $response_code == 206)) {
            $this->debug_log[] = "  File not accessible (HTTP {$response_code})";
            return false;
        }
        
        // Get remote content (first 700 bytes)
        $remote_content = substr(wp_remote_retrieve_body($response), 0, 700);
        
        // Handle different matching modes
        if ($mode === 'regex' && $pattern) {
            // Regex mode: check if remote content matches pattern
            $matches = preg_match($pattern, $remote_content);
            $this->debug_log[] = "  Regex pattern match: " . ($matches ? 'YES' : 'NO');
            $this->debug_log[] = "  Pattern: {$pattern}";
            $this->debug_log[] = "  Remote content preview: " . $this->clean_debug_content(substr($remote_content, 0, 100)) . "...";
            return $matches;
        } else {
            // Exact mode: compare local vs remote content
            // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Read-only file comparison for security scanning
            $handle = @fopen($file_path, 'r');
            if (!$handle) {
                $this->debug_log[] = "  Cannot read local file: {$file_path}";
                return false;
            }
            
            $local_content = fread($handle, 700);
            fclose($handle);
            // phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_fclose
            
            $this->debug_log[] = "  Local content length: " . strlen($local_content);
            $this->debug_log[] = "  Remote content length: " . strlen($remote_content);
            $this->debug_log[] = "  Local content preview: " . $this->clean_debug_content(substr($local_content, 0, 50)) . "...";
            $this->debug_log[] = "  Remote content preview: " . $this->clean_debug_content(substr($remote_content, 0, 50)) . "...";
            
            // Compare local vs remote content
            $matches = $local_content === $remote_content;
            $this->debug_log[] = "  Content matches: " . ($matches ? 'YES' : 'NO');
            
            return $matches;
        }
    }
    
    private function file_path_to_url($file_path) {
        // Convert absolute file path to URL
        $wordpress_root = ABSPATH;
        $site_url = site_url();
        
        if (strpos($file_path, $wordpress_root) === 0) {
            $relative_path = substr($file_path, strlen($wordpress_root));
            return $site_url . '/' . $relative_path;
        }
        
        // Handle wp-content files
        if (strpos($file_path, WP_CONTENT_DIR) === 0) {
            $relative_path = substr($file_path, strlen(WP_CONTENT_DIR));
            return content_url($relative_path);
        }
        
        return null;
    }
    
    public function get_debug_log() {
        return $this->debug_log;
    }
    
    /**
     * Clean up old public files issues before running new scan
     */
    private function cleanup_old_issues() {
        global $wpdb;
        
        // Clear previous scan issues from new table structure  
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from WordPress prefix
        $issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        $deleted = $wpdb->query($wpdb->prepare(
            "DELETE FROM {$issues_table} WHERE issue_type = %s AND status != 'ignored'", 'public_files'
        ));
        
        // Clear previous scan results
        $results_table = $wpdb->prefix . 'vmpfence_scan_results';
        $deleted_results = $wpdb->query($wpdb->prepare(
            "DELETE FROM {$results_table} WHERE scan_type = %s", 'public_files'
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        
        
        if ($deleted !== false) {
            $this->debug_log[] = "Cleaned up {$deleted} old public files issues and {$deleted_results} scan results from database";
        } else {
            $this->debug_log[] = "Failed to clean up old public files issues";
        }
    }
    
    /**
     * Store scan results in the database (always saves scan record, even with 0 issues)
     *
     * @param array $issues Array of issue results
     * @param int $scanned_count Number of files scanned
     */
    private function store_scan_results($issues, $scanned_count) {
        global $wpdb;
        
        // Always save scan summary to scan_results table
        $scan_results_table = $wpdb->prefix . 'vmpfence_scan_results';
        $scan_result_id = $wpdb->insert(
            $scan_results_table,
            [
                'scan_type' => 'public_files',
                'scan_time' => current_time('mysql'),
                'total_files' => $scanned_count, // Total files checked
                'issues_found' => count($issues),
                'scan_status' => 'completed',
                'scan_data' => wp_json_encode([
                    'files_checked' => $scanned_count,
                    'issues_found' => count($issues),
                    'scan_type' => 'public_files'
                ])
            ]
        );
        
        if ($scan_result_id === false) {
            $this->debug_log[] = 'Error saving scan summary: ' . $wpdb->last_error;
            return;
        }
        
        $scan_id = $wpdb->insert_id;
        $this->debug_log[] = "Saved scan summary (ID: " . $scan_id . ") - checked {$scanned_count} files, found " . count($issues) . " issues";
        
        // Only save individual issues if there are any
        if (!empty($issues)) {
            $scan_issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
            
            foreach ($issues as $result) {
                $description = $result['title'] . ': ' . $result['description'];
                
                // First check if this issue should be recreated as ignored
                if ($this->handle_ignored_issue($result['file_path'], 'public_files', $description, $scan_id)) {
                    continue; // Issue was handled as ignored, skip normal processing
                }
                
                // Check if this issue is currently ignored (skip normal creation)
                if ($this->is_issue_ignored($result['file_path'], 'public_files', $description)) {
                    $this->debug_log[] = 'SKIP: Public files issue already ignored - ' . $result['file_path'];
                    continue;
                }

                $insert_result = $wpdb->insert(
                    $scan_issues_table,
                    [
                        'scan_id' => $scan_id,
                        'file_path' => $result['file_path'],
                        'issue_type' => 'public_files',
                        'severity' => $this->map_issue_type_to_severity('public_files'),
                        'description' => $description,
                        'detected_time' => current_time('mysql'),
                        'status' => 'new'
                    ]
                );
                
                if ($insert_result === false) {
                    $this->debug_log[] = 'Error saving public files issue: ' . $wpdb->last_error;
                }
            }
            
            $this->debug_log[] = "Saved " . count($issues) . " public file vulnerabilities to database";
        } else {
            $this->debug_log[] = "No issues to save (scan completed with 0 vulnerabilities)";
        }
    }
    
    /**
     * Check if an issue is already ignored
     */
    private function is_issue_ignored($file_path, $issue_type, $description) {
        global $wpdb;
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $scan_issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        $count = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $scan_issues_table 
             WHERE file_path = %s AND issue_type = %s AND description = %s AND status = 'ignored'",
            $file_path,
            $issue_type,
            $description
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        
        return $count > 0;
    }
    
    /**
     * Handle previously ignored issues - recreate them in current scan as ignored
     *
     * @param string $file_path File path
     * @param string $issue_type Issue type
     * @param string $description Issue description
     * @param int $scan_id Current scan ID
     * @return bool Whether issue was handled
     */
    private function handle_ignored_issue($file_path, $issue_type, $description, $scan_id) {
        global $wpdb;
        
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from WordPress prefix
        $issues_table = $wpdb->prefix . 'vmpfence_scan_issues';
        
        // Check if this exact issue was previously ignored
        $ignored_issue = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$issues_table} WHERE file_path = %s AND issue_type = %s AND description = %s AND status = 'ignored' ORDER BY detected_time DESC LIMIT 1",
            $file_path,
            $issue_type,
            $description
        ));
        
        if ($ignored_issue) {
            // Recreate the ignored issue in current scan
            $insert_result = $wpdb->insert(
                $issues_table,
                [
                    'scan_id' => $scan_id,
                    'file_path' => $file_path,
                    'issue_type' => $issue_type,
                    'severity' => $this->map_issue_type_to_severity($issue_type),
                    'description' => $description,
                    'status' => 'ignored',
                    'detected_time' => current_time('mysql'),
                    'resolved_time' => current_time('mysql')
                ],
                ['%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s']
            );
            
            if ($insert_result) {
                // Clean up old ignored entries for this same issue (keep only the latest)
                $wpdb->query($wpdb->prepare(
                    "DELETE FROM {$issues_table} WHERE file_path = %s AND issue_type = %s AND description = %s AND status = 'ignored' AND id != %d",
                    $file_path,
                    $issue_type,
                    $description,
                    $wpdb->insert_id
                ));
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                
                $this->debug_log[] = '[' . current_time('H:i:s') . '] PRESERVED: Recreated ignored public_files - ' . $file_path;
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Map issue type to severity level
     *
     * @param string $issue_type Issue type
     * @return string Severity level
     */
    private function map_issue_type_to_severity($issue_type) {
        switch ($issue_type) {
            case 'public_files':
                return 'medium';
            default:
                return 'medium';
        }
    }

    /**
     * Map text severity to numeric value for database storage
     *
     * @param string $severity Severity text (critical, high, medium, low)
     * @return int Numeric severity value
     */
    private function map_severity_to_number($severity) {
        switch (strtolower($severity)) {
            case 'critical':
                return 10;
            case 'high':
                return 7;
            case 'medium':
                return 5;
            case 'low':
                return 3;
            default:
                return 5; // Default to medium
        }
    }
    
    /**
     * Clean content for debug logging to prevent database save issues
     * Removes null bytes and non-UTF8 characters that can corrupt database saves
     */
    private function clean_debug_content($content) {
        // Remove null bytes and other control characters
        $cleaned = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $content);
        
        // Ensure UTF-8 encoding
        $cleaned = mb_convert_encoding($cleaned, 'UTF-8', 'UTF-8');
        
        // Replace any remaining problematic characters with placeholders
        $cleaned = preg_replace('/[^\x20-\x7E\x80-\xFF]/', '?', $cleaned);
        
        return $cleaned;
    }
}
