<?php
/**
 * VulnerabilityScanner — Identifies installed plugins/themes/core with known CVEs.
 *
 * Data sources (in order of preference):
 *   1. WPScan Vulnerability Database REST API (free tier: 25 req/day).
 *   2. Patchstack API (fallback, no key required for basic lookups).
 *   3. Local signature table (ultraguard_malware_signatures type='vulnerability').
 *
 * BUG FIX: Progress transient is now updated after each component phase so the
 * UI can show live percentage instead of jumping 0 → 100.
 *
 * @package UltraguardPro\modules\vulnerability
 */

namespace UltraguardPro\modules\vulnerability;

use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\core\Logger;
use UltraguardPro\modules\vulnerability\db\VulnerabilityResultsTable;

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

class VulnerabilityScanner {

    private const WPSCAN_API_BASE     = 'https://wpscan.com/api/v3/';
    private const PATCHSTACK_API_BASE = 'https://psapi.patchstack.com/v1/';
    private const CACHE_TTL           = 6 * HOUR_IN_SECONDS;
    private const STATUS_TRANSIENT    = 'ultraguard_vuln_scan_status';

    /** @var VulnerabilityResultsTable */
    private VulnerabilityResultsTable $table;

    /** @var string */
    private string $wpscan_api_key;

    public function __construct( VulnerabilityResultsTable $table, string $wpscan_api_key = '' ) {
        $this->table          = $table;
        $this->wpscan_api_key = $wpscan_api_key;
    }

    // ── Orchestration ──────────────────────────────────────────────────────────

    /**
     * Run a full scan of all installed plugins, themes, and WordPress core.
     * Progress updates: plugins 5→60%, themes 60→80%, core 80→95%, done 100%.
     *
     * @return array{plugins:int,themes:int,core:int,total:int}
     */
    public function scan_installed_components(): array {
        $counts  = [ 'plugins' => 0, 'themes' => 0, 'core' => 0, 'total' => 0 ];
        $plugins = $this->get_installed_plugins();
        $themes  = $this->get_installed_themes();
        $total_p = count( $plugins );
        $total_t = count( $themes );

        // ── Plugins (5% → 60%) ────────────────────────────────────────────────
        foreach ( $plugins as $i => $component ) {
            // Check if scan has been cancelled before each component
            if ( $this->is_cancelled() ) {
                Logger::info( '[VulnerabilityScanner] Scan cancelled mid-run' );
                return $counts;
            }

            $vulns              = $this->check_plugin_vulnerabilities( $component['slug'], $component['version'] );
            $counts['plugins'] += $this->persist_findings( $vulns, 'plugin', $component['slug'], $component['version'] );

            // Update progress proportionally within the plugin slice (5–60%)
            $pct = $total_p > 0 ? (int) ( 5 + ( ( $i + 1 ) / $total_p ) * 55 ) : 60;
            $this->update_progress( $pct, sprintf(
                /* translators: %1$s: plugin slug, %2$d: current, %3$d: total */
                __( 'Checking plugins… %1$s (%2$d/%3$d)', 'ultraguard-security-suite' ),
                $component['slug'], $i + 1, $total_p
            ) );
        }

        // ── Themes (60% → 80%) ────────────────────────────────────────────────
        foreach ( $themes as $i => $component ) {
            if ( $this->is_cancelled() ) { return $counts; }

            $vulns             = $this->check_theme_vulnerabilities( $component['slug'], $component['version'] );
            $counts['themes'] += $this->persist_findings( $vulns, 'theme', $component['slug'], $component['version'] );

            $pct = $total_t > 0 ? (int) ( 60 + ( ( $i + 1 ) / $total_t ) * 20 ) : 80;
            $this->update_progress( $pct, sprintf(
                /* translators: Placeholder values are inserted into this string. */
                __( 'Checking themes… %1$s (%2$d/%3$d)', 'ultraguard-security-suite' ),
                $component['slug'], $i + 1, $total_t
            ) );
        }

        // ── WordPress Core (80% → 95%) ─────────────────────────────────────────
        if ( ! $this->is_cancelled() ) {
            $this->update_progress( 85, __( 'Checking WordPress core…', 'ultraguard-security-suite' ) );
            global $wp_version;
            $core_vulns      = $this->check_core_vulnerabilities( $wp_version ?? '' );
            $counts['core'] += $this->persist_findings( $core_vulns, 'core', 'wordpress', $wp_version ?? '' );
        }

        $counts['total'] = $counts['plugins'] + $counts['themes'] + $counts['core'];

        Logger::info( '[VulnerabilityScanner] Scan complete', $counts );

        return $counts;
    }

    // ── Component enumerators ──────────────────────────────────────────────────

