<?php
/**
 * VmpFence SIEM Scan Sync
 *
 * Pushes scan progress (every 30s during active scan) and final scan results
 * (issues list + statistics) to the Newton Security server.
 *
 * Called from:
 *   - VmpFence_Sync_Manager::sync_all() — progress push during cron cycle
 *   - Directly after scan completes — results push
 *
 * @package    VMPFence
 * @subpackage Includes
 * @since 2.2.4
 * @since 2.2.6 Scan results and Skipped path description updated for server 
 */

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

class VmpFence_Scan_Sync {

    /**
     * API client instance
     * @var VMPFence_API_Client
     */
    private $api_client;

    /**
     * Site UUID for server identification
     * @var string
     */
    private $site_uuid;

    /**
     * API key for authentication
     * @var string
     */
    private $api_key;

    /**
     * Minimum interval between progress pushes (seconds)
     * @var int
     */
    const PUSH_INTERVAL = 30;

    /**
     * Option name for tracking last push timestamp
     * @var string
     */
    const LAST_PUSH_OPTION = 'vmpfence_siem_last_scan_push';

    /**
     * Circuit breaker — if a progress push fails (WP_Error), skip all
     * subsequent pushes for this instance's lifetime (i.e. this scan run).
     * Prevents ~17 min of wasted retries when server is unreachable.
     *
     * @since 2.2.5
     * @var bool
     */
    private $push_failed = false;

    /**
     * Constructor — initialises API client and credentials.
     *
     * @since 2.2.4
     */
    public function __construct() {
        $this->api_client = new VMPFence_API_Client();
        $this->site_uuid  = get_option(VMPFence_Constants::OPTION_SITE_UUID, '');
        $this->api_key    = VMPFence_Config::get('central_api_key', '');
    }

    /**
     * Push scan status to server immediately, bypassing throttle.
     *
     * Called at scan start (both SIEM-initiated and WP-admin-initiated)
     * so the portal sees 'initializing' within seconds instead of waiting
     * for the next 5-min cron cycle.
     *
     * @since 2.2.5
     * @param string $status  Status string (e.g. 'initializing', 'scanning').
     * @param string $section Human-readable section label.
     * @return void
     */
    public function push_immediate_status(string $status = 'initializing', string $section = 'Initializing scan...'): void {
        if (empty($this->site_uuid) || empty($this->api_key)) {
            return;
        }

        $scan_percentage = array(
            'scan_type'    => 0,
            'malware_sigs' => 0,
            'reputation'   => 0,
        );
        if (class_exists('VMPFence_Scan_Percentage') && method_exists('VMPFence_Scan_Percentage', 'calculateScanPercentageBreakdown')) {
            $raw = VMPFence_Scan_Percentage::calculateScanPercentageBreakdown();
            if (is_array($raw)) {
                $scan_percentage = array_merge($scan_percentage, $raw);
            }
        }

        $payload = array(
            'status'                   => $status,
            'scan_progress'            => 0,
            'total_scans'              => 11,
            'current_section'          => $section,
            'files_scanned'            => 0,
            'total_files'              => 0,
            'percentage'               => 0.0,
            'files_per_second'         => 0.0,
            'remaining_time'           => 0,
            'scan_type_percentage'     => $scan_percentage['scan_type'] ?? 0,
            'malware_sigs_percentage'  => $scan_percentage['malware_sigs'] ?? 0,
            'reputation_percentage'    => $scan_percentage['reputation'] ?? 0,
            'stage_statuses'           => $this->get_stage_statuses(),
            'comprehensive_statistics' => array(),
            'scan_started_at'          => time(),
            'plugin_version'           => VMPFENCE_VERSION,
        );

        $this->api_client->push_scan_progress(
            $this->api_key,
            $this->site_uuid,
            $payload
        );

        // Reset the throttle timer so the next maybe_push_progress works correctly
        update_option(self::LAST_PUSH_OPTION, time(), false);
    }

