<?php
/**
 * Signature Sync Class - Sync Malware Signatures from API
 *
 * @package VMPFence
 * @since 1.0.0
 */

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

/**
 * Signature Sync Class (matches firewall Rule_Importer pattern)
 *
 * @since 1.0.0
 */
class VMPFence_Signature_Sync {

    /**
     * Get PHP array file path (primary format, opcache-backed)
     *
     * @since 2.2.4
     * @return string
     */
    public static function get_php_file_path() {
        return VMPFENCE_PLUGIN_DIR . 'data/malware-signatures.php';
    }
    
    /**
     * Get JSON file path (legacy fallback)
     *
     * @since 1.0.0
     * @deprecated 2.2.4 Use get_php_file_path() instead. Kept for backward compat.
     * @return string
     */
    public static function get_json_file_path() {
        return VMPFENCE_PLUGIN_DIR . 'data/malware-signatures.json';
    }

    /**
     * Sync signatures from API server (PRIMARY METHOD)
     *
     * Uses streamed page-by-page writing to avoid PHP memory exhaustion.
     * Peak memory stays under ~5-10 MB regardless of total signature count.
     * Writes to a .tmp file first, then atomically renames on success.
     *
     * @since 1.0.0
     * @since 2.2.4 Major refactor to use streaming and PHP array format for opcache benefits.
     * @return array Result with 'success', 'imported', 'errors', 'license_tier', 'version' keys
     */
    public static function sync_from_api() {
        // ── Concurrency lock ────────────────────────────────────────────
        // Prevents AJAX manual sync and cron hourly sync from running at
        // the same time and stomping on the same temp files.
        $lock_key = 'vmpfence_signature_sync_lock';
        $lock_time = get_transient($lock_key);
        if ($lock_time) {
            // Staleness check: if the lock is older than 5 minutes, treat it
            // as stale (the process that set it likely crashed). Allow retry.
            $lock_age = time() - (int) $lock_time;
            if ($lock_age < 300) {
                return array(
                    'success' => false,
                    'imported' => 0,
                    'errors' => array('Signature sync is already in progress. Please try again in a few minutes.'),
                    'license_tier' => 'unknown',
                    'version' => 'unknown'
                );
            }
            // Lock is stale (> 5 min old) — previous sync likely crashed.
            // Fall through and re-acquire the lock.
            delete_transient($lock_key);
        }
        // Lock for 10 minutes (generous upper bound; sync typically takes < 2 min)
        set_transient($lock_key, time(), 10 * MINUTE_IN_SECONDS);

        // Wrap everything so the lock is always released
        $result = self::_do_sync_from_api();

        // Release lock
        delete_transient($lock_key);

        return $result;
    }

