<?php
/**
 * DatabaseScannerAjax — AJAX handlers for the Database Scanner module.
 *
 * Registered actions:
 *   ultraguard_db_scanner_start_scan
 *   ultraguard_db_scanner_stop_scan        ← NEW
 *   ultraguard_db_scanner_scan_status
 *   ultraguard_db_scanner_clean_injection
 *   ultraguard_db_scanner_ignore_result
 *   ultraguard_db_scanner_export_results
 *
 * BUG FIXES:
 *   - stop_scan action added
 *   - handle_scan_status() now has stale-scan auto-cancellation
 *
 * @package UltraguardPro\modules\database_scanner\ajax
 */

namespace UltraguardPro\modules\database_scanner\ajax;

use UltraguardPro\Plugin;
use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\core\Logger;
use UltraguardPro\support\AjaxResponseBuilder;
use UltraguardPro\support\LicenseInfo;
use UltraguardPro\support\UpgradeManager;
use UltraguardPro\modules\database_scanner\DatabaseScannerEngine;

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

// phpcs:disable WordPress.Security.NonceVerification.Missing -- Database Scanner AJAX handlers verify nonce centrally via $this->cap().

class DatabaseScannerAjax {

    private const NONCE_ACTION      = 'ultraguard_admin_nonce';
    private const STATUS_TRANSIENT  = 'ultraguard_db_scan_status';
    private const RUNNING_TRANSIENT = 'ultraguard_db_scan_running';
    private const STALE_SECONDS     = 600;

    private Plugin $plugin;

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

    public function register(): void {
        $actions = [
            'ultraguard_db_scanner_start_scan'       => 'handle_start_scan',
            'ultraguard_db_scanner_stop_scan'        => 'handle_stop_scan',
            'ultraguard_db_scanner_scan_status'      => 'handle_scan_status',
            'ultraguard_db_scanner_clean_injection'  => 'handle_clean_injection',
            'ultraguard_db_scanner_ignore_result'    => 'handle_ignore_result',
            'ultraguard_db_scanner_delete_result'    => 'handle_delete_result',
            'ultraguard_db_scanner_export_results'   => 'handle_export_results',
            'ultraguard_save_db_scanner_settings'    => 'handle_save_settings',
            // ── Realtime fallback polling ─────────────────────────────────────
            'ultraguard_rt_db_scan_status'           => 'handle_rt_db_scan_status',
        ];

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

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

    public function handle_start_scan(): void {
        $this->cap();

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

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

        $cursor  = absint( $_POST['cursor'] ?? 0 );
        $scan_id = 'dbscan_' . 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,
            'cursor'   => $cursor,
            '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_db_scanner_run_scan', [ $scan_id ] );

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

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

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

        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( __( 'Database scan stopped.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Scan cancelled.', 'ultraguard-security-suite' ), 'info' )
            ->send();
    }