    /**
     * Push current scan progress to server, bypassing throttle.
     *
     * Called after each scan stage completes (up to 11 times per scan)
     * for near-real-time progress visibility on the portal.
     *
     * @since 2.2.5
     * @return void
     */
    public function force_push_progress(): void {
        // FIX-3 (v6.5): Circuit breaker — skip all pushes after first failure.
        // Prevents ~17 min of wasted retries (30s timeout × 3 retries × 11 calls)
        // when server is unreachable.
        if ($this->push_failed) {
            return;
        }

        if (empty($this->site_uuid) || empty($this->api_key)) {
            return;
        }

        $progress = get_option('vmpfence_scan_progress', array());
        if (empty($progress) || ($progress['status'] ?? 'idle') === 'idle') {
            return;
        }

        $scan_percentage = array(
            'scan_type'    => 0,
            'malware_sigs' => 0,
            'reputation'   => 0,
        );
        if (class_exists('VMPFence_Scan_Percentage') && method_exists('VMPFence_Scan_Percentage', 'calculateScanPercentageBreakdown')) {
            $raw = VMPFence_Scan_Percentage::calculateScanPercentageBreakdown();
            if (is_array($raw)) {
                $scan_percentage = array_merge($scan_percentage, $raw);
            }
        }

        $payload = array(
            'status'                   => $progress['status'] ?? 'scanning',
            'scan_progress'            => $progress['scan_progress'] ?? 0,
            'total_scans'              => $progress['total_scans'] ?? 11,
            'current_section'          => $progress['current_section'] ?? '',
            'files_scanned'            => $progress['files_scanned'] ?? 0,
            'total_files'              => $progress['total_files'] ?? 0,
            'percentage'               => $this->calc_overall_percentage($progress),
            'files_per_second'         => $this->calc_files_per_second($progress),
            'remaining_time'           => $this->calc_remaining_time($progress),
            'scan_type_percentage'     => $scan_percentage['scan_type'] ?? 0,
            'malware_sigs_percentage'  => $scan_percentage['malware_sigs'] ?? 0,
            'reputation_percentage'    => $scan_percentage['reputation'] ?? 0,
            'stage_statuses'           => $this->get_stage_statuses(),
            'comprehensive_statistics' => $this->get_comprehensive_statistics(),
            'scan_started_at'          => $progress['scan_start'] ?? ($progress['started'] ?? null),
            'plugin_version'           => VMPFENCE_VERSION,
        );

        $result = $this->api_client->push_scan_progress(
            $this->api_key,
            $this->site_uuid,
            $payload
        );

        if (is_wp_error($result)) {
            $this->push_failed = true;
            if (class_exists('VMPFence_Secure_Logger')) {
                VMPFence_Secure_Logger::log('warning', 'SIEM progress push failed — circuit breaker activated, skipping remaining pushes this scan', array(
                    'error' => $result->get_error_message(),
                ));
            }
            return;
        }

        update_option(self::LAST_PUSH_OPTION, time(), false);
    }

    /**
     * Push scan progress to server if enough time has elapsed.
     *
     * Throttled to self::PUSH_INTERVAL seconds to avoid flooding.
     * Reads from the vmpfence_scan_progress option written by
     * VMPFence_Progress_Tracker::update_progress().
     *
     * @since 2.2.4
     * @return void
     */
    public function maybe_push_progress(): void {
        if (empty($this->site_uuid) || empty($this->api_key)) {
            return;
        }

        // Throttle — only push every PUSH_INTERVAL seconds
        $last_push = (int) get_option(self::LAST_PUSH_OPTION, 0);
        $now       = time();
        if (($now - $last_push) < self::PUSH_INTERVAL) {
            return;
        }

        $progress = get_option('vmpfence_scan_progress', array());
        if (empty($progress) || ($progress['status'] ?? 'idle') === 'idle') {
            return;
        }

        // Collect scan-type percentage circles
        $scan_percentage = array(
            'scan_type'    => 0,
            'malware_sigs' => 0,
            'reputation'   => 0,
        );
        if (class_exists('VMPFence_Scan_Percentage') && method_exists('VMPFence_Scan_Percentage', 'calculateScanPercentageBreakdown')) {
            $raw = VMPFence_Scan_Percentage::calculateScanPercentageBreakdown();
            if (is_array($raw)) {
                $scan_percentage = array_merge($scan_percentage, $raw);
            }
        }

        $payload = array(
            'status'                   => $progress['status'] ?? 'idle',
            'scan_progress'            => $progress['scan_progress'] ?? 0,
            'total_scans'              => $progress['total_scans'] ?? 11,
            'current_section'          => $progress['current_section'] ?? '',
            'files_scanned'            => $progress['files_scanned'] ?? 0,
            'total_files'              => $progress['total_files'] ?? 0,
            'percentage'               => $this->calc_overall_percentage($progress),
            'files_per_second'         => $this->calc_files_per_second($progress),
            'remaining_time'           => $this->calc_remaining_time($progress),
            'scan_type_percentage'     => $scan_percentage['scan_type'] ?? 0,
            'malware_sigs_percentage'  => $scan_percentage['malware_sigs'] ?? 0,
            'reputation_percentage'    => $scan_percentage['reputation'] ?? 0,
            'stage_statuses'           => $this->get_stage_statuses(),
            'comprehensive_statistics' => $this->get_comprehensive_statistics(),
            'scan_started_at'          => $progress['scan_start'] ?? ($progress['started'] ?? null),
            'plugin_version'           => VMPFENCE_VERSION,
        );

        $this->api_client->push_scan_progress(
            $this->api_key,
            $this->site_uuid,
            $payload
        );

        update_option(self::LAST_PUSH_OPTION, $now, false);
    }

