<?php
/**
 * VulnerabilityAjax — AJAX handlers for the Vulnerability Scanner module.
 *
 * Registered actions:
 *   ultraguard_vuln_run_scan
 *   ultraguard_vuln_stop_scan          ← NEW
 *   ultraguard_vuln_scan_status
 *   ultraguard_vuln_apply_virtual_patch
 *   ultraguard_vuln_ignore_vulnerability
 *   ultraguard_vuln_update_component
 *   ultraguard_save_vulnerability_settings
 *
 * @package UltraguardPro\modules\vulnerability\ajax
 */

namespace UltraguardPro\modules\vulnerability\ajax;

use UltraguardPro\Plugin;
use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\core\Logger;
use UltraguardPro\modules\core\ModuleAjaxSettingsTrait;
use UltraguardPro\support\AjaxResponseBuilder;
use UltraguardPro\support\LicenseInfo;
use UltraguardPro\support\UpgradeManager;
use UltraguardPro\modules\vulnerability\VulnerabilityScanner;
use UltraguardPro\modules\vulnerability\VirtualPatcher;

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

class VulnerabilityAjax {

    use ModuleAjaxSettingsTrait;

    private const NONCE_ACTION      = 'ultraguard_admin_nonce';
    private const STATUS_TRANSIENT  = 'ultraguard_vuln_scan_status';
    private const RUNNING_TRANSIENT = 'ultraguard_vuln_scan_running';
    private const STALE_SECONDS     = 600;  // 10 min with ≤5% progress = stale

    private Plugin $plugin;

    public function __construct( Plugin $plugin ) {
        $this->plugin = $plugin;
    }

    public function register(): void {
        $actions = [
            'ultraguard_vuln_run_scan'               => 'handle_run_scan',
            'ultraguard_vuln_stop_scan'              => 'handle_stop_scan',
            'ultraguard_vuln_scan_status'            => 'handle_scan_status',
            'ultraguard_vuln_apply_virtual_patch'    => 'handle_apply_virtual_patch',
            'ultraguard_vuln_ignore_vulnerability'   => 'handle_ignore_vulnerability',
            'ultraguard_vuln_update_component'       => 'handle_update_component',
            'ultraguard_save_vulnerability_settings' => 'handle_save_settings',
            // ── Realtime fallback polling ─────────────────────────────────────
            'ultraguard_rt_vuln_scan_status'         => 'handle_rt_vuln_scan_status',
        ];

        foreach ( $actions as $action => $method ) {
            add_action( 'wp_ajax_' . $action, [ $this, $method ] );
        }
    }

    // ── Handlers ───────────────────────────────────────────────────────────────

    /**
     * Queue a vulnerability scan via WP-Cron.
     * BUG FIX: scan_id is now stored in the status transient so JS can track it.
     */
    public function handle_run_scan(): void {
        $this->verify();

        if ( ! LicenseInfo::instance()->isPro() ) {
            UpgradeManager::soft_gate_response(
                'vulnerability_scan',
                __( 'run a vulnerability scan', 'ultraguard-security-suite' )
            );
            return;
        }

        if ( get_transient( self::RUNNING_TRANSIENT ) ) {
            AjaxResponseBuilder::make()
                ->error( __( 'A vulnerability scan is already in progress.', 'ultraguard-security-suite' ) )
                ->send();
            return;
        }

        // BUG FIX: generate a scan_id and include it in the status transient
        $scan_id = 'vuln_' . wp_generate_uuid4();

        set_transient( self::RUNNING_TRANSIENT, $scan_id, HOUR_IN_SECONDS );
        set_transient( self::STATUS_TRANSIENT, [
            'scan_id'  => $scan_id,
            'status'   => 'queued',
            'progress' => 0,
            'phase'    => __( 'Queued — waiting for cron…', 'ultraguard-security-suite' ),
            'started'  => gmdate( 'Y-m-d H:i:s' ),
        ], HOUR_IN_SECONDS );

        wp_schedule_single_event( time() + 3, 'ultraguard_pro_vulnerability_scan', [ $scan_id ] );

        if ( function_exists( 'spawn_cron' ) ) { spawn_cron(); }

        AjaxResponseBuilder::make()
            ->success( __( 'Vulnerability scan queued. Results will appear shortly.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Vulnerability scan started.', 'ultraguard-security-suite' ), 'success' )
            ->send( 200, [ 'scan_id' => $scan_id ] );
    }

