<?php
/**
 * VmpFence SIEM Command Executor
 *
 * Polls the Newton Security server for pending commands (from the SIEM
 * website) and executes them locally on the WordPress site. Scoped to
 * Scan Page + Options Page actions only — 10 command types.
 *
 * Security:
 *   - Settings whitelist prevents arbitrary option injection
 *   - No file deletion commands available remotely
 *   - Issue actions use issue_hash (SHA256) not raw paths
 *   - Scan concurrency lock prevents double scans
 *   - Settings changes trigger immediate re-sync to server
 *
 * @package    VMPFence
 * @subpackage Includes
 * @since      2.2.4
 */

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

class VmpFence_Command_Executor {

    /**
     * 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;

    /**
     * Map of command types to handler methods.
     *
     * v2.2.4 SCOPE: Only Scan Page + Options Page commands.
     *
     * @var array
     */
    private $command_handlers = array(
        // === SCAN PAGE ACTIONS ===
        'start_scan'       => 'handle_start_scan',
        'start_yara_scan'  => 'handle_start_yara_scan',
        'cancel_scan'      => 'handle_cancel_scan',
        'ignore_issue'     => 'handle_ignore_issue',
        'unignore_issue'   => 'handle_unignore_issue',
        'mark_as_fixed'    => 'handle_mark_as_fixed',
        'repair_file'      => 'handle_repair_file',
        'bulk_repair'      => 'handle_bulk_repair',

        // === OPTIONS PAGE ACTIONS ===
        'update_settings'   => 'handle_update_settings',
        'save_scan_options' => 'handle_save_scan_options',
    );

    /**
     * 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', '');
    }

    /**
     * Poll server for pending commands, execute each, report results.
     * Called from VmpFence_Sync_Manager::sync_all().
     *
     * @since 2.2.4
     * @return array Execution summary
     */
    public function poll_and_execute(): array {
        if (empty($this->site_uuid) || empty($this->api_key)) {
            return array('success' => false, 'error' => 'No site UUID or API key');
        }

        $response = $this->api_client->get_pending_commands(
            $this->api_key,
            $this->site_uuid
        );

        if (is_wp_error($response)) {
            return array('success' => false, 'error' => $response->get_error_message());
        }

        // Server wraps commands in {"commands": [...]} — unwrap
        $commands = array();
        if (is_array($response) && isset($response['commands']) && is_array($response['commands'])) {
            $commands = $response['commands'];
        } elseif (is_array($response) && !isset($response['commands'])) {
            // Fallback: treat response as flat array if server format changes
            $commands = $response;
        }

        if (empty($commands)) {
            return array('success' => true, 'executed' => 0);
        }

        $results = array();
        foreach ($commands as $command) {
            if (!is_array($command) || empty($command['id'])) {
                continue;
            }
            $results[] = $this->execute_command($command);
        }

        if (class_exists('VMPFence_Secure_Logger')) {
            VMPFence_Secure_Logger::log('info', 'SIEM commands executed', array(
                'count' => count($results),
            ));
        }

        return array('success' => true, 'executed' => count($results), 'results' => $results);
    }

    /**
     * Execute a single command: acknowledge -> run handler -> report result.
     *
     * @since 2.2.4
     * @param array $command Command data from server
     * @return array Per-command result
     */
    private function execute_command(array $command): array {
        $id     = $command['id'];
        $type   = $command['command_type'] ?? '';
        $params = $command['parameters'] ?? array();

        // Acknowledge receipt first
        $this->api_client->acknowledge_command(
            $this->api_key,
            $this->site_uuid,
            $id
        );

        if (!isset($this->command_handlers[$type])) {
            $this->api_client->complete_command(
                $this->api_key,
                $this->site_uuid,
                $id,
                'failed',
                'Unknown command type: ' . sanitize_text_field($type)
            );
            return array('id' => $id, 'status' => 'failed', 'error' => 'Unknown command');
        }

        try {
            $method = $this->command_handlers[$type];
            $result = $this->$method($params);

            $this->api_client->complete_command(
                $this->api_key,
                $this->site_uuid,
                $id,
                'completed',
                null,
                $result
            );

            return array('id' => $id, 'status' => 'completed', 'result' => $result);
        } catch (\Exception $e) {
            $this->api_client->complete_command(
                $this->api_key,
                $this->site_uuid,
                $id,
                'failed',
                $e->getMessage()
            );
            return array('id' => $id, 'status' => 'failed', 'error' => $e->getMessage());
        }
    }

