<?php
/**
 * VulnerabilityModule — CVE scanning and virtual patching feature module.
 *
 * BUG FIXES applied:
 *   - process_scan() now accepts $scan_id passed through from the cron event
 *   - ultraguard_vuln_scan_running is cleaned up in the finally block (not just on success)
 *   - ultraguard_vuln_stop_scan AJAX hook wired through VulnerabilityAjax
 *
 * @package UltraguardPro\modules\vulnerability
 */

namespace UltraguardPro\modules\vulnerability;

use UltraguardPro\modules\core\AbstractModule;
use UltraguardPro\core\MailManager;
use UltraguardPro\support\CronManager;
use UltraguardPro\support\LicenseInfo;
use UltraguardPro\core\Logger;

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

class VulnerabilityModule extends AbstractModule {

    private const STATUS_TRANSIENT  = 'ultraguard_vuln_scan_status';
    private const RUNNING_TRANSIENT = 'ultraguard_vuln_scan_running';

    public function get_id(): string { return 'vulnerability'; }

    protected function requires_pro(): bool { return true; }

    protected function allow_preview_mode(): bool { return true; }

    public function boot(): void {
        // 1. Register DB schema
        $_db_tables = $this->plugin->modules->get_db_tables();
        if ( $_db_tables ) {
            $_db_tables->register_schema( new schema\VulnerabilitySchema() );
        }

        // 2. Register AJAX handlers (includes stop_scan)
        ( new ajax\VulnerabilityAjax( $this->plugin ) )->register();

        // 3. Admin pages
        add_action( 'ultraguard_register_admin_pages', [ $this, 'register_pages' ], 32, 1 );

        if ( $this->is_preview_mode() ) {
            do_action( 'ultraguard_pro/vulnerability/preview_booted', $this );
            return;
        }

        // 4. Daily vulnerability scan cron (Pro only — checked inside callback)
        // BUG FIX: cron event now passes $scan_id so process_scan can update the correct transient
        add_action( 'ultraguard_pro_vulnerability_scan', [ $this, 'process_scan' ] );

        // 5. Register the cron schedule
        CronManager::make()
            ->hook( 'ultraguard_pro_vulnerability_scan' )
            ->label( 'UltraGuard Vulnerability Scan' )
            ->recurrence( CronManager::DAILY )
            ->register();

        // 6. Cleanup expired virtual patches daily
        add_action( 'ultraguard_pro_daily_scan', [ $this, 'cleanup_expired_patches' ] );

        // 7. Auto-remove patches when components are updated
        add_action( 'upgrader_process_complete', [ $this, 'handle_component_updated' ], 10, 2 );
        add_filter( 'ultraguard_pro/compliance/available_sections', [ $this, 'add_compliance_sections' ] );
        add_filter( 'ultraguard_pro/compliance/collect_data', [ $this, 'add_compliance_data' ], 10, 4 );
        add_action( 'ultraguard_vulnerability_threats_found', [ $this, 'send_critical_findings_notification' ], 10, 1 );

        do_action( 'ultraguard_pro/vulnerability/booted', $this );
    }

    // ── Admin pages ────────────────────────────────────────────────────────────

    public function register_pages( $admin ): void {
        $admin->register_page( [
            'id'              => 'vulnerability-scanner',
            'page_title'      => __( 'Vulnerability Scanner', 'ultraguard-security-suite' ),
            'menu_title'      => __( 'Vulnerabilities', 'ultraguard-security-suite' ),
            'capability'      => 'manage_options',
            'scheme_callback' => [ pages\VulnerabilityDashboard::class, $this->plugin ],
            'is_parent'       => true,
            'priority'        => 15,
            'group'           => 'Detection',
            'icon'            => 'dashicons-shield-alt',
        ] );

        $subpages = [
            [
                'id'       => 'vulnerability-results',
                'title'    => __( 'CVE Results', 'ultraguard-security-suite' ),
                'menu'     => __( 'CVE Results', 'ultraguard-security-suite' ),
                'class'    => pages\VulnerabilityResults::class,
                'priority' => 16,
            ],
            [
                'id'       => 'virtual-patches',
                'title'    => __( 'Virtual Patches', 'ultraguard-security-suite' ),
                'menu'     => __( 'Virtual Patches', 'ultraguard-security-suite' ),
                'class'    => pages\VirtualPatchesPage::class,
                'priority' => 17,
            ],
        ];

        foreach ( $subpages as $page ) {
            $admin->register_page( [
                'id'              => $page['id'],
                'page_title'      => $page['title'],
                'menu_title'      => $page['menu'],
                'capability'      => 'manage_options',
                'scheme_callback' => [ $page['class'], $this->plugin ],
                'parent_id'       => 'vulnerability-scanner',
                'priority'        => $page['priority'],
            'group'           => 'Detection',
            ] );
        }
    }

    public function add_compliance_sections( array $sections ): array {
        $sections['vulnerability_findings'] = __( 'Vulnerability Findings', 'ultraguard-security-suite' );
        return $sections;
    }