    /**
     * Push final scan results (issues) to server.
     *
     * Queries wp_vmpfence_scan_issues directly so we don't depend
     * on AJAX handler methods (which require nonce checks).
     *
     * @since 2.2.4
     * @since 2.2.5 Returns bool instead of void (FIX-5 v6.6).
     * @return bool True on success, false on API failure or missing credentials.
     */
    public function push_scan_results(): bool {
        if (empty($this->site_uuid) || empty($this->api_key)) {
            return false;
        }

        global $wpdb;
        $issues_table = $wpdb->prefix . 'vmpfence_scan_issues';

        // Fetch active (status = 'new') and ignored issues from DB
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $active_rows = $wpdb->get_results(
            "SELECT * FROM {$issues_table} WHERE status = 'new' ORDER BY detected_time DESC",
            ARRAY_A
        );
        $ignored_rows = $wpdb->get_results(
            "SELECT * FROM {$issues_table} WHERE status = 'ignored' ORDER BY detected_time DESC",
            ARRAY_A
        );
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        if (!is_array($active_rows)) {
            $active_rows = array();
        }
        if (!is_array($ignored_rows)) {
            $ignored_rows = array();
        }

        // Build issues payload
        $issues = array();

        foreach ($active_rows as $row) {
            $issues[] = $this->format_issue_for_server($row, false);
        }
        foreach ($ignored_rows as $row) {
            $issues[] = $this->format_issue_for_server($row, true);
        }

        // Get last scan metadata
        $last_scan_data = get_option('vmpfence_last_scan_data', array());

        $payload = array(
            'scan_completed_at'        => gmdate('Y-m-d\TH:i:s\Z'),
            'duration'                 => $last_scan_data['duration'] ?? 0,
            'issues'                   => $issues,
            'comprehensive_statistics' => $last_scan_data['comprehensive_statistics'] ?? array(),
            'active_count'             => count($active_rows),
            'ignored_count'            => count($ignored_rows),
            'plugin_version'           => VMPFENCE_VERSION,
        );

        $result = $this->api_client->push_scan_results(
            $this->api_key,
            $this->site_uuid,
            $payload
        );

        // FIX-5 (v6.6): Check API response so callers can distinguish success/failure.
        if (is_wp_error($result)) {
            if (class_exists('VMPFence_Secure_Logger')) {
                VMPFence_Secure_Logger::log('error', 'SIEM scan results push failed', array(
                    'error'   => $result->get_error_message(),
                    'active'  => count($active_rows),
                    'ignored' => count($ignored_rows),
                ));
            }
            return false;
        }

        if (class_exists('VMPFence_Secure_Logger')) {
            VMPFence_Secure_Logger::log('info', 'SIEM scan results pushed', array(
                'active'  => count($active_rows),
                'ignored' => count($ignored_rows),
            ));
        }
        return true;
    }

    /**
     * Push cancelled scan status to server.
     *
     * @since 2.2.4
     * @return void
     */
    public function push_cancelled(): void {
        if (empty($this->site_uuid) || empty($this->api_key)) {
            return;
        }

        $payload = array(
            'status'          => 'cancelled',
            'scan_progress'   => 0,
            'total_scans'     => 0,
            'percentage'      => 0,
            'current_section' => 'Scan cancelled',
            'plugin_version'  => VMPFENCE_VERSION,
        );

        $this->api_client->push_scan_progress(
            $this->api_key,
            $this->site_uuid,
            $payload
        );
    }