    /**
     * Internal implementation of sync_from_api (runs inside concurrency lock).
     *
     * @since 2.2.4
     * @access private
     * @return array
     */
    private static function _do_sync_from_api() {
        // Get API key
        $api_key = VMPFence_Config::get('central_api_key');
        
        if (empty($api_key)) {
            return array(
                'success' => false,
                'imported' => 0,
                'errors' => array('API key not configured'),
                'license_tier' => 'unknown',
                'version' => 'unknown'
            );
        }
        
        // Initialize API client
        require_once VMPFENCE_PLUGIN_DIR . 'lib/class-api-client.php';
        $api_client = new VMPFence_API_Client();
        
        // Prepare atomic file paths (PHP array format for opcache)
        $php_file_path = self::get_php_file_path();
        $json_file_path = self::get_json_file_path(); // kept for empty-sigs fallback
        $tmp_path  = $php_file_path . '.tmp';
        
        // Open temp file for streaming write
        $fh = @fopen($tmp_path, 'w');
        if (!$fh) {
            return array(
                'success' => false,
                'imported' => 0,
                'errors' => array('Failed to open temp file for writing: ' . $tmp_path),
                'license_tier' => 'unknown',
                'version' => 'unknown'
            );
        }
        
        // State tracking across pages
        $total_written = 0;
        $first_signature = true;
        $write_error = null;
        
        // Page callback — writes each page's signatures as PHP array entries to the temp file
        $page_callback = function(array $signatures, int $page_number) use ($fh, &$total_written, &$first_signature, &$write_error) {
            foreach ($signatures as $sig) {
                // Comma separator between signatures (not before the first one)
                $prefix = $first_signature ? "\n" : ",\n";
                $first_signature = false;
                
                // Use var_export for safe PHP-breaking char escaping (handles ', \, <?php, etc.)
                $php_entry = var_export($sig, true);
                if ($php_entry === null || $php_entry === false) {
                    $write_error = 'Failed to var_export signature on page ' . $page_number;
                    return false; // Abort fetching
                }
                
                $bytes = @fwrite($fh, $prefix . $php_entry);
                if ($bytes === false) {
                    $write_error = 'Failed to write signature to temp file on page ' . $page_number;
                    return false; // Abort fetching
                }
                
                $total_written++;
            }
            
            // Free memory after each page (signatures array will be GC'd by caller)
            return true; // Continue fetching
        };
        
        // Fetch signatures with streamed callback
        $response = $api_client->get_malware_signatures_streamed($api_key, $page_callback);
        
        // Handle API-level error
        if (is_wp_error($response)) {
            fclose($fh);
            @unlink($tmp_path);
            return array(
                'success' => false,
                'imported' => 0,
                'errors' => array('API error: ' . $response->get_error_message()),
                'license_tier' => 'unknown',
                'version' => 'unknown'
            );
        }
        
        // Handle write error during streaming
        if ($write_error !== null) {
            fclose($fh);
            @unlink($tmp_path);
            return array(
                'success' => false,
                'imported' => 0,
                'errors' => array($write_error),
                'license_tier' => $response['tier'] ?? 'unknown',
                'version' => $response['version'] ?? 'unknown'
            );
        }
        
        // Extract metadata
        $version      = $response['version'] ?? 'unknown';
        $license_tier = $response['tier'] ?? 'free';
        $last_updated = $response['last_updated'] ?? current_time('mysql');
        $server_total = $response['server_total'] ?? $total_written;
        
        // Handle empty signatures case
        if ($total_written === 0) {
            fclose($fh);
            @unlink($tmp_path);
            
            update_option('vmpfence_server_signature_total', $server_total);
            update_option('vmpfence_signature_count', 0);
            update_option('vmpfence_last_signature_sync', time());
            
            // Write a valid empty-signatures PHP file
            $empty_php = "<?php\n// Auto-generated by VMPFence Signature Sync — do not edit\n// Generated: " . gmdate('c') . " | Count: 0\nreturn array(\n    'count' => 0,\n    'signatures' => array(),\n);\n";
            @file_put_contents($php_file_path, $empty_php, LOCK_EX);
            if (function_exists('opcache_invalidate')) {
                opcache_invalidate($php_file_path, true);
            }
            
            // Write metadata sidecar file for empty case too
            require_once VMPFENCE_PLUGIN_DIR . 'includes/class-signature-loader.php';
            VMPFence_Signature_Loader::write_meta_file(0, $license_tier, 'php');
            
            return array(
                'success' => true,
                'imported' => 0,
                'errors' => array(),
                'license_tier' => $license_tier,
                'version' => $version
            );
        }
        
        // Now wrap the streamed signatures with the PHP array envelope
        // We need to prepend the header and append the footer around what we've written.
        // Strategy: we wrote signatures as a comma-separated var_export list to the temp file.
        // Now we create the final PHP file with the full array structure.
        fclose($fh);
        
        // Build the final file: PHP header + signatures + footer
        $final_path = $php_file_path . '.final.tmp';
        $fh_final = @fopen($final_path, 'w');
        if (!$fh_final) {
            @unlink($tmp_path);
            return array(
                'success' => false,
                'imported' => 0,
                'errors' => array('Failed to open final temp file for writing'),
                'license_tier' => $license_tier,
                'version' => $version
            );
        }
        
        // Write PHP array header
        $header = "<?php\n"
            . "// Auto-generated by VMPFence Signature Sync — do not edit\n"
            . "// Generated: " . gmdate('c') . " | Count: " . $total_written . "\n"
            . "return array(\n"
            . "    'count' => " . $total_written . ",\n"
            . "    'signatures' => array(";
        @fwrite($fh_final, $header);
        
        // Stream-copy the signatures from temp file
        $fh_sigs = @fopen($tmp_path, 'r');
        if ($fh_sigs) {
            while (!feof($fh_sigs)) {
                $chunk = fread($fh_sigs, 65536); // 64KB chunks
                if ($chunk !== false && $chunk !== '') {
                    @fwrite($fh_final, $chunk);
                }
            }
            fclose($fh_sigs);
        }
        
        // Write PHP array footer
        @fwrite($fh_final, "\n    ),\n);\n");
        fclose($fh_final);
        
        // Clean up the intermediate signatures temp file
        @unlink($tmp_path);
        
        // Validate PHP file before renaming (prevents corrupt file from replacing good one)
        $test_data = @include $final_path;
        if (!is_array($test_data) || !isset($test_data['signatures'])) {
            @unlink($final_path);
            return array(
                'success' => false,
                'imported' => 0,
                'errors' => array('Generated PHP signature file failed validation — aborting'),
                'license_tier' => $license_tier,
                'version' => $version
            );
        }
        unset($test_data); // Free memory from validation load
        
        // Atomic rename: final temp → real PHP file
        // On Windows, rename() fails if target exists, so delete first
        if (file_exists($php_file_path)) {
            @unlink($php_file_path);
        }
        $renamed = @rename($final_path, $php_file_path);
        if (!$renamed) {
            // Fallback: copy + delete
            $copied = @copy($final_path, $php_file_path);
            @unlink($final_path);
            if (!$copied) {
                return array(
                    'success' => false,
                    'imported' => 0,
                    'errors' => array('Failed to move temp file to final destination: ' . $php_file_path),
                    'license_tier' => $license_tier,
                    'version' => $version
                );
            }
        }
        
        // Invalidate opcache so new signatures are picked up immediately
        if (function_exists('opcache_invalidate')) {
            opcache_invalidate($php_file_path, true);
        }
        
        // Update WordPress options metadata
        update_option('vmpfence_last_signature_sync', time());
        update_option('vmpfence_signature_count', $total_written);
        update_option('vmpfence_signature_version', $version);
        update_option('vmpfence_server_signature_total', $server_total);
        
        // Write metadata sidecar file (AFTER successful main file write)
        // Tiny ~100 byte JSON file so dashboard/AJAX can read count without loading 26 MB
        require_once VMPFENCE_PLUGIN_DIR . 'includes/class-signature-loader.php';
        VMPFence_Signature_Loader::write_meta_file($total_written, $license_tier, 'php');
        
        // DO NOT overwrite license type from signature sync response
        // Only get_license_status() or activate_license() should update license settings
        VMPFence_Config::set('scanner_license_tier', $license_tier);
        
        VMPFence_Secure_Logger::log('info', 'Synced signatures (streamed)', [
            'count' => $total_written,
            'version' => $version,
            'tier' => $license_tier,
            'server_total' => $server_total,
        ]);
        
        return array(
            'success' => true,
            'imported' => $total_written,
            'errors' => array(),
            'license_tier' => $license_tier,
            'version' => $version,
            'total_available' => $total_written
        );
    }
    