    /**
     * NEW: Stop a running vulnerability scan.
     */
    public function handle_stop_scan(): void {
        $this->verify();

        delete_transient( self::RUNNING_TRANSIENT );

        $status = get_transient( self::STATUS_TRANSIENT ) ?: [];
        if ( ! empty( $status ) ) {
            set_transient( self::STATUS_TRANSIENT, array_merge( $status, [
                'status'   => 'cancelled',
                'progress' => 0,
            ] ), HOUR_IN_SECONDS );
        }

        AjaxResponseBuilder::make()
            ->success( __( 'Vulnerability scan stopped.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Scan cancelled.', 'ultraguard-security-suite' ), 'info' )
            ->send();
    }

    /**
     * Return current scan progress.
     * BUG FIX: stale-scan auto-cancellation added (mirrors antivirus pattern).
     */
    public function handle_scan_status(): void {
        $this->verify();

        $status = get_transient( self::STATUS_TRANSIENT ) ?: [
            'scan_id'  => '',
            'status'   => 'idle',
            'progress' => 0,
        ];

        // Auto-cancel stale scans: running >10 min and still ≤5%
        $status = $this->maybe_cancel_stale_scan( $status );

        if ( ( $status['status'] ?? '' ) === 'complete' ) {
            $table = $this->get_table( 'vulnerability_results' );
            $status['summary'] = $table ? $table->get_summary() : [];
        }

        AjaxResponseBuilder::make()->success()->mergeData( (array) $status )->send();
    }