    // ========================================================================
    // SCAN PAGE COMMAND HANDLERS
    // ========================================================================

    /**
     * Start a security scan (same as clicking "START NEW SCAN" button).
     *
     * @since 2.2.4
     * @param array $params {scan_type?: "standard"|"high_sensitivity"|"limited"|"custom"}
     * @return array Result
     */
    private function handle_start_scan(array $params): array {
        $scan_type = sanitize_text_field($params['scan_type'] ?? 'standard');

        // Check if scan already running
        $lock = get_transient('vmpfence_scan_lock');
        if ($lock) {
            return array('success' => false, 'error' => 'Scan already in progress');
        }

        // Set scan lock (4 hour max TTL)
        set_transient('vmpfence_scan_lock', time(), 4 * HOUR_IN_SECONDS);

        // Apply scan type preset if the method exists
        if ($scan_type !== 'custom' && class_exists('VMPFence_Scan_Options_Ajax') && method_exists('VMPFence_Scan_Options_Ajax', 'apply_preset')) {
            VMPFence_Scan_Options_Ajax::apply_preset($scan_type);
        }

        // Schedule the async scan worker with arguments (same format as AJAX handler)
        $event_timestamp = time();
        $event_args = array(
            'scan_type' => $scan_type,
            'user_id'   => 0,
            'timestamp' => $event_timestamp,
        );
        wp_schedule_single_event($event_timestamp, 'vmpfence_async_scan_worker', $event_args);

        // Fire loopback to trigger WP-Cron immediately
        wp_remote_post(admin_url('admin-ajax.php'), array(
            'timeout'   => 0.01,
            'blocking'  => false,
            'sslverify' => false,
            'body'      => array(
                'action' => 'vmpfence_trigger_cron',
            ),
        ));

        // Initialize progress indicator
        update_option('vmpfence_scan_progress', array(
            'status'          => 'initializing',
            'scan_progress'   => 0,
            'total_scans'     => 11,
            'current_section' => 'Initializing scan...',
            'files_scanned'   => 0,
            'total_files'     => 0,
            'async_mode'      => true,
            'last_update'     => time(),
            'scan_start'      => time(),
            'source'          => 'siem_portal',
        ));

        // Push initializing status to server immediately so the portal
        // shows progress within seconds instead of waiting for the 5-min cron.
        try {
            (new VmpFence_Scan_Sync())->push_immediate_status('initializing', 'Initializing scan...');
        } catch (\Throwable $e) {
            // Non-fatal — worst case the next cron cycle will push progress.
        }

        return array(
            'success'   => true,
            'message'   => 'Scan started',
            'scan_type' => $scan_type,
        );
    }

    /**
     * Start YARA deep scan.
     *
     * @since 2.2.4
     * @param array $params {directories?: [...], file_extensions?: [...]}
     * @return array Result
     */
    private function handle_start_yara_scan(array $params): array {
        if (!class_exists('VMPFence_YARA_Scanner_Ajax')) {
            return array('success' => false, 'error' => 'YARA scanner not available');
        }

        $directories = isset($params['directories']) && is_array($params['directories'])
            ? array_map('sanitize_text_field', $params['directories'])
            : array(ABSPATH);
        $extensions  = isset($params['file_extensions']) && is_array($params['file_extensions'])
            ? array_map('sanitize_text_field', $params['file_extensions'])
            : array('php', 'js', 'html');

        update_option('vmpfence_yara_batch_config', array(
            'directories' => $directories,
            'extensions'  => $extensions,
            'source'      => 'siem_portal',
        ));

        wp_schedule_single_event(time(), 'vmpfence_yara_batch_scan_worker');

        return array('success' => true, 'message' => 'YARA scan started');
    }

    /**
     * Cancel a running scan.
     *
     * @since 2.2.4
     * @param array $params Unused
     * @return array Result
     */
    private function handle_cancel_scan(array $params): array {
        delete_transient('vmpfence_scan_lock');
        delete_option('vmpfence_scan_progress');

        // Push cancelled status to server immediately
        if (class_exists('VmpFence_Scan_Sync')) {
            $scan_sync = new VmpFence_Scan_Sync();
            $scan_sync->push_cancelled();
        }

        return array('success' => true, 'message' => 'Scan cancelled');
    }