    /**
     * List all installed plugins with their slug and version.
     *
     * @return array<int,array{slug:string,version:string,active:bool}>
     */
    private function get_installed_plugins(): array {
        if ( ! function_exists( 'get_plugins' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        $active  = (array) get_option( 'active_plugins', [] );
        $result  = [];

        foreach ( get_plugins() as $file => $data ) {
            $slug     = dirname( $file );
            $slug     = ( $slug === '.' ) ? basename( $file, '.php' ) : $slug;
            $result[] = [
                'slug'    => $slug,
                'version' => $data['Version'] ?? '',
                'active'  => in_array( $file, $active, true ),
            ];
        }

        return $result;
    }

    /**
     * List all installed themes with their slug and version.
     *
     * @return array<int,array{slug:string,version:string}>
     */
    private function get_installed_themes(): array {
        $result = [];
        foreach ( wp_get_themes() as $slug => $theme ) {
            $result[] = [
                'slug'    => $slug,
                'version' => $theme->get( 'Version' ) ?: '',
            ];
        }
        return $result;
    }

    // ── CVE lookup — Plugins ──────────────────────────────────────────────────

    private function check_plugin_vulnerabilities( string $slug, string $version ): array {
        $cache_key = 'ugvuln_p_' . md5( $slug . $version );
        $cached    = get_transient( $cache_key );
        if ( $cached !== false ) { return $cached; }

        $vulns = $this->query_wpscan_plugin( $slug, $version )
              ?: $this->query_patchstack_plugin( $slug, $version )
              ?: $this->check_local_signatures( $slug, 'plugin' );

        set_transient( $cache_key, $vulns, self::CACHE_TTL );
        return $vulns;
    }

    // ── CVE lookup — Themes ───────────────────────────────────────────────────

    private function check_theme_vulnerabilities( string $slug, string $version ): array {
        $cache_key = 'ugvuln_t_' . md5( $slug . $version );
        $cached    = get_transient( $cache_key );
        if ( $cached !== false ) { return $cached; }

        $vulns = $this->query_wpscan_theme( $slug, $version )
              ?: $this->query_patchstack_theme( $slug, $version )
              ?: [];

        set_transient( $cache_key, $vulns, self::CACHE_TTL );
        return $vulns;
    }

    // ── CVE lookup — Core ─────────────────────────────────────────────────────

    private function check_core_vulnerabilities( string $version ): array {
        $cache_key = 'ugvuln_core_' . md5( $version );
        $cached    = get_transient( $cache_key );
        if ( $cached !== false ) { return $cached; }

        $vulns = $this->query_wpscan_core( $version ) ?: [];
        set_transient( $cache_key, $vulns, self::CACHE_TTL );
        return $vulns;
    }

    // ── WPScan API ────────────────────────────────────────────────────────────

    private function query_wpscan_plugin( string $slug, string $version ): array {
        if ( empty( $this->wpscan_api_key ) ) { return []; }

        $url      = self::WPSCAN_API_BASE . 'plugins/' . urlencode( $slug );
        $response = $this->api_get( $url );
        if ( ! $response ) { return []; }

        return $this->parse_wpscan_response( $response, $slug, $version );
    }

    private function query_wpscan_theme( string $slug, string $version ): array {
        if ( empty( $this->wpscan_api_key ) ) { return []; }

        $url      = self::WPSCAN_API_BASE . 'themes/' . urlencode( $slug );
        $response = $this->api_get( $url );
        if ( ! $response ) { return []; }

        return $this->parse_wpscan_response( $response, $slug, $version );
    }

    private function query_wpscan_core( string $version ): array {
        if ( empty( $this->wpscan_api_key ) ) { return []; }

        $url      = self::WPSCAN_API_BASE . 'wordpresses/' . urlencode( str_replace( '.', '', $version ) );
        $response = $this->api_get( $url );
        if ( ! $response ) { return []; }

        return $this->parse_wpscan_response( $response, 'wordpress', $version );
    }

    private function parse_wpscan_response( array $body, string $slug, string $version ): array {
        $slug_data  = $body[ $slug ] ?? reset( $body );
        $vulns_raw  = $slug_data['vulnerabilities'] ?? [];
        $results    = [];

        foreach ( $vulns_raw as $v ) {
            $fixed_in = $v['fixed_in'] ?? null;

            // Skip if the installed version is already patched
            if ( $fixed_in && version_compare( $version, $fixed_in, '>=' ) ) {
                continue;
            }

            $cvss = (float) ( $v['cvss']['score'] ?? 0.0 );

            $results[] = [
                'cve_id'             => $v['references']['cve'][0] ?? ( 'WPSCAN-' . ( $v['id'] ?? '' ) ),
                'title'              => $v['title'] ?? '',
                'severity'           => $this->cvss_to_severity( $cvss ),
                'cvss_score'         => $cvss,
                'patched_in_version' => $fixed_in,
                'references'         => wp_json_encode( $v['references'] ?? [] ),
                'source'             => 'wpscan',
            ];
        }

        return $results;
    }

    // ── Patchstack API ────────────────────────────────────────────────────────

    private function query_patchstack_plugin( string $slug, string $version ): array {
        $url      = self::PATCHSTACK_API_BASE . 'advisories?type=plugin&slug=' . urlencode( $slug );
        $response = wp_remote_get( $url, [ 'timeout' => 10 ] );

        if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
            return [];
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ! is_array( $body ) ) { return []; }

        return $this->parse_patchstack_response( $body, $version );
    }

    private function query_patchstack_theme( string $slug, string $version ): array {
        $url      = self::PATCHSTACK_API_BASE . 'advisories?type=theme&slug=' . urlencode( $slug );
        $response = wp_remote_get( $url, [ 'timeout' => 10 ] );

        if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
            return [];
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ! is_array( $body ) ) { return []; }

        return $this->parse_patchstack_response( $body, $version );
    }