    /**
     * Return live progress JSON for dashboard polling.
     * BUG FIX: stale-scan auto-cancellation added.
     */
    public function handle_scan_status(): void {
        $this->cap();

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

        // BUG FIX: auto-cancel stale scans
        $status = $this->maybe_cancel_stale_scan( $status );

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

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

    /**
     * Clean a single database injection with before/after preview.
     *
     * POST: result_id (int), clean_value (string).
     */
    public function handle_clean_injection(): void {
        $this->cap();

        if ( ! LicenseInfo::instance()->isPro() ) {
            UpgradeManager::soft_gate_response(
                'database_scan',
                __( 'clean a database injection', 'ultraguard-security-suite' )
            );
            return;
        }

        $result_id = absint( $_POST['result_id'] ?? 0 );
        $clean_val = sanitize_textarea_field( wp_unslash( $_POST['clean_value'] ?? '' ) );

        if ( ! $result_id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid result ID.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        $result = DatabaseCrud::get_by_id( 'ultraguard_db_scan_results', $result_id );
        if ( ! $result ) {
            AjaxResponseBuilder::make()->error( __( 'Scan result not found.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        $r       = (array) $result;
        $scan_id = $r['scan_id']     ?? '';
        $table   = $r['db_table']    ?? '';
        $col     = $r['column_name'] ?? '';
        $row_id  = (int) ( $r['row_id'] ?? 0 );

        $engine  = new DatabaseScannerEngine( $scan_id );
        $success = $engine->clean_db_injection( $table, $col, $row_id, '', $clean_val );

        if ( ! $success ) {
            AjaxResponseBuilder::make()
                ->error( __( 'Could not clean the injection. Check server error log for details.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'DB injection clean failed.', 'ultraguard-security-suite' ), 'error' )
                ->send();
            return;
        }

        $row_sel = '#ug-db-row-' . $result_id;

        AjaxResponseBuilder::make()
            ->success( __( 'Injection cleaned successfully.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Database injection removed.', 'ultraguard-security-suite' ), 'success' )
            ->addUpdate(
                $row_sel . ' .ultraguard-cell-status',
                '<span class="ultraguard-cell-inner success">' . esc_html__( 'Cleaned', 'ultraguard-security-suite' ) . '</span>'
            )
            ->emptyElement( $row_sel . ' .ultraguard-cell-actions' )
            ->send();
    }

    /**
     * Mark a scan result as a false positive.
     *
     * POST: result_id (int).
     */
    public function handle_ignore_result(): void {
        $this->cap();

        if ( ! LicenseInfo::instance()->isPro() ) {
            UpgradeManager::soft_gate_response(
                'database_scan',
                __( 'mark database findings as false positives', 'ultraguard-security-suite' )
            );
            return;
        }

        $result_id = absint( $_POST['result_id'] ?? 0 );

        if ( ! $result_id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid result ID.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        DatabaseCrud::update(
            'ultraguard_db_scan_results',
            [ 'status' => 'false_positive' ],
            [ 'id' => $result_id ]
        );

        $row_sel = '#ug-db-row-' . $result_id;

        AjaxResponseBuilder::make()
            ->success( __( 'Result marked as false positive.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Marked as false positive.', 'ultraguard-security-suite' ), 'info' )
            ->addUpdate(
                $row_sel . ' .ultraguard-cell-status',
                '<span class="ultraguard-cell-inner secondary">' . esc_html__( 'False Positive', 'ultraguard-security-suite' ) . '</span>'
            )
            ->emptyElement( $row_sel . ' .ultraguard-cell-actions' )
            ->send();
    }

    /**
     * Export current scan results as a CSV download.
     */
    public function handle_export_results(): void {
        $this->cap();

        if ( ! LicenseInfo::instance()->isPro() ) {
            UpgradeManager::soft_gate_response(
                'database_scan',
                __( 'export database scan results', 'ultraguard-security-suite' )
            );
            return;
        }

        $upload_dir = wp_upload_dir();
        $export_dir = trailingslashit( $upload_dir['basedir'] ) . 'ultraguard-security-suite/exports/';
        wp_mkdir_p( $export_dir );

        $htaccess = $export_dir . '.htaccess';
        if ( ! file_exists( $htaccess ) ) {
            file_put_contents( $htaccess, "Deny from all\nOptions -Indexes\n" );
        }

        $filename = 'db-scan-results-' . gmdate( 'Y-m-d-His' ) . '.csv';
        $filepath = $export_dir . $filename;

        $rows = DatabaseCrud::get_records( 'ultraguard_db_scan_results', [
            'orderby' => 'created_at',
            'order'   => 'DESC',
            'limit'   => 5000,
        ] );

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming CSV export to a plugin-owned temporary file.
        $fp = fopen( $filepath, 'w' );
        if ( ! $fp ) {
            AjaxResponseBuilder::make()->error( __( 'Could not create export file.', 'ultraguard-security-suite' ) )->send();
            return;
        }

        fputcsv( $fp, [ 'ID', 'Scan ID', 'Table', 'Column', 'Row ID', 'Threat Type', 'Severity', 'Matched Pattern', 'Excerpt', 'Status', 'Cleaned At', 'Found At' ] );

        foreach ( $rows as $r ) {
            $r = (array) $r;
            fputcsv( $fp, [
                $r['id']              ?? '',
                $r['scan_id']         ?? '',
                $r['db_table']        ?? '',
                $r['column_name']     ?? '',
                $r['row_id']          ?? '',
                $r['threat_type']     ?? '',
                $r['threat_level']    ?? '',
                $r['matched_pattern'] ?? '',
                $r['content_excerpt'] ?? '',
                $r['status']          ?? '',
                $r['cleaned_at']      ?? '',
                $r['created_at']      ?? '',
            ] );
        }
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closes the CSV export handle opened above.
        fclose( $fp );

        AjaxResponseBuilder::make()
            ->success( __( 'Export ready.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'DB scan results exported.', 'ultraguard-security-suite' ), 'success' )
            ->send( 200, [
                'download_url' => trailingslashit( $upload_dir['baseurl'] ) . 'ultraguard-security-suite/exports/' . $filename,
                'records'      => count( $rows ),
            ] );
    }

    /**
     * Save database scanner settings.
     */
    public function handle_save_settings(): void {
        $this->cap();

        if ( ! LicenseInfo::instance()->isPro() ) {
            UpgradeManager::soft_gate_response(
                'database_scan',
                __( 'save database scanner settings', 'ultraguard-security-suite' )
            );
            return;
        }

        $scheduled = filter_var( wp_unslash( $_POST['database_scanner_scheduled'] ?? '0' ), FILTER_VALIDATE_BOOLEAN );

        $ok = $this->plugin->settings->save_module_settings( 'database_scanner', [
            'database_scanner_scheduled' => $scheduled,
        ] );

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

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

    /**
     * Hard-delete a single DB scan result log entry.
     *
     * Available to all users (not Pro-gated) — deleting a log entry is
     * non-destructive to the actual database content.
     *
     * POST: result_id (int)
     */
    public function handle_delete_result(): void {
        $this->cap();

        $result_id = absint( $_POST['result_id'] ?? 0 );

        if ( ! $result_id ) {
            AjaxResponseBuilder::make()
                ->error( __( 'Invalid result ID.', 'ultraguard-security-suite' ) )
                ->send();
            return;
        }

        $table = $this->plugin->modules->get_db_tables()->get_table( 'db_scan_results' );

        if ( $table && $table->delete( $result_id ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'Scan result deleted.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Result deleted.', 'ultraguard-security-suite' ), 'success' )
                ->send();
        } else {
            AjaxResponseBuilder::make()
                ->error( __( 'Could not delete scan result.', 'ultraguard-security-suite' ) )
                ->send();
        }
    }

    // ── Stale scan detection ───────────────────────────────────────────────────

    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;
    }

    // ── Security ───────────────────────────────────────────────────────────────

    private function cap(): 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 );
        }
    }
    // ── Realtime fallback handler ─────────────────────────────────────────────

    /**
     * RT fallback: db_scan_status channel.
     * Reuses the same STATUS_TRANSIENT read as handle_scan_status() so the data
     * shape is always consistent regardless of delivery path (SSE vs AJAX).
     */
    public function handle_rt_db_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'    => '',
        ];

        // Auto-cancel stale scans (same logic as handle_scan_status)
        $status = $this->maybe_cancel_stale_scan( $status );

        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( 'total_checked', (int) ( $status['total_checked'] ?? 0 ) )
            ->send();
    }

}

// phpcs:enable