    /**
     * Apply a virtual patch for a specific vulnerability (Pro).
     *
     * POST: vuln_id (int)
     */
    public function handle_apply_virtual_patch(): void {
        $this->verify();

        if ( ! LicenseInfo::instance()->isPro() ) {
            UpgradeManager::soft_gate_response(
                'virtual_patching',
                __( 'apply a virtual patch', 'ultraguard-security-suite' )
            );
            return;
        }

        $vuln_id = absint( $_POST['vuln_id'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by $this->verify() at the start of this handler.
        if ( ! $vuln_id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid vulnerability ID.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        $vuln = DatabaseCrud::get_by_id( 'ultraguard_vulnerability_results', $vuln_id );
        if ( ! $vuln ) {
            AjaxResponseBuilder::make()->error( __( 'Vulnerability not found.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        $vuln_table  = $this->get_table( 'vulnerability_results' );
        $patch_table = $this->get_table( 'virtual_patches' );

        if ( ! $vuln_table || ! $patch_table ) {
            AjaxResponseBuilder::make()->error( __( 'DB tables unavailable.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        $patcher = new VirtualPatcher( $vuln_table, $patch_table );
        $success = $patcher->apply_virtual_patch( (array) $vuln );

        if ( ! $success ) {
            AjaxResponseBuilder::make()
                ->error( __( 'Could not apply virtual patch. The CVE may already be patched, or the WAF table is unavailable.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Virtual patch failed.', 'ultraguard-security-suite' ), 'error' )
                ->send();
            return;
        }

        $row_sel = '#ug-vuln-row-' . $vuln_id;

        AjaxResponseBuilder::make()
            ->success( __( 'Virtual patch applied. WAF rule is now active.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Virtual patch applied successfully.', 'ultraguard-security-suite' ), 'success' )
            ->addUpdate(
                $row_sel . ' .ultraguard-cell-status',
                '<span class="ultraguard-cell-inner info">' . esc_html__( 'Virtual Patched', 'ultraguard-security-suite' ) . '</span>'
            )
            ->emptyElement( $row_sel . ' .ultraguard-cell-patch-actions' )
            ->send();
    }

    /**
     * Mark a vulnerability as ignored / false positive.
     *
     * POST: vuln_id (int)
     */
    public function handle_ignore_vulnerability(): void {
        $this->verify();

        $vuln_id = absint( $_POST['vuln_id'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by $this->verify() at the start of this handler.
        if ( ! $vuln_id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid vulnerability ID.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        DatabaseCrud::update(
            'ultraguard_vulnerability_results',
            [ 'status' => 'ignored' ],
            [ 'id' => $vuln_id ]
        );

        $row_sel = '#ug-vuln-row-' . $vuln_id;

        AjaxResponseBuilder::make()
            ->success( __( 'Vulnerability ignored.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Vulnerability marked as ignored.', 'ultraguard-security-suite' ), 'info' )
            ->addUpdate(
                $row_sel . ' .ultraguard-cell-status',
                '<span class="ultraguard-cell-inner secondary">' . esc_html__( 'Ignored', 'ultraguard-security-suite' ) . '</span>'
            )
            ->emptyElement( $row_sel . ' .ultraguard-cell-patch-actions' )
            ->send();
    }

    /**
     * Trigger a WordPress core/plugin/theme update for a specific component.
     *
     * POST: slug (string), type ('plugin'|'theme'|'core')
     */
    public function handle_update_component(): void {
        $this->verify();

        $slug = sanitize_text_field( wp_unslash( $_POST['slug'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by $this->verify() at the start of this handler.
        $type = sanitize_key( wp_unslash( $_POST['type'] ?? 'plugin'  ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by $this->verify() at the start of this handler.

        if ( ! in_array( $type, [ 'plugin', 'theme', 'core' ], true ) ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid component type.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
        require_once ABSPATH . 'wp-admin/includes/plugin.php';

        $skin = new \WP_Ajax_Upgrader_Skin();

        if ( $type === 'core' ) {
            $upgrader = new \Core_Upgrader( $skin );
            $result   = $upgrader->upgrade( get_bloginfo( 'version' ) );
        } elseif ( $type === 'theme' ) {
            $upgrader = new \Theme_Upgrader( $skin );
            $result   = $upgrader->upgrade( $slug );
        } else {
            $plugin_file = $this->find_plugin_file( $slug );
            if ( ! $plugin_file ) {
                AjaxResponseBuilder::make()->error( __( 'Plugin not found.', 'ultraguard-security-suite' ) )->send();
                return;
            }
            $upgrader = new \Plugin_Upgrader( $skin );
            $result   = $upgrader->upgrade( $plugin_file );
        }

        if ( is_wp_error( $result ) ) {
            AjaxResponseBuilder::make()
                ->error( $result->get_error_message() )
                ->addNotification( $result->get_error_message(), 'error' )
                ->send();
            return;
        }

        AjaxResponseBuilder::make()
            /* translators: %s: string */
            ->success( sprintf( __( '%s updated successfully.', 'ultraguard-security-suite' ), esc_html( $slug ) ) )
            /* translators: %s: string */
            ->addNotification( sprintf( __( '%s updated.', 'ultraguard-security-suite' ), esc_html( $slug ) ), 'success' )
            ->addReload( 2000 )
            ->send();
    }

    // ── Settings ───────────────────────────────────────────────────────────────

    private function field_schema(): array {
        return [
            'wpscan_api_key'                   => [ 'type' => 'text',   'default' => '' ],
            'vulnerability_auto_scan'          => [ 'type' => 'bool' ],
            'vulnerability_virtual_patch'      => [ 'type' => 'bool' ],
            'vulnerability_notify_on_critical' => [ 'type' => 'bool' ],
        ];
    }

    public function handle_save_settings(): void {
        $this->verify();

        $data = $this->sanitize_fields( (array) wp_unslash( $_POST ), $this->field_schema() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified by $this->verify() at the start of this handler.
        $ok   = $this->plugin->settings->save_module_settings( 'vulnerability', $data );

        if ( ! $ok ) {
            AjaxResponseBuilder::make()->error( __( 'Failed to save settings.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        AjaxResponseBuilder::make()
            ->success( __( 'Vulnerability settings saved.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Settings saved.', 'ultraguard-security-suite' ), 'success' )
            ->send();
    }

    // ── Private helpers ────────────────────────────────────────────────────────

    /**
     * BUG FIX: auto-cancel scans that have been "running" for >10 minutes at ≤5%.
     */
    private function maybe_cancel_stale_scan( array $status ): array {
        if ( ( $status['status'] ?? '' ) !== 'running' ) {
            return $status;
        }

        $started = isset( $status['started'] ) ? strtotime( $status['started'] ) : 0;
        $elapsed = $started ? ( time() - $started ) : PHP_INT_MAX;
        $prog    = (int) ( $status['progress'] ?? 0 );

        if ( ( $elapsed > self::STALE_SECONDS && $prog <= 5 ) || $elapsed > 1200 ) {
            delete_transient( self::RUNNING_TRANSIENT );
            $status['status']   = 'error';
            $status['progress'] = 0;
            $status['error']    = __( 'Scan timed out. This usually means WP-Cron is not running. Check your cron configuration.', 'ultraguard-security-suite' );
            set_transient( self::STATUS_TRANSIENT, $status, HOUR_IN_SECONDS );
        }

        return $status;
    }

    private function verify(): void {
        if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
            AjaxResponseBuilder::make()
                ->nonceError( __( 'Security check failed.', 'ultraguard-security-suite' ) )
                ->send( 403 );
        }
        if ( ! \UltraguardPro\ultraguard_current_user_can_manage() ) {
            AjaxResponseBuilder::make()
                ->permissionError( __( 'Permission denied.', 'ultraguard-security-suite' ) )
                ->send( 403 );
        }
    }

    private function get_table( string $key ) {
        return $this->plugin->modules->get_db_tables()->get_table( $key );
    }

    private function find_plugin_file( string $slug ): ?string {
        if ( ! function_exists( 'get_plugins' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }
        foreach ( array_keys( get_plugins() ) as $plugin_file ) {
            if ( dirname( $plugin_file ) === $slug || basename( $plugin_file, '.php' ) === $slug ) {
                return $plugin_file;
            }
        }
        return null;
    }
    // ── Realtime fallback handler ─────────────────────────────────────────────

    /**
     * RT fallback: vuln_scan_status channel.
     * Reads from the same STATUS_TRANSIENT used by handle_scan_status() so the
     * data shape is consistent across SSE and AJAX delivery paths.
     */
    public function handle_rt_vuln_scan_status(): void {
        if ( ! check_ajax_referer( 'ultraguard_admin_nonce', 'nonce', false ) ) {
            AjaxResponseBuilder::make()
                ->nonceError( __( 'Security check failed.', 'ultraguard-security-suite' ) )
                ->send( 403 );
        }
        if ( ! \UltraguardPro\ultraguard_current_user_can_manage() ) {
            AjaxResponseBuilder::make()
                ->permissionError( __( 'Permission denied.', 'ultraguard-security-suite' ) )
                ->send( 403 );
            return;
        }

        $status = get_transient( self::STATUS_TRANSIENT ) ?: [
            'scan_id'  => '',
            'status'   => 'idle',
            'progress' => 0,
            'phase'    => '',
        ];

        AjaxResponseBuilder::make()
            ->success()
            ->addData( 'scan_id',       $status['scan_id']       ?? '' )
            ->addData( 'status',        $status['status']        ?? 'idle' )
            ->addData( 'progress',      (int) ( $status['progress'] ?? 0 ) )
            ->addData( 'phase',         $status['phase']         ?? '' )
            ->addData( 'threats_found', (int) ( $status['threats_found'] ?? 0 ) )
            ->addData( 'completed_at',  $status['completed_at']  ?? '' )
            ->addData( 'error',         $status['error']         ?? '' )
            ->send();
    }

}