    private function parse_patchstack_response( array $body, string $version ): array {
        $results = [];

        foreach ( $body as $v ) {
            $fixed_in = $v['patched_in_version'] ?? null;
            if ( $fixed_in && version_compare( $version, $fixed_in, '>=' ) ) {
                continue;
            }

            $cvss      = (float) ( $v['cvss_score'] ?? 0.0 );
            $results[] = [
                'cve_id'             => $v['cve_id'] ?? ( 'PS-' . ( $v['id'] ?? '' ) ),
                'title'              => $v['title'] ?? '',
                'severity'           => $v['severity'] ?? $this->cvss_to_severity( $cvss ),
                'cvss_score'         => $cvss,
                'patched_in_version' => $fixed_in,
                'references'         => wp_json_encode( $v['references'] ?? [] ),
                'source'             => 'patchstack',
            ];
        }

        return $results;
    }

    // ── Local signatures fallback ─────────────────────────────────────────────

    private function check_local_signatures( string $slug, string $type ): array {
        $rows = DatabaseCrud::get_records(
            'ultraguard_malware_signatures',
            [
                'where'       => [ 'type' => 'vulnerability', 'is_active' => 1 ],
                'limit'       => 500,
                'orderby'     => 'id',
                'order'       => 'DESC',
                'output_type' => ARRAY_A,
            ]
        );

        if ( ! $rows ) { return []; }

        $results = [];
        foreach ( $rows as $sig ) {
            $results[] = [
                'cve_id'             => 'LOCAL-' . $sig['id'],
                'title'              => $sig['name'] ?? 'Local signature match',
                'severity'           => $sig['threat_level'] ?? 'medium',
                'cvss_score'         => 0.0,
                'patched_in_version' => null,
                'references'         => '{}',
                'source'             => 'local',
            ];
        }

        return $results;
    }

    // ── Persistence ───────────────────────────────────────────────────────────

    private function persist_findings(
        array  $vulns,
        string $component_type,
        string $slug,
        string $version
    ): int {
        $count = 0;

        foreach ( $vulns as $v ) {
            // Upsert: skip if this slug+CVE combo is already recorded
            $exists = DatabaseCrud::record_exists(
                'ultraguard_vulnerability_results',
                [ 'component_slug' => $slug, 'cve_id' => $v['cve_id'] ]
            );

            if ( $exists ) { continue; }

            $inserted = DatabaseCrud::insert_raw(
                'ultraguard_vulnerability_results',
                [
                    'component_slug'      => $slug,
                    'component_type'      => $component_type,
                    'component_version'   => $version,
                    'cve_id'              => $v['cve_id'],
                    'title'               => $v['title'],
                    'severity'            => $v['severity'],
                    'cvss_score'          => $v['cvss_score'],
                    'patched_in_version'  => $v['patched_in_version'],
                    'references'          => $v['references'] ?? '{}',
                    'source'              => $v['source']    ?? 'unknown',
                    'status'              => 'open',
                    'virtual_patch_active'=> 0,
                ]
            );

            if ( $inserted ) { $count++; }
        }

        return $count;
    }

    // ── Progress helpers ──────────────────────────────────────────────────────

    /**
     * Update the scan-status transient with current progress and phase label.
     */
    private function update_progress( int $pct, string $phase ): void {
        $current = get_transient( self::STATUS_TRANSIENT ) ?: [];
        set_transient( self::STATUS_TRANSIENT, array_merge( $current, [
            'status'   => 'running',
            'progress' => min( $pct, 99 ),
            'phase'    => $phase,
        ] ), HOUR_IN_SECONDS );
    }

    /**
     * Check whether the scan has been cancelled externally (stop_scan AJAX hit).
     */
    private function is_cancelled(): bool {
        $status = get_transient( self::STATUS_TRANSIENT );
        return ( $status['status'] ?? '' ) === 'cancelled';
    }

    // ── Utilities ─────────────────────────────────────────────────────────────

    private function cvss_to_severity( float $score ): string {
        if ( $score >= 9.0 ) { return 'critical'; }
        if ( $score >= 7.0 ) { return 'high'; }
        if ( $score >= 4.0 ) { return 'medium'; }
        return 'low';
    }

    private function api_get( string $url ): ?array {
        $response = wp_remote_get( $url, [
            'timeout' => 15,
            'headers' => [
                'Authorization' => 'Token token=' . $this->wpscan_api_key,
                'User-Agent'    => 'UltraGuard-Security-Suite/' . ( defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '1.0' ),
            ],
        ] );

        if ( is_wp_error( $response ) ) {
            Logger::warning( '[VulnerabilityScanner] API error: ' . $response->get_error_message() );
            return null;
        }

        if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
            return null;
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        return is_array( $body ) ? $body : null;
    }
}