    /**
     * Ignore a scan issue.
     *
     * @since 2.2.4
     * @param array $params {issue_hash: "abc123...", issue_type: "public_files"}
     * @return array Result
     */
    private function handle_ignore_issue(array $params): array {
        $hash = sanitize_text_field($params['issue_hash'] ?? '');
        $type = sanitize_text_field($params['issue_type'] ?? '');

        if (empty($hash)) {
            return array('success' => false, 'error' => 'No issue_hash provided');
        }

        $ignored        = get_option('vmpfence_ignored_issues', array());
        $ignored[$hash] = array(
            'type'       => $type,
            'ignored_at' => time(),
            'source'     => 'siem_portal',
        );
        update_option('vmpfence_ignored_issues', $ignored);

        // Also update status in wp_vmpfence_scan_issues table so push_scan_results() picks it up
        $this->update_scan_issue_status_by_hash($hash, 'ignored');

        return array('success' => true, 'issue_hash' => $hash, 'action' => 'ignored');
    }

    /**
     * Unignore a scan issue.
     *
     * @since 2.2.4
     * @param array $params {issue_hash: "abc123..."}
     * @return array Result
     */
    private function handle_unignore_issue(array $params): array {
        $hash = sanitize_text_field($params['issue_hash'] ?? '');

        if (empty($hash)) {
            return array('success' => false, 'error' => 'No issue_hash provided');
        }

        $ignored = get_option('vmpfence_ignored_issues', array());
        unset($ignored[$hash]);
        update_option('vmpfence_ignored_issues', $ignored);

        // Also revert status in wp_vmpfence_scan_issues table back to 'new'
        $this->update_scan_issue_status_by_hash($hash, 'new');

        return array('success' => true, 'issue_hash' => $hash, 'action' => 'unignored');
    }

    /**
     * Mark issue as fixed.
     *
     * @since 2.2.4
     * @param array $params {issue_hash: "abc123..."}
     * @return array Result
     */
    private function handle_mark_as_fixed(array $params): array {
        $hash = sanitize_text_field($params['issue_hash'] ?? '');

        if (empty($hash)) {
            return array('success' => false, 'error' => 'No issue_hash provided');
        }

        $fixed        = get_option('vmpfence_fixed_issues', array());
        $fixed[$hash] = array(
            'fixed_at' => time(),
            'source'   => 'siem_portal',
        );
        update_option('vmpfence_fixed_issues', $fixed);

        // Also update status in wp_vmpfence_scan_issues table
        $this->update_scan_issue_status_by_hash($hash, 'cleaned');

        return array('success' => true, 'issue_hash' => $hash, 'action' => 'marked_fixed');
    }

    /**
     * Repair a modified file (download original from WP.org).
     *
     * Uses VMPFence_AJAX_Handler::repair_file_headless() if available,
     * otherwise returns not-available gracefully.
     *
     * @since 2.2.4
     * @param array $params {file_path: "wp-includes/version.php", issue_hash: "abc123..."}
     * @return array Result
     */
    private function handle_repair_file(array $params): array {
        $file_path = sanitize_text_field($params['file_path'] ?? '');

        if (empty($file_path)) {
            return array('success' => false, 'error' => 'No file_path provided');
        }

        // Validate path is within ABSPATH (prevent path traversal)
        $real_path = realpath(ABSPATH . $file_path);
        if ($real_path === false || strpos($real_path, realpath(ABSPATH)) !== 0) {
            return array('success' => false, 'error' => 'Invalid file path');
        }

        // Use headless repair if available
        if (class_exists('VMPFence_AJAX_Handler') && method_exists('VMPFence_AJAX_Handler', 'repair_file_headless')) {
            $result = VMPFence_AJAX_Handler::repair_file_headless($file_path);
            return array('success' => (bool) $result, 'file_path' => $file_path, 'action' => 'repaired');
        }

        return array('success' => false, 'error' => 'Repair method not available in this version');
    }

    /**
     * Bulk repair multiple files.
     *
     * @since 2.2.4
     * @param array $params {files: [{file_path: "...", issue_hash: "..."}, ...]}
     * @return array Result
     */
    private function handle_bulk_repair(array $params): array {
        $files = $params['files'] ?? array();

        if (empty($files) || !is_array($files)) {
            return array('success' => false, 'error' => 'No files provided');
        }

        $repaired = 0;
        $failed   = 0;

        foreach ($files as $file) {
            if (!is_array($file)) {
                $failed++;
                continue;
            }
            $result = $this->handle_repair_file($file);
            if (!empty($result['success'])) {
                $repaired++;
            } else {
                $failed++;
            }
        }

        return array(
            'success'  => true,
            'repaired' => $repaired,
            'failed'   => $failed,
            'total'    => count($files),
        );
    }