    /**
     * Save signatures to JSON file (LEGACY — kept for backward compatibility)
     *
     * WARNING: For large signature databases (100K+), this loads everything into memory.
     * The primary sync_from_api() method now uses streaming and does not call this.
     *
     * @since 1.0.0
     * @param array $signatures Signatures array from API
     * @param string $version Signature database version
     * @param string $tier License tier
     * @param string $last_updated Last updated timestamp
     * @param int $server_total Total signatures on server (all tiers)
     * @return array Result with 'success' and 'errors' keys
     */
    private static function save_signatures_to_file($signatures, $version, $tier, $last_updated, $server_total = 0) {
        $file_path = self::get_json_file_path();
        
        // Prepare data structure
        $data = array(
            'version' => $version,
            'last_updated' => $last_updated,
            'tier' => $tier,
            'total_signatures' => count($signatures),
            'server_total' => $server_total,  // Total on server (all tiers)
            'signatures' => $signatures
        );
        
        // Encode to JSON
        $json_content = wp_json_encode($data, JSON_PRETTY_PRINT);
        
        if ($json_content === false) {
            return array(
                'success' => false,
                'errors' => array('Failed to encode signatures to JSON')
            );
        }
        
        // Write to file
        $bytes_written = file_put_contents($file_path, $json_content);
        
        if ($bytes_written === false) {
            return array(
                'success' => false,
                'errors' => array('Failed to write signatures to file: ' . $file_path)
            );
        }
        
        return array(
            'success' => true,
            'errors' => array()
        );
    }
    