    public function add_compliance_data( array $sections, string $type, array $options, array $meta ): array {
        $requested = isset( $options['sections'] ) && is_array( $options['sections'] ) ? $options['sections'] : [];
        if ( $requested && ! in_array( 'vulnerability_findings', $requested, true ) ) {
            return $sections;
        }

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

        if ( ! $vuln_table ) {
            return $sections;
        }

        $summary      = method_exists( $vuln_table, 'get_summary' ) ? $vuln_table->get_summary() : [
            'total'           => (int) $vuln_table->count(),
            'open'            => (int) $vuln_table->count( [ 'status' => 'open' ] ),
            'critical'        => (int) $vuln_table->count( [ 'severity' => 'critical', 'status' => 'open' ] ),
            'high'            => (int) $vuln_table->count( [ 'severity' => 'high', 'status' => 'open' ] ),
            'virtual_patched' => (int) $vuln_table->count( [ 'virtual_patch_active' => 1 ] ),
        ];
        $recent_findings = method_exists( $vuln_table, 'get_recent' ) ? $vuln_table->get_recent( 20 ) : $vuln_table->get_by( [], 20, 'updated_at', 'DESC' );

        $sections['vulnerability_findings'] = [
            'title'   => __( 'Vulnerability Findings', 'ultraguard-security-suite' ),
            'content' => [
                'summary' => [
                    'total_findings'   => (int) ( $summary['total'] ?? 0 ),
                    'open_findings'    => (int) ( $summary['open'] ?? 0 ),
                    'critical_findings'=> (int) ( $summary['critical'] ?? 0 ),
                    'high_findings'    => (int) ( $summary['high'] ?? 0 ),
                    'virtual_patches'  => $patch_table ? (int) $patch_table->count() : 0,
                ],
                'findings' => array_map( static function( array $row ): array {
                    return [
                        'component'    => $row['component_slug'] ?? '',
                        'cve_id'       => $row['cve_id'] ?? '',
                        'severity'     => $row['severity'] ?? '',
                        'status'       => $row['status'] ?? '',
                        'first_seen'   => $row['first_seen'] ?? '',
                        'last_checked' => $row['last_checked'] ?? '',
                    ];
                }, $recent_findings ),
            ],
        ];

        return $sections;
    }

    // ── Cron callbacks ─────────────────────────────────────────────────────────