    // ========================================================================
    // OPTIONS PAGE COMMAND HANDLERS
    // ========================================================================

    /**
     * Update plugin settings.
     *
     * Only whitelisted keys are accepted — see get_allowed_setting_keys().
     * Triggers an immediate settings re-sync to server after changes.
     *
     * @since 2.2.4
     * @param array $params {settings: {key: value, ...}}
     * @return array Result
     */
    private function handle_update_settings(array $params): array {
        $settings = $params['settings'] ?? array();

        if (empty($settings) || !is_array($settings)) {
            return array('success' => false, 'error' => 'No settings provided');
        }

        $allowed_keys = self::get_allowed_setting_keys();
        $updated      = 0;
        $rejected     = array();

        foreach ($settings as $key => $value) {
            $key = sanitize_text_field($key);
            if (in_array($key, $allowed_keys, true)) {
                VMPFence_Config::set($key, $value);
                $updated++;
            } else {
                $rejected[] = $key;
            }
        }

        // Push updated settings to server immediately
        if (class_exists('VmpFence_Sync_Settings')) {
            VmpFence_Sync_Settings::sync();
        }

        return array(
            'success'       => true,
            'updated'       => $updated,
            'rejected_keys' => $rejected,
        );
    }

    /**
     * Save scan options (scheduling, performance, enabled checks).
     *
     * Triggers immediate settings re-sync and reschedules scan cron
     * if scheduling options changed.
     *
     * @since 2.2.4
     * @param array $params {scan_options: {scheduledScansEnabled, schedMode, ...}}
     * @return array Result
     */
    private function handle_save_scan_options(array $params): array {
        $options = $params['scan_options'] ?? array();

        if (empty($options) || !is_array($options)) {
            return array('success' => false, 'error' => 'No scan options provided');
        }

        // Save scan scheduling options
        $schedule_keys = array(
            'scheduledScansEnabled', 'schedMode', 'schedPattern',
            'schedStartHour', 'schedTimezone', 'schedRandomOffset',
        );
        foreach ($schedule_keys as $key) {
            if (isset($options[$key])) {
                VMPFence_Config::set($key, $options[$key]);
            }
        }

        // Save performance options
        $perf_keys = array(
            'lowResourceScansEnabled', 'scan_maxIssues', 'scan_maxDuration',
            'maxMem', 'scan_maxExecTime', 'scan_exclude',
        );
        foreach ($perf_keys as $key) {
            if (isset($options[$key])) {
                VMPFence_Config::set($key, $options[$key]);
            }
        }

        // Save scan-enabled toggles (any key starting with scansEnabled_ plus known extras)
        foreach ($options as $key => $value) {
            if (strpos($key, 'scansEnabled_') === 0
                || in_array($key, array('spamvertizeCheck', 'checkSpamIP', 'other_scanOutside', 'scansEnabled_scanImages'), true)
            ) {
                VMPFence_Config::set(sanitize_text_field($key), $value);
            }
        }

        // Reschedule scan cron if scheduling option changed
        if (isset($options['scheduledScansEnabled'])) {
            if ($options['scheduledScansEnabled']) {
                if (!wp_next_scheduled('vmpfence_scheduled_scan')) {
                    wp_schedule_event(time(), 'daily', 'vmpfence_scheduled_scan');
                }
            } else {
                wp_clear_scheduled_hook('vmpfence_scheduled_scan');
            }
        }

        // Push updated settings to server immediately
        if (class_exists('VmpFence_Sync_Settings')) {
            VmpFence_Sync_Settings::sync();
        }

        return array('success' => true, 'message' => 'Scan options saved');
    }

    // ========================================================================
    // DB HELPERS
    // ========================================================================