    /**
     * Get signature count from JSON file
     *
     * @deprecated 2.2.4 Use VMPFence_Signature_Loader::get_count() instead.
     * @since 1.0.0
     * @return int Number of signatures
     */
    public static function get_signature_count() {
        _doing_it_wrong(
            __METHOD__,
            'Use VMPFence_Signature_Loader::get_count() instead. This method loads the full JSON into memory.',
            '2.2.4'
        );
        $file_path = self::get_json_file_path();
        
        if (!file_exists($file_path)) {
            return 0;
        }
        
        $json_content = file_get_contents($file_path);
        if ($json_content === false) {
            return 0;
        }
        
        $data = json_decode($json_content, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            return 0;
        }
        
        if (isset($data['total_signatures'])) {
            return intval($data['total_signatures']);
        }
        
        if (isset($data['signatures']) && is_array($data['signatures'])) {
            return count($data['signatures']);
        }
        
        return 0;
    }
    
    /**
     * Get signature version from JSON file
     *
     * @deprecated 2.2.4 Use VMPFence_Signature_Loader::get_sync_metadata() instead.
     * @since 1.0.0
     * @return string Version string
     */
    public static function get_signature_version() {
        _doing_it_wrong(
            __METHOD__,
            'Use VMPFence_Signature_Loader::get_sync_metadata() instead. This method loads the full JSON into memory.',
            '2.2.4'
        );
        $file_path = self::get_json_file_path();
        
        if (!file_exists($file_path)) {
            return 'unknown';
        }
        
        $json_content = file_get_contents($file_path);
        if ($json_content === false) {
            return 'unknown';
        }
        
        $data = json_decode($json_content, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            return 'unknown';
        }
        
        return isset($data['version']) ? $data['version'] : 'unknown';
    }
    
    /**
     * Validate signature file integrity
     *
     * @deprecated 2.2.4 Validation is now handled internally during sync.
     * @since 1.0.0
     * @return array Result with 'valid' and 'errors' keys
     */
    public static function validate_signature_file() {
        _doing_it_wrong(
            __METHOD__,
            'Validation is now handled internally during sync. This method loads the full JSON into memory.',
            '2.2.4'
        );
        $file_path = self::get_json_file_path();
        $errors = array();
        
        // Check file exists
        if (!file_exists($file_path)) {
            $errors[] = 'Signature file not found';
            return array('valid' => false, 'errors' => $errors);
        }
        
        // Check file is readable
        if (!is_readable($file_path)) {
            $errors[] = 'Signature file is not readable';
            return array('valid' => false, 'errors' => $errors);
        }
        
        // Read and parse JSON
        $json_content = file_get_contents($file_path);
        if ($json_content === false) {
            $errors[] = 'Failed to read signature file';
            return array('valid' => false, 'errors' => $errors);
        }
        
        $data = json_decode($json_content, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $errors[] = 'JSON parse error: ' . json_last_error_msg();
            return array('valid' => false, 'errors' => $errors);
        }
        
        // Validate structure
        if (!isset($data['version'])) {
            $errors[] = 'Missing version field';
        }
        
        if (!isset($data['signatures']) || !is_array($data['signatures'])) {
            $errors[] = 'Missing or invalid signatures array';
        }
        
        return array(
            'valid' => empty($errors),
            'errors' => $errors
        );
    }
}