    /**
     * Run a full vulnerability scan.
     * Pro-gated — silently skips if not Pro.
     *
     * BUG FIX: $scan_id accepted as parameter (passed from cron event).
     * BUG FIX: both running/lock transients deleted in finally so they're
     *           always cleaned up regardless of success or error.
     *
     * @param string $scan_id  Unique scan ID set by handle_run_scan().
     */
    public function process_scan( string $scan_id = '' ): void {
        if ( ! LicenseInfo::instance()->isPro() ) {
            return;
        }

        // Concurrency guard
        $lock_key = 'ultraguard_vuln_scan_lock';
        if ( get_transient( $lock_key ) ) {
            return;
        }
        set_transient( $lock_key, 1, 10 * MINUTE_IN_SECONDS );

        // If scan_id wasn't passed (e.g. legacy daily cron), generate one
        if ( ! $scan_id ) {
            $scan_id = 'vuln_' . wp_generate_uuid4();
        }

        set_transient( self::STATUS_TRANSIENT, [
            'scan_id'  => $scan_id,
            'status'   => 'running',
            'progress' => 5,
            'phase'    => __( 'Initialising scan…', 'ultraguard-security-suite' ),
            'started'  => gmdate( 'Y-m-d H:i:s' ),
        ], HOUR_IN_SECONDS );

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

            if ( ! $vuln_table || ! $patch_table ) {
                throw new \RuntimeException( esc_html( 'DB tables unavailable' ) );
            }

            $api_key = (string) $this->option( 'wpscan_api_key', '' );
            $scanner = new VulnerabilityScanner( $vuln_table, $api_key );
            $counts  = $scanner->scan_installed_components();

            // Auto-apply virtual patches for high/critical with no fix (if enabled)
            $patched = 0;
            if ( (bool) $this->option( 'vulnerability_virtual_patch', true ) ) {
                $patcher = new VirtualPatcher( $vuln_table, $patch_table );
                $patched = $patcher->sync_patches_from_api();
            }

            set_transient( self::STATUS_TRANSIENT, [
                'scan_id'        => $scan_id,
                'status'         => 'complete',
                'progress'       => 100,
                'phase'          => __( 'Scan complete', 'ultraguard-security-suite' ),
                'threats_found'  => $counts['total'],
                'completed_at'   => gmdate( 'Y-m-d H:i:s' ),
            ], DAY_IN_SECONDS );

            $this->plugin->settings->save_module_settings( 'vulnerability', [
                'vulnerability_last_scan' => gmdate( 'Y-m-d H:i:s' ),
            ] );

            if ( $counts['total'] > 0 && (bool) $this->option( 'vulnerability_notify_on_critical', true ) ) {
                do_action( 'ultraguard_vulnerability_threats_found', $counts );
            }

            do_action( 'ultraguard_vulnerability_scan_complete', $counts );

            Logger::info( '[VulnerabilityModule] Scan complete', array_merge( $counts, [ 'patches_applied' => $patched ] ) );

        } catch ( \Throwable $e ) {
            Logger::error( '[VulnerabilityModule] Scan failed', [ 'error' => $e->getMessage() ] );
            set_transient( self::STATUS_TRANSIENT, [
                'scan_id'  => $scan_id,
                'status'   => 'error',
                'progress' => 0,
                'error'    => $e->getMessage(),
            ], HOUR_IN_SECONDS );
        } finally {
            // BUG FIX: both keys cleaned here so they're always released
            delete_transient( $lock_key );
            delete_transient( self::RUNNING_TRANSIENT );
        }
    }

    /**
     * Remove virtual patches for components that just got updated.
     */
    public function handle_component_updated( $upgrader, array $hook_extra ): void {
        $type = $hook_extra['type'] ?? '';
        if ( ! in_array( $type, [ 'plugin', 'theme' ], true ) ) {
            return;
        }

        $vuln_table  = $this->get_table( 'vulnerability_results' );
        $patch_table = $this->get_table( 'virtual_patches' );
        if ( ! $vuln_table || ! $patch_table ) {
            return;
        }

        $slugs   = (array) ( $hook_extra['plugins'] ?? $hook_extra['themes'] ?? [] );
        $patcher = new VirtualPatcher( $vuln_table, $patch_table );

        foreach ( $slugs as $slug_file ) {
            $slug           = ( $type === 'plugin' ) ? dirname( $slug_file ) : $slug_file;
            $active_patches = $patch_table->get_by( [ 'component_slug' => $slug ], 50 );

            foreach ( $active_patches as $patch ) {
                $patch = (array) $patch;
                if ( ! empty( $patch['cve_id'] ) ) {
                    $new_version = $type === 'plugin'
                        ? ( get_plugin_data( WP_PLUGIN_DIR . '/' . $slug_file )['Version'] ?? '' )
                        : ( wp_get_theme( $slug )->get( 'Version' ) ?: '' );
                    $patcher->remove_patch_when_fixed( $patch['cve_id'], $new_version );
                }
            }
        }
    }

    /**
     * Remove expired virtual patches. Hooked on daily scan.
     */
    public function cleanup_expired_patches(): void {
        if ( ! LicenseInfo::instance()->isPro() ) {
            return;
        }

        $vuln_table  = $this->get_table( 'vulnerability_results' );
        $patch_table = $this->get_table( 'virtual_patches' );
        if ( ! $vuln_table || ! $patch_table ) {
            return;
        }

        ( new VirtualPatcher( $vuln_table, $patch_table ) )->cleanup_expired_patches();
    }

    /**
     * Send an admin email when critical vulnerability findings are present.
     *
     * @param array $counts
     * @return void
     */
    public function send_critical_findings_notification( array $counts ): void {
        if ( ! (bool) $this->option( 'vulnerability_notify_on_critical', true ) ) {
            return;
        }

        $email = sanitize_email( (string) get_option( 'admin_email', '' ) );
        if ( ! is_email( $email ) ) {
            return;
        }

        $vuln_table = $this->get_table( 'vulnerability_results' );
        $critical   = $vuln_table ? (array) $vuln_table->get_by( [ 'status' => 'open', 'severity' => 'critical' ], 5, 'cvss_score', 'DESC' ) : [];

        MailManager::notification( [
            'to'        => $email,
            'subject'   => __( 'UltraGuard detected critical vulnerability findings', 'ultraguard-security-suite' ),
            'title'     => __( 'Critical vulnerabilities discovered', 'ultraguard-security-suite' ),
            'message'   => __( 'UltraGuard found open high-risk vulnerabilities during the latest scan and recommends immediate review.', 'ultraguard-security-suite' ),
            'tone'      => 'danger',
            'details'   => [
                __( 'Total findings', 'ultraguard-security-suite' )   => (int) ( $counts['total'] ?? 0 ),
                __( 'Critical findings', 'ultraguard-security-suite' ) => (int) ( $counts['critical'] ?? 0 ),
                __( 'High findings', 'ultraguard-security-suite' )     => (int) ( $counts['high'] ?? 0 ),
                __( 'Top CVEs', 'ultraguard-security-suite' )          => array_values( array_filter( array_map( static function ( array $row ): string {
                    $component = (string) ( $row['component_slug'] ?? '' );
                    $cve       = (string) ( $row['cve_id'] ?? '' );
                    return trim( $component . ( $cve ? ' - ' . $cve : '' ) );
                }, $critical ) ) ),
            ],
            'cta_url'   => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard&ug_page=vulnerability-results' ),
            'cta_label' => __( 'Review Vulnerabilities', 'ultraguard-security-suite' ),
        ] );
    }

    // ── Protected helpers ──────────────────────────────────────────────────────

    protected function get_table( string $key ) {
        return ( $this->plugin->modules->get_db_tables() ? $this->plugin->modules->get_db_tables()->get_table( $key ) : null );
    }
}