    /**
     * Update scan issue status in wp_vmpfence_scan_issues table by issue hash.
     *
     * Computes hashes for all rows matching a broad filter and updates
     * the row whose SHA-256 hash matches the supplied hash. This keeps
     * the wp_options and DB table in sync so push_scan_results() reports
     * the correct status.
     *
     * @since 2.2.4
     * @param string $hash  Issue hash (SHA-256 of "type|file_path|scanner")
     * @param string $status Target status ('ignored', 'new', 'cleaned', etc.)
     * @return bool True if a row was updated
     */
    private function update_scan_issue_status_by_hash(string $hash, string $status): bool {
        global $wpdb;
        $table = $wpdb->prefix . 'vmpfence_scan_issues';

        // Fetch candidates — get all rows not already in the target status
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, issue_type, type, file_path FROM {$table} WHERE status != %s LIMIT 5000",
                $status
            ),
            ARRAY_A
        );

        if (empty($rows)) {
            return false;
        }

        $updated = false;

        foreach ($rows as $row) {
            // Hash formula must match format_issue_for_server() in class-vmpfence-scan-sync.php
            $type      = $row['type'] ?? $row['issue_type'] ?? 'unknown';
            $file_path = $row['file_path'] ?? '';
            $scanner   = $row['issue_type'] ?? '';
            $computed  = hash('sha256', $type . '|' . $file_path . '|' . $scanner);

            if ($computed === $hash) {
                $wpdb->update(
                    $table,
                    array('status' => $status),
                    array('id' => $row['id']),
                    array('%s'),
                    array('%d')
                );
                $updated = true;
                // Don't return — continue updating all matching rows (same issue across scan runs)
            }
        }

        return $updated;
    }

    // ========================================================================
    // SETTINGS WHITELIST
    // ========================================================================

    /**
     * Whitelist of setting keys that can be changed remotely.
     * Any key NOT in this list is rejected by handle_update_settings().
     *
     * @since 2.2.4
     * @return array Flat array of allowed key names
     */
    private static function get_allowed_setting_keys(): array {
        return array(
            // Firewall options
            'waf_enabled', 'waf_protection_level',
            'brute_force_enabled', 'max_login_attempts', 'max_forgot_password_attempts',
            'count_failures_window', 'lockout_duration', 'permanent_ban_threshold',
            'lock_invalid_usernames', 'prevent_leaked_passwords_enabled',
            'enforce_strong_passwords', 'dont_reveal_users',
            'prevent_admin_username', 'prevent_username_discovery',
            'disable_app_passwords', 'block_post_requests', 'check_password_strength',
            'participate_security_network',
            'rate_limiting_enabled', 'realtime_ip_blocklist_enabled',
            'global_rate_limit', 'global_rate_action',
            'crawler_rate_limit', 'crawler_rate_action',
            'human_rate_limit', 'human_rate_action',
            'rate_block_duration',
            'trusted_proxies', 'trusted_proxy_preset',
            'whitelisted_ips_bypass', 'blocked_urls',

            // Scan options
            'scheduledScansEnabled', 'schedMode', 'schedPattern',
            'lowResourceScansEnabled', 'scan_maxIssues', 'scan_maxDuration',
            'scan_exclude',
            'scansEnabled_malware', 'scansEnabled_fileContents', 'scansEnabled_core',
            'scansEnabled_plugins', 'scansEnabled_themes', 'scansEnabled_coreUnknown',
            'scansEnabled_posts', 'scansEnabled_comments', 'scansEnabled_suspiciousOptions',
            'scansEnabled_oldVersions', 'scansEnabled_suspiciousAdminUsers',
            'scansEnabled_passwds', 'scansEnabled_checkGSB',
            'spamvertizeCheck', 'checkSpamIP',
            'scansEnabled_diskSpace', 'scansEnabled_wafStatus',
            'scansEnabled_checkReadableConfig', 'scansEnabled_suspectedFiles',
            'exclude_directories', 'whitelisted_services',

            // General options
            'auto_update', 'notification_email', 'ip_detection_method',
            'hide_wp_version', 'pause_live_updates', 'update_interval',
            'display_all_options_menu', 'blocking_menu_display',
            'display_live_traffic_menu', 'display_audit_log_menu',

            // Email alerts
            'alert_on_auto_update', 'alert_on_deactivation', 'alert_on_waf_disabled',
            'alert_on_scan_results', 'alert_scan_severity', 'alert_on_ip_blocked',
            'alert_on_lockout', 'max_email_alerts_per_hour',
            'alert_on_breached_password', 'alert_on_lost_password',
            'alert_on_admin_login', 'alert_admin_login_new_device',
            'alert_on_user_login', 'alert_user_login_new_device',
            'alert_on_attack_increase',

            // Login security
            'two_factor_enabled',

            // Activity report
            'enable_email_summary', 'email_summary_frequency', 'enable_activity_widget',

            // Dashboard notifications
            'notify_updates_needed', 'notify_scan_status',
            'notify_security_alerts', 'notify_promotions',
        );
    }
}