    /**
     * Format a single scan issue row for server payload.
     *
     * @since 2.2.4
     * @since 2.2.6 Added 'description' for proper issues on the portal
     * @param array $row     Database row from wp_vmpfence_scan_issues
     * @param bool  $ignored Whether the issue is ignored
     * @return array Formatted issue for server
     */
    private function format_issue_for_server(array $row, bool $ignored): array {
        $type        = $row['type'] ?? $row['issue_type'] ?? 'unknown';
        $file_path   = $row['file_path'] ?? '';
        $scanner     = $row['issue_type'] ?? '';
        $description = $row['description'] ?? '';

        // For scan_exclusions, the WP DB description is "title: {JSON}".
        // Extract clean title and human-readable description for the server.
        $title_for_server = $description;
        $desc_for_server  = $description;

        if ($type === 'scan_exclusions' && preg_match('/^(.*?):\s*(\{.+\})$/s', $description, $matches)) {
            $title_for_server = trim($matches[1]);
            $json_data = json_decode($matches[2], true);
            if ($json_data && isset($json_data['explanation'])) {
                $paths     = $json_data['skipped_paths'] ?? [];
                $count     = count($paths);
                $path_text = $count === 1 ? 'path' : 'paths';
                if ($count === 1) {
                    $path_list = $paths[0] ?? '';
                } elseif ($count === 2) {
                    $path_list = implode(' and ', $paths);
                } else {
                    $last      = array_pop($paths);
                    $path_list = implode(', ', $paths) . ', and ' . $last;
                }
                $desc_for_server = $json_data['explanation'] . "\n\nThe {$path_text} skipped "
                    . ($count === 1 ? 'is' : 'are') . ": {$path_list}";
            }
        }

        return array(
            'issue_hash'  => hash('sha256', $type . '|' . $file_path . '|' . $scanner . '|' . $description),
            'issue_type'  => $type,
            'severity'    => $row['severity'] ?? 'medium',
            'scanner'     => $scanner,
            'title'       => $title_for_server,
            'description' => $desc_for_server,
            'file_path'   => $file_path,
            'data'        => array(
                'scan_id'       => $row['scan_id'] ?? null,
                'file_hash'     => $row['file_hash'] ?? '',
                'file_size'     => $row['file_size'] ?? 0,
                'is_repairable' => (bool) ($row['is_repairable'] ?? false),
                'repair_status' => $row['repair_status'] ?? 'not_checked',
            ),
            'ignored'     => $ignored,
            'fixed'       => ($row['status'] ?? '') === 'cleaned',
        );
    }

    /**
     * Get per-stage status indicators.
     *
     * Maps each scan stage to: pending | scanning | success | warning.
     *
     * @since 2.2.4
     * @return array Stage name => status string
     */
    private function get_stage_statuses(): array {
        $stages = array(
            'server_state',
            'file_changes',
            'user_security',
            'content_safety',
            'public_files',
            'user_option_audit',
            'vulnerability_scan',
            'malware_scan',
            'blocklist_check',
            'spamvertising_checks',
            'spam_check',
        );

        $progress      = get_option('vmpfence_scan_progress', array());
        $current_stage = (int) ($progress['scan_progress'] ?? 0);
        $last_data     = get_option('vmpfence_last_scan_data', array());
        $stats         = $last_data['comprehensive_statistics'] ?? array();

        $statuses = array();
        foreach ($stages as $i => $stage) {
            $stage_num = $i + 1;
            if ($stage_num < $current_stage) {
                $has_issues       = ($stats[$stage]['issues'] ?? 0) > 0;
                $statuses[$stage] = $has_issues ? 'warning' : 'success';
            } elseif ($stage_num === $current_stage) {
                $statuses[$stage] = 'scanning';
            } else {
                $statuses[$stage] = 'pending';
            }
        }

        return $statuses;
    }

    /**
     * Get comprehensive scan statistics from cached last scan data.
     *
     * @since 2.2.4
     * @return array Statistics array
     */
    private function get_comprehensive_statistics(): array {
        $last_data = get_option('vmpfence_last_scan_data', array());
        return $last_data['comprehensive_statistics'] ?? array();
    }

    /**
     * Calculate overall percentage from progress data.
     *
     * @since 2.2.4
     * @param array $progress Scan progress data
     * @return float Percentage 0.0 – 100.0
     */
    private function calc_overall_percentage(array $progress): float {
        $stage = (int) ($progress['scan_progress'] ?? 0);
        $total = (int) ($progress['total_scans'] ?? 11);
        if ($total === 0) {
            return 0.0;
        }
        return round(($stage / $total) * 100, 1);
    }

    /**
     * Compute files-per-second from progress data.
     *
     * Falls back to 0 if scan_start is unknown or no files scanned yet.
     *
     * @since 2.2.4
     * @param array $progress Scan progress data
     * @return float Files per second
     */
    private function calc_files_per_second(array $progress): float {
        $files_scanned = (int) ($progress['files_scanned'] ?? 0);
        if ($files_scanned <= 0) {
            return 0.0;
        }
        $start   = (int) ($progress['scan_start'] ?? ($progress['last_update'] ?? time()));
        $elapsed = max(1, time() - $start);
        return round($files_scanned / $elapsed, 2);
    }

    /**
     * Compute estimated remaining time in seconds from progress data.
     *
     * @since 2.2.4
     * @param array $progress Scan progress data
     * @return int Estimated seconds remaining (0 if unknown)
     */
    private function calc_remaining_time(array $progress): int {
        $fps = $this->calc_files_per_second($progress);
        if ($fps <= 0) {
            return 0;
        }
        $remaining_files = max(0, (int) ($progress['total_files'] ?? 0) - (int) ($progress['files_scanned'] ?? 0));
        return (int) round($remaining_files / $fps);
    }
}
